源码: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