element-ui input元件原始碼分析整理筆記(六)
阿新 • • 發佈:2018-12-04
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