Skip to content

Instantly share code, notes, and snippets.

@caasi
Created January 25, 2021 10:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save caasi/3fed137dd2946a2b0ee294de75c677cd to your computer and use it in GitHub Desktop.
Save caasi/3fed137dd2946a2b0ee294de75c677cd to your computer and use it in GitHub Desktop.
Patched Element UI input element
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;
};
<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