element input-number源码

input-number.vue

<template>
  <div
    @dragstart.prevent
    :class="[
      'el-input-number',
      inputNumberSize ? 'el-input-number--' + inputNumberSize : '',
      { 'is-disabled': inputNumberDisabled },
      { 'is-without-controls': !controls },
      { 'is-controls-right': controlsAtRight }
    ]">
    <span
      class="el-input-number__decrease"
      role="button"
      v-if="controls"
      v-repeat-click="decrease"
      :class="{'is-disabled': minDisabled}"
      @keydown.enter="decrease">
      <i :class="`el-icon-${controlsAtRight ? 'arrow-down' : 'minus'}`"></i>
    </span>
    <span
      class="el-input-number__increase"
      role="button"
      v-if="controls"
      v-repeat-click="increase"
      :class="{'is-disabled': maxDisabled}"
      @keydown.enter="increase">
      <i :class="`el-icon-${controlsAtRight ? 'arrow-up' : 'plus'}`"></i>
    </span>
    <el-input
      ref="input"
      :value="displayValue"
      :placeholder="placeholder"
      :disabled="inputNumberDisabled"
      :size="inputNumberSize"
      :max="max"
      :min="min"
      :name="name"
      :label="label"
      @keydown.up.native.prevent="increase"
      @keydown.down.native.prevent="decrease"
      @blur="handleBlur"
      @focus="handleFocus"
      @input="handleInput"
      @change="handleInputChange">
    </el-input>
  </div>
