mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
* 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
490 lines
13 KiB
Vue
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>
|