源碼:javascript
<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>
以下圖所示:
css
(2)核心部分 input 輸入框html
<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" >
一、 :tabindex="tabindex" 是控制tab鍵按下後的訪問順序,由用戶傳入tabindex;若是設置爲負數則沒法經過tab鍵訪問,設置爲0則是在最後訪問。java
二、 v-bind="$attrs" 爲了簡化父組件向子組件傳值,props沒有註冊的屬性,能夠經過$attrs來取。element-ui
三、inputDisabled :返回當前input是否被禁用;readonly:input的原生屬性,是不是隻讀狀態;app
四、 原生方法compositionstart、compositionupdate、compositionendpost
compositionstart 官方解釋 : 觸發於一段文字的輸入以前(相似於 keydown 事件,可是該事件僅在若干可見字符的輸入以前,而這些可見字符的輸入可能須要一連串的鍵盤操做、語音識別或者點擊輸入法的備選詞),通俗點,假如咱們要輸入一段中文,當咱們按下第一個字母的時候觸發 。
compositionupdate在咱們中文開始輸入到結束完成的每一次keyup觸發。
compositionend則在咱們完成當前中文的輸入觸發 。ui
這三個事件主要解決中文輸入的響應問題,從compositionstart觸發開始,意味着中文輸入的開始且還沒完成,因此此時咱們不須要作出響應,在compositionend觸發時,表示中文輸入完成,這時咱們能夠作相應事件的處理。this
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使用來計算文本框的高度spa
//原理:讓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
http://www.javashuo.com/article/p-fpumvytq-bs.html