Files
element-plus/packages/components/input/src/input.vue
三咲智子 6503e55277 refactor(utils): migrate utils (#5949)
* refactor(utils-v2): migrate utils

* refactor(utils-v2): migrate utils

* refactor(utils-v2): migrate utils

* refactor(utils): remove

* refactor(utils): rename

* refactor(utils): move EVENT_CODE to constants

* refactor: remove generic
2022-02-11 11:03:15 +08:00

490 lines
13 KiB
Vue

<template>
<div
v-show="type !== 'hidden'"
:class="[
type === 'textarea' ? nsTextarea.b() : nsInput.b(),
nsInput.m(inputSize),
nsInput.is('disabled', inputDisabled),
nsInput.is('exceed', inputExceed),
{
[nsInput.b('group')]: $slots.prepend || $slots.append,
[nsInput.bm('group', 'append')]: $slots.append,
[nsInput.bm('group', 'prepend')]: $slots.prepend,
[nsInput.m('prefix')]: $slots.prefix || prefixIcon,
[nsInput.m('suffix')]:
$slots.suffix || suffixIcon || clearable || showPassword,
[nsInput.m('suffix--password-clear')]: clearable && showPassword,
},
$attrs.class,
]"
:style="containerStyle"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<!-- input -->
<template v-if="type !== 'textarea'">
<!-- prepend slot -->
<div v-if="$slots.prepend" :class="nsInput.be('group', 'prepend')">
<slot name="prepend" />
</div>
<input
ref="input"
:class="nsInput.e('inner')"
v-bind="attrs"
:type="showPassword ? (passwordVisible ? 'text' : 'password') : type"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autocomplete"
:tabindex="tabindex"
:aria-label="label"
:placeholder="placeholder"
:style="inputStyle"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
@keydown="handleKeydown"
/>
<!-- prefix slot -->
<span v-if="$slots.prefix || prefixIcon" :class="nsInput.e('prefix')">
<span :class="nsInput.e('prefix-inner')">
<slot name="prefix"></slot>
<el-icon v-if="prefixIcon" :class="nsInput.e('icon')">
<component :is="prefixIcon" />
</el-icon>
</span>
</span>
<!-- suffix slot -->
<span v-if="suffixVisible" :class="nsInput.e('suffix')">
<span :class="nsInput.e('suffix-inner')">
<template v-if="!showClear || !showPwdVisible || !isWordLimitVisible">
<slot name="suffix"></slot>
<el-icon v-if="suffixIcon" :class="nsInput.e('icon')">
<component :is="suffixIcon" />
</el-icon>
</template>
<el-icon
v-if="showClear"
:class="[nsInput.e('icon'), nsInput.e('clear')]"
@mousedown.prevent
@click="clear"
>
<circle-close />
</el-icon>
<el-icon
v-if="showPwdVisible"
:class="[nsInput.e('icon'), nsInput.e('clear')]"
@click="handlePasswordVisible"
>
<icon-view />
</el-icon>
<span v-if="isWordLimitVisible" :class="nsInput.e('count')">
<span :class="nsInput.e('count-inner')">
{{ textLength }} / {{ attrs.maxlength }}
</span>
</span>
</span>
<el-icon
v-if="validateState && validateIcon && needStatusIcon"
:class="[nsInput.e('icon'), nsInput.e('validateIcon')]"
>
<component :is="validateIcon" />
</el-icon>
</span>
<!-- append slot -->
<div v-if="$slots.append" :class="nsInput.be('group', 'append')">
<slot name="append" />
</div>
</template>
<!-- textarea -->
<template v-else>
<textarea
ref="textarea"
:class="nsTextarea.e('inner')"
v-bind="attrs"
:tabindex="tabindex"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autocomplete"
:style="computedTextareaStyle"
:aria-label="label"
:placeholder="placeholder"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
@keydown="handleKeydown"
/>
<span v-if="isWordLimitVisible" :class="nsInput.e('count')">
{{ textLength }} / {{ attrs.maxlength }}
</span>
</template>
</div>
</template>
<script lang="ts">
import {
defineComponent,
computed,
watch,
nextTick,
getCurrentInstance,
ref,
shallowRef,
onMounted,
onUpdated,
} from 'vue'
import { isClient } from '@vueuse/core'
import { ElIcon } from '@element-plus/components/icon'
import { CircleClose, View as IconView } from '@element-plus/icons-vue'
import { ValidateComponentsMap, isObject, isKorean } from '@element-plus/utils'
import {
useAttrs,
useDisabled,
useFormItem,
useSize,
useNamespace,
} from '@element-plus/hooks'
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
import { calcTextareaHeight } from './calc-textarea-height'
import { inputProps, inputEmits } from './input'
import type { StyleValue } from 'vue'
type TargetElement = HTMLInputElement | HTMLTextAreaElement
const PENDANT_MAP = {
suffix: 'append',
prefix: 'prepend',
} as const
export default defineComponent({
name: 'ElInput',
components: { ElIcon, CircleClose, IconView },
inheritAttrs: false,
props: inputProps,
emits: inputEmits,
setup(props, { slots, emit, attrs: rawAttrs }) {
const instance = getCurrentInstance()!
const attrs = useAttrs()
const { form, formItem } = useFormItem()
const inputSize = useSize()
const inputDisabled = useDisabled()
const nsInput = useNamespace('input')
const nsTextarea = useNamespace('textarea')
const input = ref<HTMLInputElement>()
const textarea = ref<HTMLTextAreaElement>()
const focused = ref(false)
const hovering = ref(false)
const isComposing = ref(false)
const passwordVisible = ref(false)
const _textareaCalcStyle = shallowRef(props.inputStyle)
const inputOrTextarea = computed(() => input.value || textarea.value)
const needStatusIcon = computed(() => form?.statusIcon ?? false)
const validateState = computed(() => formItem?.validateState || '')
const validateIcon = computed(
() => ValidateComponentsMap[validateState.value]
)
const containerStyle = computed(() => rawAttrs.style as StyleValue)
const computedTextareaStyle = computed<StyleValue>(() => [
props.inputStyle,
_textareaCalcStyle.value,
{ resize: props.resize },
])
const nativeInputValue = computed(() =>
props.modelValue === null || props.modelValue === undefined
? ''
: String(props.modelValue)
)
const showClear = computed(
() =>
props.clearable &&
!inputDisabled.value &&
!props.readonly &&
!!nativeInputValue.value &&
(focused.value || hovering.value)
)
const showPwdVisible = computed(
() =>
props.showPassword &&
!inputDisabled.value &&
!props.readonly &&
(!!nativeInputValue.value || focused.value)
)
const isWordLimitVisible = computed(
() =>
props.showWordLimit &&
!!attrs.value.maxlength &&
(props.type === 'text' || props.type === 'textarea') &&
!inputDisabled.value &&
!props.readonly &&
!props.showPassword
)
const textLength = computed(() => Array.from(nativeInputValue.value).length)
const inputExceed = computed(
() =>
// show exceed style if length of initial value greater then maxlength
!!isWordLimitVisible.value &&
textLength.value > Number(attrs.value.maxlength)
)
const resizeTextarea = () => {
const { type, autosize } = props
if (!isClient || type !== 'textarea') return
if (autosize) {
const minRows = isObject(autosize) ? autosize.minRows : undefined
const maxRows = isObject(autosize) ? autosize.maxRows : undefined
_textareaCalcStyle.value = {
...calcTextareaHeight(textarea.value!, minRows, maxRows),
}
} else {
_textareaCalcStyle.value = {
minHeight: calcTextareaHeight(textarea.value!).minHeight,
}
}
}
const setNativeInputValue = () => {
const input = inputOrTextarea.value
if (!input || input.value === nativeInputValue.value) return
input.value = nativeInputValue.value
}
const calcIconOffset = (place: 'prefix' | 'suffix') => {
const { el } = instance.vnode
if (!el) return
const elList: HTMLSpanElement[] = Array.from(
el.querySelectorAll(`.${nsInput.e(place)}`)
)
const target = elList.find((item) => item.parentNode === el)
if (!target) return
const pendant = PENDANT_MAP[place]
if (slots[pendant]) {
target.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${
el.querySelector(`.${nsInput.be('group', pendant)}`).offsetWidth
}px)`
} else {
target.removeAttribute('style')
}
}
const updateIconOffset = () => {
calcIconOffset('prefix')
calcIconOffset('suffix')
}
const handleInput = (event: Event) => {
const { value } = event.target as TargetElement
// should not emit input during composition
// see: https://github.com/ElemeFE/element/issues/10516
if (isComposing.value) return
// hack for https://github.com/ElemeFE/element/issues/8548
// should remove the following line when we don't support IE
if (value === nativeInputValue.value) return
emit(UPDATE_MODEL_EVENT, value)
emit('input', value)
// ensure native input value is controlled
// see: https://github.com/ElemeFE/element/issues/12850
nextTick(setNativeInputValue)
}
const handleChange = (event: Event) => {
emit('change', (event.target as TargetElement).value)
}
const focus = () => {
// see: https://github.com/ElemeFE/element/issues/18573
nextTick(() => {
inputOrTextarea.value?.focus()
})
}
const blur = () => {
inputOrTextarea.value?.blur()
}
const handleFocus = (event: FocusEvent) => {
focused.value = true
emit('focus', event)
}
const handleBlur = (event: FocusEvent) => {
focused.value = false
emit('blur', event)
if (props.validateEvent) {
formItem?.validate?.('blur')
}
}
const select = () => {
inputOrTextarea.value?.select()
}
const handleCompositionStart = (event: CompositionEvent) => {
emit('compositionstart', event)
isComposing.value = true
}
const handleCompositionUpdate = (event: CompositionEvent) => {
emit('compositionupdate', event)
const text = (event.target as HTMLInputElement)?.value
const lastCharacter = text[text.length - 1] || ''
isComposing.value = !isKorean(lastCharacter)
}
const handleCompositionEnd = (event: CompositionEvent) => {
emit('compositionend', event)
if (isComposing.value) {
isComposing.value = false
handleInput(event)
}
}
const clear = () => {
emit(UPDATE_MODEL_EVENT, '')
emit('change', '')
emit('clear')
emit('input', '')
}
const handlePasswordVisible = () => {
passwordVisible.value = !passwordVisible.value
focus()
}
const suffixVisible = computed(
() =>
!!slots.suffix ||
!!props.suffixIcon ||
showClear.value ||
props.showPassword ||
isWordLimitVisible.value ||
(!!validateState.value && needStatusIcon.value)
)
watch(
() => props.modelValue,
() => {
nextTick(resizeTextarea)
if (props.validateEvent) {
formItem?.validate?.('change')
}
}
)
// native input value is set explicitly
// do not use v-model / :value in template
// see: https://github.com/ElemeFE/element/issues/14521
watch(nativeInputValue, () => setNativeInputValue())
// when change between <input> and <textarea>,
// update DOM dependent value and styles
// https://github.com/ElemeFE/element/issues/14857
watch(
() => props.type,
() => {
nextTick(() => {
setNativeInputValue()
resizeTextarea()
updateIconOffset()
})
}
)
onMounted(() => {
setNativeInputValue()
updateIconOffset()
nextTick(resizeTextarea)
})
onUpdated(() => {
nextTick(updateIconOffset)
})
const onMouseLeave = (evt: MouseEvent) => {
hovering.value = false
emit('mouseleave', evt)
}
const onMouseEnter = (evt: MouseEvent) => {
hovering.value = true
emit('mouseenter', evt)
}
const handleKeydown = (evt: KeyboardEvent) => {
emit('keydown', evt)
}
return {
input,
textarea,
attrs,
inputSize,
validateState,
validateIcon,
containerStyle,
computedTextareaStyle,
inputDisabled,
showClear,
showPwdVisible,
isWordLimitVisible,
textLength,
hovering,
inputExceed,
passwordVisible,
inputOrTextarea,
suffixVisible,
needStatusIcon,
resizeTextarea,
handleInput,
handleChange,
handleFocus,
handleBlur,
handleCompositionStart,
handleCompositionUpdate,
handleCompositionEnd,
handlePasswordVisible,
clear,
select,
focus,
blur,
onMouseLeave,
onMouseEnter,
handleKeydown,
nsInput,
nsTextarea,
}
},
})
</script>