element-ui input组件源码分析整理笔记(六)
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; };
参考博文:
上一篇: Django的时区设置问题
下一篇: 关于JavaScript原型对象那些事儿
推荐阅读
-
element-ui Tag、Dialog组件源码分析整理笔记(五)
-
element-ui button组件 radio组件源码分析整理笔记(一)
-
element-ui Carousel 走马灯源码分析整理笔记(十一)
-
element-ui Upload 上传组件源码分析整理笔记(十四)
-
element-ui input组件源码分析整理笔记(六)
-
element-ui Steps步骤条组件源码分析整理笔记(九)
-
element-ui switch组件源码分析整理笔记(二)
-
element-ui inputNumber、Card 、Breadcrumb组件源码分析整理笔记(三)
-
element-ui Rate组件源码分析整理笔记(十三)
-
element-ui Message组件源码分析整理笔记(八)