input 輸入框組件 源碼: html <! 當type的值不等於textarea時 <! 前置元素 <! 核心部分:輸入框 <! input框內的頭部的內容 <! prefixIcon頭部圖標存在時,顯示i標簽 <! input框內的尾部的內容 <! showClear為false時,顯示尾部圖 ...
input 輸入框組件
源碼:
<template>
<div :class="[
type === 'textarea' ? 'el-textarea' : 'el-input',
inputSize ? 'el-input--' + inputSize : '',
{
'is-disabled': inputDisabled,
'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
}
]"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<!--當type的值不等於textarea時-->
<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="type"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete || autocomplete"
:value="currentValue"
ref="input"
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
:aria-label="label"
>
<!-- input框內的頭部的內容 -->
<span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
<slot name="prefix"></slot>
<!--prefixIcon頭部圖標存在時,顯示i標簽-->
<i class="el-input__icon" v-if="prefixIcon" :class="prefixIcon"></i>
</span>
<!-- input框內的尾部的內容 -->
<span class="el-input__suffix" v-if="$slots.suffix || suffixIcon || showClear || validateState && needStatusIcon">
<span class="el-input__suffix-inner">
<!--showClear為false時,顯示尾部圖標-->
<template v-if="!showClear">
<slot name="suffix"></slot>
<i class="el-input__icon" v-if="suffixIcon" :class="suffixIcon"></i>
</template>
<!--showClear為true時,顯示清空圖標-->
<i v-else class="el-input__icon el-icon-circle-close el-input__clear" @click="clear"></i>
</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>
<!--當type的值等於textarea時-->
<textarea
v-else
:tabindex="tabindex"
class="el-textarea__inner"
:value="currentValue"
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
@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>
</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 {
currentValue: this.value === undefined || this.value === null
? ''
: this.value,
textareaCalcStyle: {},
hovering: false,
focused: false,
isOnComposition: false,
valueBeforeComposition: null
};
},
props: {
value: [String, Number], //綁定值
size: String, //輸入框尺寸,只在type!="textarea" 時有效
resize: String, //控制是否能被用戶縮放
form: String,
disabled: Boolean, //禁用
readonly: Boolean,
type: { //類型texttextarea和其他原生input的type值
type: String,
default: 'text'
},
autosize: { //自適應內容高度,只對 type="textarea" 有效,可傳入對象,如,{ minRows: 2, maxRows: 6 }
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, //輸入框關聯的label文字
clearable: { //是否可清空
type: Boolean,
default: false
},
tabindex: String //輸入框的tabindex
},
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];
},
//textarea的樣式
textareaStyle() {
return merge({}, this.textareaCalcStyle, { resize: this.resize });
},
//輸入框尺寸,只在 type!="textarea" 時有效
inputSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
//input是否被禁用
inputDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
//是否顯示清空按鈕
showClear() {
// clearable屬性為true,即用戶設置了顯示清空按鈕的屬性;並且在非禁用且非只讀狀態下才且當前input的value不是空且該input獲得焦點或者滑鼠移動上去才顯示
return this.clearable &&
!this.inputDisabled &&
!this.readonly &&
this.currentValue !== '' &&
(this.focused || this.hovering);
}
},
watch: {
value(val, oldValue) {
this.setCurrentValue(val);
}
},
methods: {
focus() {
(this.$refs.input || this.$refs.textarea).focus();
},
blur() {
(this.$refs.input || this.$refs.textarea).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.currentValue]);
}
},
select() {
(this.$refs.input || this.$refs.textarea).select();
},
resizeTextarea() {
if (this.$isServer) return;
//autosize自適應內容高度,只對 type="textarea" 有效,可傳入對象,如,{ minRows: 2, maxRows: 6 }
const { autosize, type } = this;
if (type !== 'textarea') return;
//如果沒設置自適應內容高度
if (!autosize) {
this.textareaCalcStyle = { //高度取文本框的最小高度
minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
};
return;
}
const minRows = autosize.minRows;
const maxRows = autosize.maxRows;
//如果設置了minRows和maxRows需要計算文本框的高度
this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
},
handleFocus(event) {
this.focused = true;
this.$emit('focus', event);
},
handleComposition(event) {
// 如果中文輸入已完成
if (event.type === 'compositionend') {
// isOnComposition設置為false
this.isOnComposition = false;
this.currentValue = this.valueBeforeComposition;
this.valueBeforeComposition = null;
//觸發input事件,因為input事件是在compositionend事件之後觸發,這時輸入未完成,不會將值傳給父組件,所以需要再調一次input方法
this.handleInput(event);
} else { //如果中文輸入未完成
const text = event.target.value;
const lastCharacter = text[text.length - 1] || '';
//isOnComposition用來判斷是否在輸入拼音的過程中
this.isOnComposition = !isKorean(lastCharacter);
if (this.isOnComposition && event.type === 'compositionstart') {
// 輸入框中輸入的值賦給valueBeforeComposition
this.valueBeforeComposition = text;
}
}
},
handleInput(event) {
const value = event.target.value;
//設置當前值
this.setCurrentValue(value);
//如果還在輸入中,將不會把值傳給父組件
if (this.isOnComposition) return;
//輸入完成時,isOnComposition為false,將值傳遞給父組件
this.$emit('input', value);
},
handleChange(event) {
this.$emit('change', event.target.value);
},
setCurrentValue(value) {
// 輸入中,直接返回
if (this.isOnComposition && value === this.valueBeforeComposition) return;
this.currentValue = value;
if (this.isOnComposition) return;
//輸入完成,設置文本框的高度
this.$nextTick(this.resizeTextarea);
if (this.validateEvent && this.currentValue === this.value) {
this.dispatch('ElFormItem', 'el.form.change', [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() {
//父組件的value值變成了空,更新父組件中v-model的值
this.$emit('input', '');
//觸發了父組件的change事件,父組件中就可以監聽到該事件
this.$emit('change', '');
//觸發了父組件的clear事件
this.$emit('clear');
//更新當前的currentValue的值
this.setCurrentValue('');
}
},
created() {
this.$on('inputSelect', this.select);
},
mounted() {
this.resizeTextarea();
this.updateIconOffset();
},
updated() {
this.$nextTick(this.updateIconOffset);
}
};
</script>
如下圖所示:
(2)核心部分 input 輸入框
<input
:tabindex="tabindex"
v-if="type !== 'textarea'"
class="el-input__inner"
v-bind="$attrs"
:type="type"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete || autocomplete"
:value="currentValue"
ref="input"
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
:aria-label="label"
>
1、 :tabindex="tabindex" 是控制tab鍵按下後的訪問順序,由用戶傳入tabindex;如果設置為負數則無法通過tab鍵訪問,設置為0則是在最後訪問。
2、 v-bind="$attrs" 為了簡化父組件向子組件傳值,props沒有註冊的屬性,可以通過$attrs來取。
3、inputDisabled :返回當前input是否被禁用;readonly:input的原生屬性,是否是只讀狀態;
4、 原生方法compositionstart、compositionupdate、compositionend
compositionstart 官方解釋 : 觸發於一段文字的輸入之前(類似於 keydown 事件,但是該事件僅在若幹可見字元的輸入之前,而這些可見字元的輸入可能需要一連串的鍵盤操作、語音識別或者點擊輸入法的備選詞),通俗點,假如我們要輸入一段中文,當我們按下第一個字母的時候觸發 。
compositionupdate在我們中文開始輸入到結束完成的每一次keyup觸發。
compositionend則在我們完成當前中文的輸入觸發 。
這三個事件主要解決中文輸入的響應問題,從compositionstart觸發開始,意味著中文輸入的開始且還沒完成,所以此時我們不需要做出響應,在compositionend觸發時,表示中文輸入完成,這時我們可以做相應事件的處理。
handleComposition(event) {
// 如果中文輸入已完成
if (event.type === 'compositionend') {
// isOnComposition設置為false
this.isOnComposition = false;
this.currentValue = this.valueBeforeComposition;
this.valueBeforeComposition = null;
//觸發input事件,因為input事件是在compositionend事件之後觸發,這時輸入未完成,不會將值傳給父組件,所以需要再調一次input方法
this.handleInput(event);
} else { //如果中文輸入未完成
const text = event.target.value;
const lastCharacter = text[text.length - 1] || '';
//isOnComposition用來判斷是否在輸入拼音的過程中
this.isOnComposition = !isKorean(lastCharacter);
if (this.isOnComposition && event.type === 'compositionstart') {
// 輸入框中輸入的值賦給valueBeforeComposition
this.valueBeforeComposition = text;
}
}
},
handleInput(event) {
const value = event.target.value;
//設置當前值
this.setCurrentValue(value);
//如果還在輸入中,將不會把值傳給父組件
if (this.isOnComposition) return;
//輸入完成時,isOnComposition為false,將值傳遞給父組件
this.$emit('input', value);
},
(3)calcTextareaHeight.js使用來計算文本框的高度
//原理:讓height等於scrollHeight,也就是滾動條捲去的高度,這裡就將height變大了,然後返回該height並綁定到input的style中從而動態改變textarea的height
let hiddenTextarea;
//存儲隱藏時候的css樣式的
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);
// getPropertyValue方法返回指定的 CSS 屬性的值;這裡返回box-sizing屬性的值
const boxSizing = style.getPropertyValue('box-sizing');
// padding-bottom和padding-top值之和
const paddingSize = (
parseFloat(style.getPropertyValue('padding-bottom')) +
parseFloat(style.getPropertyValue('padding-top'))
);
// border-bottom-width和border-top-width值之和
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 //最大行數
) {
// 創建一個隱藏的文本域
if (!hiddenTextarea) {
hiddenTextarea = document.createElement('textarea');
document.body.appendChild(hiddenTextarea);
}
//獲取目標元素的樣式
let {
paddingSize,
borderSize,
boxSizing,
contextStyle
} = calculateNodeStyling(targetElement);
//設置對應的樣式屬性
hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`);
hiddenTextarea.value = targetElement.value || targetElement.placeholder || '';
// 獲取滾動高度
let height = hiddenTextarea.scrollHeight;
const result = {};
if (boxSizing === 'border-box') {
// 如果是 border-box,高度需加上邊框
height = height + borderSize;
} else if (boxSizing === 'content-box') {
// 如果是 content-box,高度需減去上下內邊距
height = height - paddingSize;
}
// 計算單行高度,先清空內容
hiddenTextarea.value = '';
// 再用滾動高度減去上下內邊距
let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;
if (minRows !== null) { // 如果參數傳遞了 minRows
// 最少的高度=單行的高度*行數
let minHeight = singleRowHeight * minRows;
if (boxSizing === 'border-box') {
// 如果是 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 && hiddenTextarea.parentNode.removeChild(hiddenTextarea);
hiddenTextarea = null;
return result;
};
參考博文:https://www.jianshu.com/p/74ba49507fe6
https://juejin.im/post/5b7d18e46fb9a01a12502616