Created
January 25, 2021 10:05
-
-
Save caasi/3fed137dd2946a2b0ee294de75c677cd to your computer and use it in GitHub Desktop.
Patched Element UI input element
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | |
]; | |
function calculateNodeStyling(targetElement) { | |
const style = window.getComputedStyle(targetElement); | |
const boxSizing = style.getPropertyValue('box-sizing') || 'content-box'; | |
const paddingBottom = style.getPropertyValue('padding-bottom') || '0'; | |
const paddingTop = style.getPropertyValue('padding-top') || '0'; | |
const paddingSize = parseFloat(paddingBottom) + parseFloat(paddingTop); | |
const borderBottomWidth = style.getPropertyValue('border-bottom-width') || '0'; | |
const borderTopWidth = style.getPropertyValue('border-top-width') || '0'; | |
const borderSize = parseFloat(borderBottomWidth) + parseFloat(borderTopWidth); | |
const contextStyle = CONTEXT_STYLE | |
.map(name => `${name}:${style.getPropertyValue(name)}`) | |
.join(';'); | |
return { contextStyle, paddingSize, borderSize, boxSizing }; | |
} | |
function getScrollHeight(style, value) { | |
let hiddenTextarea = document.createElement('textarea'); | |
hiddenTextarea.setAttribute('style', style); | |
document.body.appendChild(hiddenTextarea); | |
hiddenTextarea.value = value; | |
return new Promise((resolve) => { | |
window.setTimeout(() => { | |
resolve(hiddenTextarea.scrollHeight); | |
hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea); | |
hiddenTextarea = null; | |
}, 100); | |
}); | |
} | |
export default async function calcTextareaHeight( | |
targetElement, | |
minRows = 1, | |
maxRows = null | |
) { | |
let { | |
paddingSize, | |
borderSize, | |
boxSizing, | |
contextStyle | |
} = calculateNodeStyling(targetElement); | |
const style = `${contextStyle};${HIDDEN_STYLE}`; | |
let height = await getScrollHeight( | |
style, | |
targetElement.value || targetElement.placeholder || '', | |
); | |
const result = {}; | |
if (boxSizing === 'border-box') { | |
height = height + borderSize; | |
} else if (boxSizing === 'content-box') { | |
height = height - paddingSize; | |
} | |
let singleRowHeight = await getScrollHeight(style, '') - paddingSize; | |
console.log('single row height', singleRowHeight); | |
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`; | |
return result; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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 class="el-input-group__prepend" v-if="$slots.prepend"> | |
<slot name="prepend"></slot> | |
</div> | |
<input | |
:tabindex="tabindex" | |
v-if="type !== 'textarea'" | |
class="el-input__inner" | |
v-bind="$attrs" | |
:type="showPassword ? (passwordVisible ? 'text': 'password') : type" | |
:disabled="inputDisabled" | |
:readonly="readonly" | |
:autocomplete="autoComplete || autocomplete" | |
ref="input" | |
@compositionstart="handleCompositionStart" | |
@compositionupdate="handleCompositionUpdate" | |
@compositionend="handleCompositionEnd" | |
@input="handleInput" | |
@focus="handleFocus" | |
@blur="handleBlur" | |
@change="handleChange" | |
:aria-label="label" | |
> | |
<!-- 前置内容 --> | |
<span class="el-input__prefix" v-if="$slots.prefix || prefixIcon"> | |
<slot name="prefix"></slot> | |
<i class="el-input__icon" | |
v-if="prefixIcon" | |
:class="prefixIcon"> | |
</i> | |
</span> | |
<!-- 后置内容 --> | |
<span | |
class="el-input__suffix" | |
v-if="getSuffixVisible()"> | |
<span class="el-input__suffix-inner"> | |
<template v-if="!showClear || !showPwdVisible || !isWordLimitVisible"> | |
<slot name="suffix"></slot> | |
<i class="el-input__icon" | |
v-if="suffixIcon" | |
:class="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 class="el-input__icon" | |
v-if="validateState" | |
:class="['el-input__validateIcon', validateIcon]"> | |
</i> | |
</span> | |
<!-- 后置元素 --> | |
<div class="el-input-group__append" v-if="$slots.append"> | |
<slot name="append"></slot> | |
</div> | |
</template> | |
<textarea | |
v-else | |
:tabindex="tabindex" | |
class="el-textarea__inner" | |
@compositionstart="handleCompositionStart" | |
@compositionupdate="handleCompositionUpdate" | |
@compositionend="handleCompositionEnd" | |
@input="handleInput" | |
ref="textarea" | |
v-bind="$attrs" | |
:disabled="inputDisabled" | |
:readonly="readonly" | |
:autocomplete="autoComplete || autocomplete" | |
:style="textareaStyle" | |
@focus="handleFocus" | |
@blur="handleBlur" | |
@change="handleChange" | |
:aria-label="label" | |
> | |
</textarea> | |
<span v-if="isWordLimitVisible && type === 'textarea'" class="el-input__count">{{ textLength }}/{{ upperLimit }}</span> | |
</div> | |
</template> | |
<script> | |
import emitter from 'element-ui/src/mixins/emitter'; | |
import Migrating from 'element-ui/src/mixins/migrating'; | |
import calcTextareaHeight from './calcTextareaHeight'; | |
import merge from 'element-ui/src/utils/merge'; | |
import {isKorean} from 'element-ui/src/utils/shared'; | |
export default { | |
name: 'ElInput', | |
componentName: 'ElInput', | |
mixins: [emitter, Migrating], | |
inheritAttrs: false, | |
inject: { | |
elForm: { | |
default: '' | |
}, | |
elFormItem: { | |
default: '' | |
} | |
}, | |
data() { | |
return { | |
textareaCalcStyle: {}, | |
hovering: false, | |
focused: false, | |
isComposing: false, | |
passwordVisible: false | |
}; | |
}, | |
props: { | |
value: [String, Number], | |
size: String, | |
resize: String, | |
form: String, | |
disabled: Boolean, | |
readonly: Boolean, | |
type: { | |
type: String, | |
default: 'text' | |
}, | |
autosize: { | |
type: [Boolean, Object], | |
default: false | |
}, | |
autocomplete: { | |
type: String, | |
default: 'off' | |
}, | |
/** @Deprecated in next major version */ | |
autoComplete: { | |
type: String, | |
validator(val) { | |
process.env.NODE_ENV !== 'production' && | |
console.warn('[Element Warn][Input]\'auto-complete\' property will be deprecated in next major version. please use \'autocomplete\' instead.'); | |
return true; | |
} | |
}, | |
validateEvent: { | |
type: Boolean, | |
default: true | |
}, | |
suffixIcon: String, | |
prefixIcon: String, | |
label: String, | |
clearable: { | |
type: Boolean, | |
default: false | |
}, | |
showPassword: { | |
type: Boolean, | |
default: false | |
}, | |
showWordLimit: { | |
type: Boolean, | |
default: false | |
}, | |
tabindex: String | |
}, | |
computed: { | |
_elFormItemSize() { | |
return (this.elFormItem || {}).elFormItemSize; | |
}, | |
validateState() { | |
return this.elFormItem ? this.elFormItem.validateState : ''; | |
}, | |
needStatusIcon() { | |
return this.elForm ? this.elForm.statusIcon : false; | |
}, | |
validateIcon() { | |
return { | |
validating: 'el-icon-loading', | |
success: 'el-icon-circle-check', | |
error: 'el-icon-circle-close' | |
}[this.validateState]; | |
}, | |
textareaStyle() { | |
return merge({}, this.textareaCalcStyle, { resize: this.resize }); | |
}, | |
inputSize() { | |
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size; | |
}, | |
inputDisabled() { | |
return this.disabled || (this.elForm || {}).disabled; | |
}, | |
nativeInputValue() { | |
return this.value === null || this.value === undefined ? '' : String(this.value); | |
}, | |
showClear() { | |
return this.clearable && | |
!this.inputDisabled && | |
!this.readonly && | |
this.nativeInputValue && | |
(this.focused || this.hovering); | |
}, | |
showPwdVisible() { | |
return this.showPassword && | |
!this.inputDisabled && | |
!this.readonly && | |
(!!this.nativeInputValue || this.focused); | |
}, | |
isWordLimitVisible() { | |
return this.showWordLimit && | |
this.$attrs.maxlength && | |
(this.type === 'text' || this.type === 'textarea') && | |
!this.inputDisabled && | |
!this.readonly && | |
!this.showPassword; | |
}, | |
upperLimit() { | |
return this.$attrs.maxlength; | |
}, | |
textLength() { | |
if (typeof this.value === 'number') { | |
return String(this.value).length; | |
} | |
return (this.value || '').length; | |
}, | |
inputExceed() { | |
// show exceed style if length of initial value greater then maxlength | |
return this.isWordLimitVisible && | |
(this.textLength > this.upperLimit); | |
} | |
}, | |
watch: { | |
value(val) { | |
this.$nextTick(this.resizeTextarea); | |
if (this.validateEvent) { | |
this.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 | |
nativeInputValue() { | |
this.setNativeInputValue(); | |
}, | |
// when change between <input> and <textarea>, | |
// update DOM dependent value and styles | |
// https://github.com/ElemeFE/element/issues/14857 | |
type() { | |
this.$nextTick(() => { | |
this.setNativeInputValue(); | |
this.resizeTextarea(); | |
this.updateIconOffset(); | |
}); | |
} | |
}, | |
methods: { | |
focus() { | |
this.getInput().focus(); | |
}, | |
blur() { | |
this.getInput().blur(); | |
}, | |
getMigratingConfig() { | |
return { | |
props: { | |
'icon': 'icon is removed, use suffix-icon / prefix-icon instead.', | |
'on-icon-click': 'on-icon-click is removed.' | |
}, | |
events: { | |
'click': 'click is removed.' | |
} | |
}; | |
}, | |
handleBlur(event) { | |
this.focused = false; | |
this.$emit('blur', event); | |
if (this.validateEvent) { | |
this.dispatch('ElFormItem', 'el.form.blur', [this.value]); | |
} | |
}, | |
select() { | |
this.getInput().select(); | |
}, | |
async resizeTextarea() { | |
if (this.$isServer) return; | |
const { autosize, type } = this; | |
if (type !== 'textarea') return; | |
if (!autosize) { | |
this.textareaCalcStyle = { | |
minHeight: await calcTextareaHeight(this.$refs.textarea).minHeight | |
}; | |
console.log(this.textareaCalcStyle); | |
return; | |
} | |
const minRows = autosize.minRows; | |
const maxRows = autosize.maxRows; | |
this.textareaCalcStyle = await calcTextareaHeight(this.$refs.textarea, minRows, maxRows); | |
console.log(this.textareaCalcStyle); | |
}, | |
setNativeInputValue() { | |
const input = this.getInput(); | |
if (!input) return; | |
if (input.value === this.nativeInputValue) return; | |
input.value = this.nativeInputValue; | |
}, | |
handleFocus(event) { | |
this.focused = true; | |
this.$emit('focus', event); | |
}, | |
handleCompositionStart() { | |
this.isComposing = true; | |
}, | |
handleCompositionUpdate(event) { | |
const text = event.target.value; | |
const lastCharacter = text[text.length - 1] || ''; | |
this.isComposing = !isKorean(lastCharacter); | |
}, | |
handleCompositionEnd(event) { | |
if (this.isComposing) { | |
this.isComposing = false; | |
this.handleInput(event); | |
} | |
}, | |
handleInput(event) { | |
// should not emit input during composition | |
// see: https://github.com/ElemeFE/element/issues/10516 | |
if (this.isComposing) 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 === this.nativeInputValue) return; | |
this.$emit('input', event.target.value); | |
// ensure native input value is controlled | |
// see: https://github.com/ElemeFE/element/issues/12850 | |
this.$nextTick(this.setNativeInputValue); | |
}, | |
handleChange(event) { | |
this.$emit('change', event.target.value); | |
}, | |
calcIconOffset(place) { | |
let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []); | |
if (!elList.length) return; | |
let el = null; | |
for (let i = 0; i < elList.length; i++) { | |
if (elList[i].parentNode === this.$el) { | |
el = elList[i]; | |
break; | |
} | |
} | |
if (!el) return; | |
const pendantMap = { | |
suffix: 'append', | |
prefix: 'prepend' | |
}; | |
const pendant = pendantMap[place]; | |
if (this.$slots[pendant]) { | |
el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth}px)`; | |
} else { | |
el.removeAttribute('style'); | |
} | |
}, | |
updateIconOffset() { | |
this.calcIconOffset('prefix'); | |
this.calcIconOffset('suffix'); | |
}, | |
clear() { | |
this.$emit('input', ''); | |
this.$emit('change', ''); | |
this.$emit('clear'); | |
}, | |
handlePasswordVisible() { | |
this.passwordVisible = !this.passwordVisible; | |
this.focus(); | |
}, | |
getInput() { | |
return this.$refs.input || this.$refs.textarea; | |
}, | |
getSuffixVisible() { | |
return this.$slots.suffix || | |
this.suffixIcon || | |
this.showClear || | |
this.showPassword || | |
this.isWordLimitVisible || | |
(this.validateState && this.needStatusIcon); | |
} | |
}, | |
created() { | |
this.$on('inputSelect', this.select); | |
}, | |
mounted() { | |
this.setNativeInputValue(); | |
this.resizeTextarea(); | |
this.updateIconOffset(); | |
}, | |
updated() { | |
this.$nextTick(this.updateIconOffset); | |
} | |
}; | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment