欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

element-ui input组件源码分析整理笔记(六)

程序员文章站 2022-07-07 19:48:34
input 输入框组件 源码: html

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>

如下图所示:
element-ui input组件源码分析整理笔记(六)

(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;
};

参考博文: