Feat/input (#216)

* feat(input): migrate input component simply

re #95

* feat(input): mainly complete input component & add more input tests

* fix: address pr comments

Co-authored-by: 陈婉玉 <simonaliachen@gmail.com>
This commit is contained in:
Hanx 2020-09-04 08:24:56 +08:00 committed by GitHub
parent ac2f477e44
commit 869cec59ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1279 additions and 0 deletions

View File

@ -29,6 +29,7 @@ import ElPopper from '@element-plus/popper'
import ElTabs from '@element-plus/tabs'
import ElTooltip from '@element-plus/tooltip'
import ElSlider from '@element-plus/slider'
import ElInput from '@element-plus/input'
export {
ElAlert,
@ -60,6 +61,7 @@ export {
ElTabs,
ElTooltip,
ElSlider,
ElInput,
}
export default function install(app: App): void {
@ -93,4 +95,5 @@ export default function install(app: App): void {
ElTabs(app)
ElTooltip(app)
ElSlider(app)
ElInput(app)
}

View File

@ -0,0 +1,419 @@
import { ref } from 'vue'
import { mount } from '@vue/test-utils'
import { sleep, defineGetter } from '@element-plus/test-utils'
import Input from '../src/index.vue'
const _mount = options => mount({
components: {
'el-input': Input,
},
...options,
})
describe('Input.vue', () => {
test('create', async () => {
const handleFocus = jest.fn()
const wrapper = _mount({
template: `
<el-input
:minlength="3"
:maxlength="5"
placeholder="请输入内容"
@focus="handleFocus"
:model-value="input">
</el-input>
`,
setup() {
const input = ref('input')
return {
input,
handleFocus,
}
},
})
const inputElm = wrapper.find('input')
const vm = wrapper.vm as any
await inputElm.trigger('focus')
expect(inputElm.exists()).toBe(true)
expect(handleFocus).toHaveBeenCalled()
expect(wrapper.attributes('placeholder')).toBe('请输入内容')
expect(inputElm.element.value).toBe('input')
expect(wrapper.attributes('minlength')).toBe('3')
expect(wrapper.attributes('maxlength')).toBe('5')
vm.input = 'text'
await sleep()
expect(inputElm.element.value).toBe('text')
})
test('default to empty', () => {
const wrapper = _mount({
template: '<el-input />',
})
const inputElm = wrapper.find('input')
expect(inputElm.element.value).toBe('')
})
test('disabled', () => {
const wrapper = _mount({
template: `<el-input disabled />`,
})
const inputElm = wrapper.find('input')
expect(inputElm.element.disabled).not.toBeNull()
})
test('suffixIcon', () => {
const wrapper = _mount({
template: `<el-input suffix-icon="time" />`,
})
const icon = wrapper.find('.el-input__icon')
expect(icon.exists()).toBe(true)
})
test('prefixIcon', () => {
const wrapper = _mount({
template: `<el-input prefix-icon="time" />`,
})
const icon = wrapper.find('.el-input__icon')
expect(icon.exists()).toBe(true)
})
test('size', () => {
const wrapper = _mount({
template: `<el-input size="large" />`,
})
expect(wrapper.classes('el-input--large')).toBe(true)
})
test('type', () => {
const wrapper = _mount({
template: `<el-input type="textarea" />`,
})
expect(wrapper.classes('el-textarea')).toBe(true)
})
test('rows', () => {
const wrapper = _mount({
template: `<el-input type="textarea" :rows="3" />`,
})
expect(wrapper.find('textarea').element.rows).toEqual(3)
})
test('resize', async() => {
const wrapper = _mount({
template: `
<div>
<el-input type="textarea" :resize="resize" />
</div>
`,
data() {
return {
resize: 'none',
}
},
})
const vm = wrapper.vm as any
const textarea = wrapper.find('textarea').element
await sleep()
expect(textarea.style.resize).toEqual(vm.resize)
vm.resize = 'horizontal'
await sleep()
expect(textarea.style.resize).toEqual(vm.resize)
})
// TODO: Due to jsdom's reason this case cannot run well, may be fixed later using headlesschrome or puppeteer
// test('autosize', async() => {
// const wrapper = _mount({
// template: `<div>
// <el-input
// ref="limitSize"
// type="textarea"
// :autosize="{minRows: 3, maxRows: 5}"
// v-model="textareaValue"
// />
// </div>`,
// data() {
// return {
// textareaValue: 'sda\ndasd\nddasdsda\ndasd\nddasdsda\ndasd\nddasdsda\ndasd\nddasd',
// }
// },
// })
// const limitSizeInput = wrapper.vm.$refs.limitSize
// const limitlessSizeInput = wrapper.vm.$refs.limitlessSize
// await sleep()
// expect(limitSizeInput.textareaStyle.height).toEqual('117px')
// expect(limitlessSizeInput.textareaStyle.height).toEqual('201px')
// wrapper.vm.textareaValue = ''
// await sleep()
// expect(limitSizeInput.textareaStyle.height).toEqual('75px')
// expect(limitlessSizeInput.textareaStyle.height).toEqual('33px')
// })
test('sets value on textarea / input type change', async () => {
const wrapper = _mount({
template: `<el-input :type="type" v-model="val" />`,
data() {
return {
type: 'text',
val: '123',
}
},
})
const vm = wrapper.vm as any
expect(vm.$el.querySelector('input').value).toEqual('123')
vm.type = 'textarea'
await sleep()
expect(vm.$el.querySelector('textarea').value).toEqual('123')
vm.type = 'password'
await sleep()
expect(vm.$el.querySelector('input').value).toEqual('123')
})
test('limit input and show word count', async () => {
const wrapper = _mount({
template: `
<div>
<el-input
class="test-text"
type="text"
v-model="input1"
maxlength="10"
:show-word-limit="show">
</el-input>
<el-input
class="test-textarea"
type="textarea"
v-model="input2"
maxlength="10"
show-word-limit>
</el-input>
<el-input
class="test-password"
type="password"
v-model="input3"
maxlength="10"
show-word-limit>
</el-input>
<el-input
class="test-initial-exceed"
type="text"
v-model="input4"
maxlength="2"
show-word-limit>
</el-input>
</div>
`,
data() {
return {
input1: '',
input2: '',
input3: '',
input4: 'exceed',
show: false,
}
},
})
const inputElm1 = wrapper.vm.$el.querySelector('.test-text')
const inputElm2 = wrapper.vm.$el.querySelector('.test-textarea')
const inputElm3 = wrapper.vm.$el.querySelector('.test-password')
const inputElm4 = wrapper.vm.$el.querySelector('.test-initial-exceed')
expect(inputElm1.querySelectorAll('.el-input__count').length).toEqual(0)
expect(inputElm2.querySelectorAll('.el-input__count').length).toEqual(1)
expect(inputElm3.querySelectorAll('.el-input__count').length).toEqual(0)
expect(inputElm4.classList.contains('is-exceed')).toBe(true)
const vm = wrapper.vm as any
vm.show = true
await sleep()
expect(inputElm1.querySelectorAll('.el-input__count').length).toEqual(1)
vm.input4 = '1'
await sleep()
expect(inputElm4.classList.contains('is-exceed')).toBe(false)
})
describe('Input Methods', () => {
test('method:select', async () => {
const testContent = 'test'
const wrapper = _mount({
template: `<el-input v-model="text" />`,
data() {
return {
text: testContent,
}
},
})
const input = wrapper.find('input').element
// mock selectionRange behaviour, due to jsdom's reason this case cannot run well, may be fixed later using headlesschrome or puppeteer
let selected = false
defineGetter(input, 'selectionStart', function() {
return selected ? 0 : this.value.length
})
defineGetter(input, 'selectionEnd', function() {
return this.value.length
})
expect(input.selectionStart).toEqual(testContent.length)
expect(input.selectionEnd).toEqual(testContent.length)
input.select()
selected = true
await sleep()
expect(input.selectionStart).toEqual(0)
expect(input.selectionEnd).toEqual(testContent.length)
})
})
describe('Input Events', () => {
const handleFocus = jest.fn()
const handleBlur = jest.fn()
test('event:focus & blur', async () => {
const wrapper = _mount({
template: `<el-input
placeholder="请输入内容"
:model-value="input"
@focus="handleFocus"
@blur="handleBlur"
/>`,
setup() {
const input = ref('')
return {
input,
handleFocus,
handleBlur,
}
},
})
const input = wrapper.find('input')
await input.trigger('focus')
expect(handleFocus).toBeCalled()
await input.trigger('blur')
expect(handleBlur).toBeCalled()
})
test('event:change', async() => {
// NOTE: should be same as native's change behavior
const wrapper = _mount({
template: `
<el-input
placeholder="请输入内容"
:model-value="input"
@change="handleChange"
/>
`,
data() {
return {
input: 'a',
val: '',
}
},
methods: {
handleChange(val) {
this.val = val
},
},
})
const el = wrapper.find('input').element
const vm = wrapper.vm as any
const simulateEvent = (text, event) => {
el.value = text
el.dispatchEvent(new Event(event))
}
// simplified test, component should emit change when native does
simulateEvent('2', 'change')
await sleep()
expect(vm.val).toBe('2')
simulateEvent('1', 'input')
await sleep()
expect(vm.val).toBe('2')
})
test('event:clear', async() => {
const handleClear = jest.fn()
const wrapper = _mount({
template: `
<el-input
placeholder="请输入内容"
clearable
v-model="input"
@clear="handleClear"
/>
`,
setup() {
const input = ref('a')
return {
input,
handleClear,
}
},
})
const input = wrapper.find('input')
const vm = wrapper.vm as any
// focus to show clear button
await input.trigger('focus')
await sleep()
vm.$el.querySelector('.el-input__clear').click()
await sleep()
expect(vm.input).toEqual('')
expect(handleClear).toBeCalled()
})
test('event:input', async() => {
const handleInput = jest.fn()
const wrapper = _mount({
template: `
<el-input
placeholder="请输入内容"
clearable
:model-value="input"
@input="handleInput"
/>
`,
setup() {
const input = ref('a')
return {
input,
handleInput,
}
},
})
const vm = wrapper.vm as any
const inputWrapper = wrapper.find('input')
const nativeInput = inputWrapper.element
nativeInput.value = '1'
await inputWrapper.trigger('compositionstart')
await inputWrapper.trigger('input')
nativeInput.value = '2'
await inputWrapper.trigger('compositionupdate')
await inputWrapper.trigger('input')
await inputWrapper.trigger('compositionend')
expect(handleInput).toBeCalled()
// native input value is controlled
expect(vm.input).toEqual('a')
expect(nativeInput.value).toEqual('a')
})
})
// TODO: validateEvent & input containes select cases should be added after the rest components finished
// ...
})

View File

@ -0,0 +1,66 @@
<template>
<div class="demo-input">
<h3>基础用法</h3>
<el-input v-model="input1" placeholder="请输入内容" />
</div>
<div class="demo-input">
<h3>禁用状态</h3>
<el-input v-model="input1" placeholder="请输入内容" :disabled="true" />
</div>
<div class="demo-input">
<h3>可清空</h3>
<el-input v-model="input2" placeholder="请输入内容" clearable />
</div>
<div class="demo-input">
<h3>密码框</h3>
<el-input v-model="input3" placeholder="请输入密码" show-password />
</div>
<div class="demo-input">
<h3> icon 的输入框</h3>
<div class="demo-input-suffix">
属性方式
<el-input v-model="input1" placeholder="请选择日期" suffix-icon="el-icon-date" />
<el-input v-model="input2" placeholder="请输入内容" prefix-icon="el-icon-search" />
</div>
<div class="demo-input-suffix">
slot 方式
<el-input v-model="input3" placeholder="请选择日期">
<template #suffix>
<i class="el-input__icon el-icon-date"></i>
</template>
</el-input>
<el-input v-model="input4" placeholder="请输入内容">
<template #prefix>
<i class="el-input__icon el-icon-search"></i>
</template>
</el-input>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
const basic = defineComponent({
data() {
return {
input1: '1111',
input2: '2222',
input3: '',
input4: '',
}
},
})
export default basic
</script>
<style>
.demo-input-suffix {
margin-bottom: 15px;
}
.demo-input .el-input {
width: 180px;
margin-right: 15px;
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<div>
<el-input v-model="input1" placeholder="请输入内容">
<template #prepend>Http://</template>
</el-input>
</div>
<div style="margin-top: 15px;">
<el-input v-model="input2" placeholder="请输入内容">
<template #append>.com</template>
</el-input>
</div>
<div style="margin-top: 15px;">
<el-input v-model="input3" class="input-with-select" placeholder="请输入内容">
<!-- <el-select v-model="select" slot="prepend" placeholder="请选择">
<el-option label="餐厅名" value="1"></el-option>
<el-option label="订单号" value="2"></el-option>
<el-option label="用户电话" value="3"></el-option>
</el-select> -->
<template #append>
<el-button icon="el-icon-search" />
</template>
</el-input>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
const basic = defineComponent({
data() {
return {
input1: '1111',
input2: '2222',
input3: '',
}
},
})
export default basic
</script>
<style>
.demo-input .el-input {
width: 180px;
margin-right: 15px;
}
</style>

View File

@ -0,0 +1,9 @@
export { default as BasicUsage } from './basic.vue'
export { default as TextareaUsage } from './textarea.vue'
export { default as CompositionUsage } from './composition.vue'
export { default as SizeUsage } from './size.vue'
export { default as LengthUsage } from './length.vue'
export default {
title: 'Input',
}

View File

@ -0,0 +1,28 @@
<template>
<el-input
v-model="text"
type="text"
placeholder="请输入内容"
maxlength="10"
show-word-limit
/>
<div style="margin: 20px 0;"></div>
<el-input
v-model="textarea"
type="textarea"
placeholder="请输入内容"
maxlength="30"
show-word-limit
/>
</template>
<script>
export default {
data() {
return {
text: '',
textarea: '',
}
},
}
</script>

View File

@ -0,0 +1,47 @@
<template>
<div class="demo-input-size">
<el-input v-model="input1" placeholder="请输入内容" suffix-icon="el-icon-date" />
<el-input
v-model="input2"
size="medium"
placeholder="请输入内容"
suffix-icon="el-icon-date"
/>
<el-input
v-model="input3"
size="small"
placeholder="请输入内容"
suffix-icon="el-icon-date"
/>
<el-input
v-model="input4"
size="mini"
placeholder="请输入内容"
suffix-icon="el-icon-date"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
const basic = defineComponent({
data() {
return {
input1: '1111',
input2: '2222',
input3: '',
input4: '',
}
},
})
export default basic
</script>
<style>
.demo-input-size .el-input {
width: 180px;
margin-right: 15px;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<div class="demo-input">
<h3>文本域</h3>
<el-input
v-model="textarea1"
:rows="2"
type="textarea"
placeholder="请输入内容"
/>
</div>
<div class="demo-input">
<h3>可自适应文本高度的文本域</h3>
<el-input
v-model="textarea2"
type="textarea"
autosize
placeholder="请输入内容"
/>
<div style="margin: 20px 0;"></div>
<el-input
v-model="textarea3"
type="textarea"
:autosize="{minRows: 3, maxRows: 5}"
placeholder="请输入内容"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
const basic = defineComponent({
data() {
return {
textarea1: 'sda\ndasd\nddasdsda\ndasd\nddasdsda\ndasd\nddasdsda\ndasd\nddasd',
textarea2: 'sda\ndasd\nddasdsda\ndasd\nddasdsda\ndasd\nddasdsda\ndasd\nddasd',
textarea3: '',
}
},
})
export default basic
</script>
<style>
.demo-input .el-textarea {
width: 414px;
}
</style>

5
packages/input/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { App } from 'vue'
import Input from './src/index.vue'
export default (app: App): void => {
app.component(Input.name, Input)
}

View File

@ -0,0 +1,12 @@
{
"name": "@element-plus/input",
"version": "0.0.0",
"main": "dist/index.js",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.0-rc.1"
},
"devDependencies": {
"@vue/test-utils": "^2.0.0-beta.0"
}
}

View File

@ -0,0 +1,117 @@
let hiddenTextarea
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): 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,
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
}

View File

@ -0,0 +1,472 @@
<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
}
]"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<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"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
>
<!-- 前置内容 -->
<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 }}/{{ upperLimit }}
</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="textareaStyle"
:aria-label="label"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
>
</textarea>
<span v-if="isWordLimitVisible && type === 'textarea'" class="el-input__count">{{ textLength }}/{{ upperLimit }}</span>
</div>
</template>
<script lang='ts'>
import {
defineComponent,
inject,
computed,
watch,
nextTick,
getCurrentInstance,
ref,
shallowRef,
onMounted,
onUpdated,
} from 'vue'
import { UPDATE_MODEL_EVENT, VALIDATE_STATE_MAP } from '@element-plus/utils/constants'
import { isObject } from '@element-plus/utils/util'
import isServer from '@element-plus/utils/isServer'
import { isKorean } from '@element-plus/utils/isDef'
import calcTextareaHeight from './calcTextareaHeight'
import type { PropType } from 'vue'
// TODOS: replace these interface definition with actual ElForm interface
interface ElForm {
disabled: boolean
statusIcon: string
}
interface ElFormItem {
elFormItemSize: number
validateState: string
}
type AutosizeProp = {
minRows?: number
maxRows?: number
} | boolean
const ELEMENT: {
size?: number
} = {}
const PENDANT_MAP = {
suffix: 'append',
prefix: 'prepend',
}
export default defineComponent({
name: 'ElInput',
props: {
modelValue: {
type: [String, Number],
default: '',
},
type: {
type: String,
default: 'text',
},
size: {
type: String as PropType<'large' | 'medium' | 'small' | 'mini'>,
validator: (val: string) => ['large', 'medium', 'small', 'mini'].includes(val),
},
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,
},
autocomplete: {
type: String,
default: 'off',
validator: (val: string) => ['on', 'off'].includes(val),
},
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,
default: '',
},
tabindex: {
type: String,
default: '',
},
validateEvent: {
type: Boolean,
default: true,
},
},
emits: [UPDATE_MODEL_EVENT, 'change', 'focus', 'blur', 'clear'],
setup(props, ctx) {
const instance = getCurrentInstance()
const elForm = inject<ElForm>('elForm', {} as any)
const elFormItem = inject<ElFormItem>('elFormItem', {} as any)
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({})
const inputOrTextarea = computed(() => input.value || textarea.value)
const inputSize = computed(() => props.size || elFormItem.elFormItemSize || ELEMENT?.size)
const needStatusIcon = computed(() => elForm ? elForm.statusIcon : false)
// TODO: adjust when ElForm done
const validateState = computed(() => elFormItem.validateState || '')
const validateIcon = computed(() => VALIDATE_STATE_MAP[validateState.value])
const textareaStyle = computed(() => ({
..._textareaCalcStyle.value,
resize: props.resize,
}))
const inputDisabled = computed(() => props.disabled || elForm?.disabled)
const nativeInputValue = computed(() => String(props.modelValue))
const upperLimit = computed(() => ctx.attrs.maxlength)
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 &&
ctx.attrs.maxlength &&
(props.type === 'text' || props.type === 'textarea') &&
!inputDisabled.value &&
!props.readonly &&
!props.showPassword
})
const textLength = computed(() => {
return typeof props.modelValue === 'number' ? String(props.modelValue).length : (props.modelValue || '').length
})
const inputExceed = computed(() => {
// show exceed style if length of initial value greater then maxlength
return isWordLimitVisible.value && (textLength.value > upperLimit.value)
})
const resizeTextarea = () => {
const { type, autosize } = props
if (isServer || type !== 'textarea') return
if (autosize) {
const minRows = isObject(autosize) ? autosize.minRows : void 0
const maxRows = isObject(autosize) ? autosize.maxRows : void 0
_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 => {
// 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 (event.target.value === nativeInputValue.value) return
ctx.emit(UPDATE_MODEL_EVENT, event.target.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 = () => {
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) {
// this.dispatch('ElFormItem', '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')
}
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, () => {
nextTick(resizeTextarea)
// TODO: should dispatch event to parent component <el-form-item>;
// if (props.validateEvent) {
// dispatch('ElFormItem', '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)
})
return {
input,
textarea,
inputSize,
validateState,
validateIcon,
textareaStyle,
inputDisabled,
showClear,
showPwdVisible,
isWordLimitVisible,
upperLimit,
textLength,
hovering,
inputExceed,
passwordVisible,
handleInput,
handleChange,
handleFocus,
handleBlur,
handleCompositionStart,
handleCompositionUpdate,
handleCompositionEnd,
handlePasswordVisible,
clear,
select,
focus,
blur,
getSuffixVisible,
}
},
})
</script>

View File

@ -1,2 +1,8 @@
export const UPDATE_MODEL_EVENT = 'update:modelValue'
export const VALIDATE_STATE_MAP = {
validating: 'el-icon-loading',
success: 'el-icon-circle-check',
error: 'el-icon-circle-close',
}