三.Input
- 组件介绍Input 输入框
index.ts
js
import Input from './src/index.vue'
import type { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils/types'
Input.install = (app: App): void => {
app.component(Input.name, Input)
}
const _Input = Input as SFCWithInstall<typeof Input>
export default _Input
export const ElInput = _Input
src
index.vue
vue
<template>
<div
:class="[
type === 'textarea' ? 'el-textarea' : 'el-input',
inputSize ? 'el-input--' + inputSize : '',
{
'is-disabled': inputDisabled,
'is-exceed': inputExceed,
'el-input-group': $slots.prepend || $slots.append,
'el-input-group--append': $slots.append,
'el-input-group--prepend': $slots.prepend,
'el-input--prefix': $slots.prefix || prefixIcon,
'el-input--suffix':
$slots.suffix || suffixIcon || clearable || showPassword,
'el-input--suffix--password-clear': clearable && showPassword,
},
$attrs.class,
]"
:style="$attrs.style"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<template v-if="type !== 'textarea'">
<!-- 前置元素 -->
<div v-if="$slots.prepend" class="el-input-group__prepend">
<slot name="prepend"></slot>
</div>
<input
v-if="type !== 'textarea'"
ref="input"
class="el-input__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"
/>
<!-- 前置内容 -->
<span v-if="$slots.prefix || prefixIcon" class="el-input__prefix">
<slot name="prefix"></slot>
<i v-if="prefixIcon" :class="['el-input__icon', prefixIcon]"></i>
</span>
<!-- 后置内容 -->
<span v-if="getSuffixVisible()" class="el-input__suffix">
<span class="el-input__suffix-inner">
<template v-if="!showClear || !showPwdVisible || !isWordLimitVisible">
<slot name="suffix"></slot>
<i v-if="suffixIcon" :class="['el-input__icon', suffixIcon]"></i>
</template>
<i
v-if="showClear"
class="el-input__icon el-icon-circle-close el-input__clear"
@mousedown.prevent
@click="clear"
></i>
<i
v-if="showPwdVisible"
class="el-input__icon el-icon-view el-input__clear"
@click="handlePasswordVisible"
></i>
<span v-if="isWordLimitVisible" class="el-input__count">
<span class="el-input__count-inner">
{{ textLength }}/{{ maxlength }}
</span>
</span>
</span>
<i
v-if="validateState"
:class="['el-input__icon', 'el-input__validateIcon', validateIcon]"
></i>
</span>
<!-- 后置元素 -->
<div v-if="$slots.append" class="el-input-group__append">
<slot name="append"></slot>
</div>
</template>
<textarea
v-else
ref="textarea"
class="el-textarea__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"
>
</textarea>
<span
v-if="isWordLimitVisible && type === 'textarea'"
class="el-input__count"
>{{ textLength }}/{{ maxlength }}</span
>
</div>
</template>
<script lang="ts">
import {
defineComponent,
inject,
computed,
watch,
nextTick,
getCurrentInstance,
ref,
shallowRef,
onMounted,
onUpdated,
} from 'vue'
import { elFormKey, elFormItemKey } from '@element-plus/tokens'
import { useAttrs } from '@element-plus/hooks'
import {
UPDATE_MODEL_EVENT,
VALIDATE_STATE_MAP,
} from '@element-plus/utils/constants'
import { isObject, useGlobalConfig } from '@element-plus/utils/util'
import isServer from '@element-plus/utils/isServer'
import { isKorean } from '@element-plus/utils/isDef'
import { isValidComponentSize } from '@element-plus/utils/validators'
import calcTextareaHeight from './calcTextareaHeight'
import type { PropType } from 'vue'
import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens'
import type { ComponentSize } from '@element-plus/utils/types'
type AutosizeProp =
| {
minRows?: number
maxRows?: number
}
| boolean
const PENDANT_MAP = {
suffix: 'append',
prefix: 'prepend',
}
export default defineComponent({
name: 'ElInput',
inheritAttrs: false,
props: {
modelValue: {
type: [String, Number],
default: '',
},
type: {
type: String,
default: 'text',
},
size: {
type: String as PropType<ComponentSize>,
validator: isValidComponentSize,
},
resize: {
type: String as PropType<'none' | 'both' | 'horizontal' | 'vertical'>,
validator: (val: string) =>
['none', 'both', 'horizontal', 'vertical'].includes(val),
},
autosize: {
type: [Boolean, Object] as PropType<AutosizeProp>,
default: false as AutosizeProp,
},
autocomplete: {
type: String,
default: 'off',
},
placeholder: {
type: String,
},
form: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
clearable: {
type: Boolean,
default: false,
},
showPassword: {
type: Boolean,
default: false,
},
showWordLimit: {
type: Boolean,
default: false,
},
suffixIcon: {
type: String,
default: '',
},
prefixIcon: {
type: String,
default: '',
},
label: {
type: String,
},
tabindex: {
type: [Number, String],
},
validateEvent: {
type: Boolean,
default: true,
},
inputStyle: {
type: Object,
default: () => ({}),
},
maxlength: {
type: [Number, String],
},
},
emits: [
UPDATE_MODEL_EVENT,
'input',
'change',
'focus',
'blur',
'clear',
'mouseleave',
'mouseenter',
'keydown',
],
setup(props, ctx) {
const instance = getCurrentInstance()
const attrs = useAttrs()
const $ELEMENT = useGlobalConfig()
const elForm = inject(elFormKey, {} as ElFormContext)
const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
const input = ref(null)
const textarea = ref(null)
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 inputSize = computed(
() => props.size || elFormItem.size || $ELEMENT.size
)
const needStatusIcon = computed(() => elForm.statusIcon)
const validateState = computed(() => elFormItem.validateState || '')
const validateIcon = computed(() => VALIDATE_STATE_MAP[validateState.value])
const computedTextareaStyle = computed(() => ({
...props.inputStyle,
..._textareaCalcStyle.value,
resize: props.resize,
}))
const inputDisabled = computed(() => props.disabled || elForm.disabled)
const nativeInputValue = computed(() =>
props.modelValue === null || props.modelValue === undefined
? ''
: String(props.modelValue)
)
const showClear = computed(() => {
return (
props.clearable &&
!inputDisabled.value &&
!props.readonly &&
nativeInputValue.value &&
(focused.value || hovering.value)
)
})
const showPwdVisible = computed(() => {
return (
props.showPassword &&
!inputDisabled.value &&
!props.readonly &&
(!!nativeInputValue.value || focused.value)
)
})
const isWordLimitVisible = computed(() => {
return (
props.showWordLimit &&
props.maxlength &&
(props.type === 'text' || props.type === 'textarea') &&
!inputDisabled.value &&
!props.readonly &&
!props.showPassword
)
})
const textLength = computed(() => {
return Array.from(nativeInputValue.value).length
})
const inputExceed = computed(() => {
// show exceed style if length of initial value greater then maxlength
return (
isWordLimitVisible.value && textLength.value > Number(props.maxlength)
)
})
const resizeTextarea = () => {
const { type, autosize } = props
if (isServer || 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) => {
const { el } = instance.vnode
const elList: HTMLSpanElement[] = Array.from(
el.querySelectorAll(`.el-input__${place}`)
)
const target = elList.find((item) => item.parentNode === el)
if (!target) return
const pendant = PENDANT_MAP[place]
if (ctx.slots[pendant]) {
target.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${
el.querySelector(`.el-input-group__${pendant}`).offsetWidth
}px)`
} else {
target.removeAttribute('style')
}
}
const updateIconOffset = () => {
calcIconOffset('prefix')
calcIconOffset('suffix')
}
const handleInput = (event) => {
let { value } = event.target
// 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
// if set maxlength
if (props.maxlength) {
const sliceIndex = inputExceed.value
? textLength.value
: props.maxlength
// Convert value to an array for get a right lenght
value = Array.from(value).slice(0, Number(sliceIndex)).join('')
}
ctx.emit(UPDATE_MODEL_EVENT, value)
ctx.emit('input', value)
// ensure native input value is controlled
// see: https://github.com/ElemeFE/element/issues/12850
nextTick(setNativeInputValue)
}
const handleChange = (event) => {
ctx.emit('change', event.target.value)
}
const focus = () => {
// see: https://github.com/ElemeFE/element/issues/18573
nextTick(() => {
inputOrTextarea.value.focus()
})
}
const blur = () => {
inputOrTextarea.value.blur()
}
const handleFocus = (event) => {
focused.value = true
ctx.emit('focus', event)
}
const handleBlur = (event) => {
focused.value = false
ctx.emit('blur', event)
if (props.validateEvent) {
elFormItem.formItemMitt?.emit('el.form.blur', [props.modelValue])
}
}
const select = () => {
inputOrTextarea.value.select()
}
const handleCompositionStart = () => {
isComposing.value = true
}
const handleCompositionUpdate = (event) => {
const text = event.target.value
const lastCharacter = text[text.length - 1] || ''
isComposing.value = !isKorean(lastCharacter)
}
const handleCompositionEnd = (event) => {
if (isComposing.value) {
isComposing.value = false
handleInput(event)
}
}
const clear = () => {
ctx.emit(UPDATE_MODEL_EVENT, '')
ctx.emit('change', '')
ctx.emit('clear')
ctx.emit('input', '')
}
const handlePasswordVisible = () => {
passwordVisible.value = !passwordVisible.value
focus()
}
const getSuffixVisible = () => {
return (
ctx.slots.suffix ||
props.suffixIcon ||
showClear.value ||
props.showPassword ||
isWordLimitVisible.value ||
(validateState.value && needStatusIcon.value)
)
}
watch(
() => props.modelValue,
(val) => {
nextTick(resizeTextarea)
if (props.validateEvent) {
elFormItem.formItemMitt?.emit('el.form.change', [val])
}
}
)
// 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 = (e) => {
hovering.value = false
ctx.emit('mouseleave', e)
}
const onMouseEnter = (e) => {
hovering.value = true
ctx.emit('mouseenter', e)
}
const handleKeydown = (e) => {
ctx.emit('keydown', e)
}
return {
input,
textarea,
attrs,
inputSize,
validateState,
validateIcon,
computedTextareaStyle,
resizeTextarea,
inputDisabled,
showClear,
showPwdVisible,
isWordLimitVisible,
textLength,
hovering,
inputExceed,
passwordVisible,
inputOrTextarea,
handleInput,
handleChange,
handleFocus,
handleBlur,
handleCompositionStart,
handleCompositionUpdate,
handleCompositionEnd,
handlePasswordVisible,
clear,
select,
focus,
blur,
getSuffixVisible,
onMouseLeave,
onMouseEnter,
handleKeydown,
}
},
})
</script>
calcTextareaHeight.ts
js
let hiddenTextarea: HTMLTextAreaElement
const HIDDEN_STYLE = `
height:0 !important;
visibility:hidden !important;
overflow:hidden !important;
position:absolute !important;
z-index:-1000 !important;
top:0 !important;
right:0 !important;
`
const CONTEXT_STYLE = [
'letter-spacing',
'line-height',
'padding-top',
'padding-bottom',
'font-family',
'font-weight',
'font-size',
'text-rendering',
'text-transform',
'width',
'text-indent',
'padding-left',
'padding-right',
'border-width',
'box-sizing',
]
type NodeStyle = {
contextStyle: string
boxSizing: string
paddingSize: number
borderSize: number
}
type TextAreaHeight = {
height: string
minHeight?: string
}
function calculateNodeStyling(targetElement: Element): NodeStyle {
const style = window.getComputedStyle(targetElement)
const boxSizing = style.getPropertyValue('box-sizing')
const paddingSize =
parseFloat(style.getPropertyValue('padding-bottom')) +
parseFloat(style.getPropertyValue('padding-top'))
const borderSize =
parseFloat(style.getPropertyValue('border-bottom-width')) +
parseFloat(style.getPropertyValue('border-top-width'))
const contextStyle = CONTEXT_STYLE.map(
(name) => `${name}:${style.getPropertyValue(name)}`
).join(';')
return { contextStyle, paddingSize, borderSize, boxSizing }
}
export default function calcTextareaHeight(
targetElement: HTMLInputElement,
minRows = 1,
maxRows = null
): TextAreaHeight {
if (!hiddenTextarea) {
hiddenTextarea = document.createElement('textarea')
document.body.appendChild(hiddenTextarea)
}
const { paddingSize, borderSize, boxSizing, contextStyle } =
calculateNodeStyling(targetElement)
hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`)
hiddenTextarea.value = targetElement.value || targetElement.placeholder || ''
let height = hiddenTextarea.scrollHeight
const result = {} as TextAreaHeight
if (boxSizing === 'border-box') {
height = height + borderSize
} else if (boxSizing === 'content-box') {
height = height - paddingSize
}
hiddenTextarea.value = ''
const singleRowHeight = hiddenTextarea.scrollHeight - paddingSize
if (minRows !== null) {
let minHeight = singleRowHeight * minRows
if (boxSizing === 'border-box') {
minHeight = minHeight + paddingSize + borderSize
}
height = Math.max(minHeight, height)
result.minHeight = `${minHeight}px`
}
if (maxRows !== null) {
let maxHeight = singleRowHeight * maxRows
if (boxSizing === 'border-box') {
maxHeight = maxHeight + paddingSize + borderSize
}
height = Math.min(maxHeight, height)
}
result.height = `${height}px`
hiddenTextarea.parentNode?.removeChild(hiddenTextarea)
hiddenTextarea = null
return result
}