</template>
<script>
  import ElInput from 'element-ui/packages/input';
  import Focus from 'element-ui/src/mixins/focus';
  import RepeatClick from 'element-ui/src/directives/repeat-click';

  export default {
    name: 'ElInputNumber',
    mixins: [Focus('input')],
    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },
    directives: {
      repeatClick: RepeatClick
    },
    components: {
      ElInput
    },
    props: {
          // 计数器步长
      step: {
        type: Number,
        default: 1
      },
      // 是否只能输入 step 的倍数
      stepStrictly: {
        type: Boolean,
        default: false
      },
      // 设置计数器允许的最大值
      max: {
        type: Number,
        default: Infinity
      },
      // 设置计数器允许的最小值
      min: {
        type: Number,
        default: -Infinity
      },
      // value / v-model    绑定值
      value: {},
      // 是否禁用计数器
      disabled: Boolean,
      // 计数器尺寸    string    large, small
      size: String,
      // 是否使用控制按钮
      controls: {
        type: Boolean,
        default: true
      },
      // 控制按钮位置    string    right
      controlsPosition: {
        type: String,
        default: ''
      },
      // 原生属性
      name: String,
      // 输入框关联的label文字
      label: String,
      // 输入框默认 placeholder
      placeholder: String,
      // 数值精度
      precision: {
        type: Number,
        validator(val) {
          return val >= 0 && val === parseInt(val, 10);
        }
      }
    },
    data() {
      return {
        currentValue: 0,
        userInput: null
      };
    },
    watch: {
      // vaue值变化
      value: {
        // 立即触发
        immediate: true,
        // 自定义函数
        handler(value) {
          let newVal = value === undefined ? value : Number(value);
          if (newVal !== undefined) {
            if (isNaN(newVal)) {
              return;
            }
            // 是否只能输入 step 的倍数
            if (this.stepStrictly) {
              const stepPrecision = this.getPrecision(this.step);
              const precisionFactor = Math.pow(10, stepPrecision);
              newVal = Math.round(newVal / this.step) * precisionFactor * this.step / precisionFactor;
            }

            if (this.precision !== undefined) {
              newVal = this.toPrecision(newVal, this.precision);
            }
          }
          if (newVal >= this.max) newVal = this.max;
          if (newVal <= this.min) newVal = this.min;
          this.currentValue = newVal;
          this.userInput = null;
          this.$emit('input', newVal);
        }
      }
    },
    computed: {
      // 不能小于最小数
      minDisabled() {
        return this._decrease(this.value, this.step) < this.min;
      },
      // 不能大于最大数
      maxDisabled() {
        return this._increase(this.value, this.step) > this.max;
      },
      numPrecision() {
        const { value, step, getPrecision, precision } = this;
        const stepPrecision = getPrecision(step);
        if (precision !== undefined) {
          if (stepPrecision > precision) {
            console.warn('[Element Warn][InputNumber]precision should not be less than the decimal places of step');
          }
          return precision;
        } else {
          return Math.max(getPrecision(value), stepPrecision);
        }
      },
      // 控制条在右侧
      controlsAtRight() {
        return this.controls && this.controlsPosition === 'right';
      },
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      inputNumberSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      },
      inputNumberDisabled() {
        return this.disabled || (this.elForm || {}).disabled;
      },
      displayValue() {
        if (this.userInput !== null) {
          return this.userInput;
        }

        let currentValue = this.currentValue;

        if (typeof currentValue === 'number') {
          // 如果只能输入 step 的倍数
          if (this.stepStrictly) {
            // 小数点后面几位小数
            const stepPrecision = this.getPrecision(this.step);
            const precisionFactor = Math.pow(10, stepPrecision);
            currentValue = Math.round(currentValue / this.step) * precisionFactor * this.step / precisionFactor;
          }

          if (this.precision !== undefined) {
            currentValue = currentValue.toFixed(this.precision);
          }
        }

        return currentValue;
      }
    },
    methods: {
      // 截取为传入的位数
      toPrecision(num, precision) {
        if (precision === undefined) precision = this.numPrecision;
        return parseFloat(Number(num).toFixed(precision));
      },
      // 获取小数点后面还有几位
      getPrecision(value) {
        if (value === undefined) return 0;
        const valueString = value.toString();
        const dotPosition = valueString.indexOf('.');
        let precision = 0;
        if (dotPosition !== -1) {
          precision = valueString.length - dotPosition - 1;
        }
        return precision;
      },
      _increase(val, step) {
        if (typeof val !== 'number' && val !== undefined) return this.currentValue;

        const precisionFactor = Math.pow(10, this.numPrecision);
        // Solve the accuracy problem of JS decimal calculation by converting the value to integer.
        return this.toPrecision((precisionFactor * val + precisionFactor * step) / precisionFactor);
      },
      // 递减函数
      _decrease(val, step) {
        if (typeof val !== 'number' && val !== undefined) return this.currentValue;
        // eg: 10的0.1次方
        const precisionFactor = Math.pow(10, this.numPrecision);

        return this.toPrecision((precisionFactor * val - precisionFactor * step) / precisionFactor);
      },
      increase() {
        if (this.inputNumberDisabled || this.maxDisabled) return;
        const value = this.value || 0;
        const newVal = this._increase(value, this.step);
        this.setCurrentValue(newVal);
      },
      decrease() {
        if (this.inputNumberDisabled || this.minDisabled) return;
        const value = this.value || 0;
        const newVal = this._decrease(value, this.step);
        this.setCurrentValue(newVal);
      },
      handleBlur(event) {
        this.$emit('blur', event);
      },
      handleFocus(event) {
        this.$emit('focus', event);
      },
      setCurrentValue(newVal) {
        const oldVal = this.currentValue;
        if (typeof newVal === 'number' && this.precision !== undefined) {
          newVal = this.toPrecision(newVal, this.precision);
        }
        if (newVal >= this.max) newVal = this.max;
        if (newVal <= this.min) newVal = this.min;
        if (oldVal === newVal) return;
        this.userInput = null;
        this.$emit('input', newVal);
        this.$emit('change', newVal, oldVal);
        this.currentValue = newVal;
      },
      handleInput(value) {
        this.userInput = value;
      },
      handleInputChange(value) {
        const newVal = value === '' ? undefined : Number(value);
        if (!isNaN(newVal) || value === '') {
          this.setCurrentValue(newVal);
        }
        this.userInput = null;
      },
      select() {
        this.$refs.input.select();
      }
    },
    mounted() {
      let innerInput = this.$refs.input.$refs.input;
      innerInput.setAttribute('role', 'spinbutton');
      innerInput.setAttribute('aria-valuemax', this.max);
      innerInput.setAttribute('aria-valuemin', this.min);
      innerInput.setAttribute('aria-valuenow', this.currentValue);
      innerInput.setAttribute('aria-disabled', this.inputNumberDisabled);
    },
    updated() {
      if (!this.$refs || !this.$refs.input) return;
      const innerInput = this.$refs.input.$refs.input;
      innerInput.setAttribute('aria-valuenow', this.currentValue);
    }
  };
</script>
repeat-click.js

 

import { once, on } from 'element-ui/src/utils/dom';

export default {
  bind (el, binding, vnode) {
    let interval = null;
    let startTime;
    /**
     * context是一个 Component 类型的数据结构,这个Component是flow定义的结构,具体可看vue源码中的flow内的内容,
     * Component就是组件,所以这个context就是该vnode所在的组件上下文,再来看 binding.expression, 官网说这就是 
     * v - repeat - click="decrease" 中的decrease方法,这个方法写在组件的methods内,
     * 那么 context[binding.expression] 就是 context['decrease'] 因此就拿到了组件内的decrease方法,
     * 类似于在组件中使用 this.decrease 一样,然后最后的 apply()
    就很奇怪了,apply的用法是参数的第一个表示要执行的目标对象,如果为null或者undefined则表示在window上调用该方法,这里没有参数,
    那就是undefined,所以是在window上执行
     *  */
    const handler = () => vnode.context[binding.expression].apply();
    const clear = () => {
      if (Date.now() - startTime < 100) {
        handler();
      }
      clearInterval(interval);
      interval = null;
    };

    on(el, 'mousedown', (e) => {
      /* 
        这个方法就是给元素绑定事件,if-else处理了兼容性的情况, attachEvent 是ie的方法, addEventListener 是其他主流浏览器的方法。
         on 的第三个参数就是事件处理函数, on 中第一句 if (e.button !== 0) return 的 e.button 是按下了鼠标的哪个键
        Element源码分析系列7-InputNumber(数字输入框)
        不等于0则是说明按下的不是左键,因为一般只处理左键的点击事件,注意 onclick 只响应鼠标左键的按下,而 onmousedown
        则响应3个键的按下,所以这里要区分。


        on 最后一句 interval = setInterval(handler, 100) 设置了定时器定时执行handler方法从而每隔0.1s触发一次数字增加或减少事件,
        然后我们思考,按下去鼠标时给dom元素添加了事件:定时执行handler,那么在鼠标抬起时肯定要销毁这个定时器,否则将会无限触发handler方法,
        造成数字一直增加或减少,因此 once(document, 'mouseup', clear) 这句话就是在鼠标抬起时销毁定时器,先看clear方法
        里面就是clearInterval销毁定时器,前面的if逻辑很关键,在按下鼠标时记录一个时间,抬起鼠标时检测当前时间 - 按下时的时间
         < 100毫秒,如果是则触发一次点击,如果不写这个if,则无法实现单击操作,因为如果不写,由于interval = setInterval(handler, 100),
         在按下后100毫秒后才会触发一次点击,则在100毫秒内抬起鼠标时interval已经被clear了。最后注意下 once(document, 'mouseup', clear) ,
          once 是只触发一次的高阶函数
      */
      if (e.button !== 0) return;
      startTime = Date.now();
      once(document, 'mouseup', clear);
      clearInterval(interval);
      interval = setInterval(handler, 100);
    });
  }
};

 

posted on 2019-05-31 13:40  心痛随缘  阅读(927)  评论(0编辑  收藏  举报