输入框只允许输入合法数字(解决type='number'中的bug)

2,211 阅读2分钟

前言:

在实际开发过程中,输入框输入正确且合理的数字是一个比较头疼的事情,我见过最多的解决方案是输入完成后,当用户点击提交提示用户输入有误,这样的处理方式对于用户来说是不合理的。在以前的开发过程中,为了缩短开发周期我也是这样做的,直到有一次和朋友聊天,他说他需求里需要一个type为number的input而且只能输入正确的数字,并且需要添加千分符。

后期有其他组件发布,可持续关注GitHub,欢迎Start、Watch

1、少啰嗦,先看东西

<template>
  <div
    :class="[type==='textarea'?'crazy-textarea':'crazy-input',{'crazy-input__focus':type==='number'&&isFocus},{'is-disabled':disabled}]"
    @mousedown="inputClick($event)">
    <template v-if="type!=='textarea'">
      <input
        class="crazy-input__inner"
        ref="input"
        :type="type==='number'?'text':type"
        :disabled="disabled"
        :readonly="readonly"
        v-bind="$attrs"
        @input="inputValueDispose($event.target)"
        @focus="handleFocus($event)"
        @blur="handleBlur($event)"
        @change="handleChange($event)"
        @compositionstart="compositionStart"
        @compositionend="compositionEnd($event.target)"/>
      <span v-if="wordLimitVisible && !disabled" class="crazy-input__count">
        {{ currentInputLength }}/{{ $attrs.maxlength }}
      </span>
      <div
        v-if="type==='number' && showStepButton && !disabled"
        @mousedown="inputButtonMousedown($event)"
        @mouseup="inputButtonMouseup"
        class="crazy-input-number-button__wrap">
        <span
          @mouseleave="inputButtonMouseup"
          :class="['crazy-input-number__button','crazy-input-number-button__top',{'crazy-input-number__button__mousedown':currentMousedownButton==='top'}]"></span>
        <span
          @mouseleave="inputButtonMouseup"
          :class="['crazy-input-number__button','crazy-input-number-button__bottom',{'crazy-input-number__button__mousedown':currentMousedownButton==='bottom'}]"></span>
      </div>
    </template>
    <template v-else>
      <textarea
        class="crazy-textarea__inner"
        ref="textarea"
        :tabindex="tabindex"
        :disabled="disabled"
        :readonly="readonly"
        v-bind="$attrs"
        @input="handleInput($event.target)"
        @focus="handleFocus($event)"
        @blur="handleBlur($event)"
        @change="handleChange($event)">
      </textarea>
      <span v-if="wordLimitVisible && !disabled" class="crazy-textarea__count">{{ currentInputLength }}/{{ $attrs.maxlength }}</span>
    </template>
  </div>
</template>

<script>
  export default {
    name: "CInput",
    inheritAttrs: false,
    data() {
      return {
        oldValue: '',
        isFocus: false,
        currentMousedownButton: "",
        intervalTimer: null,
        timeoutTimer: null,
        isComposition: false
      };
    },
    props: {
      value: [String, Number],
      tabindex: String,
      readonly: Boolean,
      disabled: Boolean,
      thousandMark: Boolean,
      valueThousandMark: Boolean,
      showWordLimit: Boolean,
      showStepButton: {
        type: Boolean,
        default: true
      },
      type: {
        type: String,
        default: "text"
      },
      step: {
        type: Number,
        default: 1
      }
    },
    computed: {
      currentInputLength() {
        return (String(this.value) || '').length;
      },
      nativeInputValue() {
        return this.value === null || this.value === undefined ? '' : String(this.value);
      },
      wordLimitVisible() {
        return this.showWordLimit && this.$attrs.maxlength && (this.type === 'text' || this.type === 'textarea');
      }
    },
    watch: {
      nativeInputValue() {
        this.setNativeInputValue();
      }
    },
    methods: {
      compositionStart() {
        if (this.type !== 'number') return;
        this.isComposition = true;
      },
      compositionEnd(input) {
        if (this.type !== 'number') return;
        this.isComposition = false;
        let value = input.value.replace(/,/g, '').replace(/([^.\-\d]|^\..*).*/g, "");
        input.value = this.thousandMark ? this.addThousandMark(value) : value;
      },
      setFocusStatus(status) {
        this.isFocus = status;
        !status && this.showStepButton && this.inputButtonMouseup();
      },
      focus() {
        this.getInput().focus();
      },
      blur() {
        this.getInput().blur();
      },
      handleFocus(event) {
        this.$emit('focus', event);
        this.type === 'number' && this.setFocusStatus(true);
      },
      handleBlur(event) {
        this.$emit('blur', event);
        this.type === 'number' && this.setFocusStatus(false);
      },
      handleChange(event) {
        this.$emit('change', this.getInputValue(event.target.value));
      },
      setNativeInputValue() {
        const input = this.getInput();
        if (!input) return;
        let value = this.type === 'number' && this.thousandMark ? this.addThousandMark(this.nativeInputValue) : this.nativeInputValue;
        this.type === 'number' && this.setOldValue(value);
        if (this.getInputValue(input.value) === this.nativeInputValue) return;
        input.value = value;
      },
      setOldValue(value) {
        this.oldValue = value;
      },
      handleInput(input) {
        this.type === 'textarea' && this.wordLimitVisible && this.setTextareaScrollTop(input);
        let value = this.getInputValue(input.value);
        this.$emit('input', value);
        this.$nextTick(this.setNativeInputValue);
      },
      setTextareaScrollTop(input) {
        if (input.value.length === input.selectionEnd) {
          input.scrollTop = input.scrollHeight;
        }
      },
      getInput() {
        return this.$refs.input || this.$refs.textarea;
      },
      getInputValue(value) {
        return this.type === 'number' && this.thousandMark && !this.valueThousandMark ? this.removeThousandMark(value) : value;
      },
      inputButtonMouseup() {
        this.currentMousedownButton = "";
        this.clearTimer();
      },
      clearTimer() {
        clearTimeout(this.timeoutTimer);
        clearInterval(this.intervalTimer);
      },
      startTimer(direction) {
        this.inputValueOperation(direction);
        this.timeoutTimer = setTimeout(() => {
          this.intervalTimer = setInterval(() => {
            this.inputValueOperation(direction);
          }, 70);
        }, 300);
      },
      inputClick(e) {
        if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
        e.preventDefault();
      },
      inputValueOperation(direction) {
        let input = this.getInput();
        if (!input.value) {
          if (direction === "top") {
            input.value = this.step;
          }
          if (direction === "bottom") {
            input.value = -this.step;
          }
        } else {
          if (direction === "top") {
            input.value = parseFloat(input.value.replace(/,/g, "")) + this.step;
          }
          if (direction === "bottom") {
            input.value = parseFloat(input.value.replace(/,/g, "")) - this.step;
          }
        }
        this.inputValueDispose(input);
      },
      inputButtonMousedown(e) {
        this.focus();
        let classNames = e.target.className.split(" ");
        let result = classNames.find(item =>
          item.startsWith("crazy-input-number-button")
        );
        this.currentMousedownButton = result.replace(/(crazy-input-number-button__)/g, "");
        this.clearTimer();
        this.startTimer(this.currentMousedownButton);
      },
      inputValueDispose(input) {
        if (this.isComposition) return;
        if (this.type === "number" && input.value) {
          input.value = input.value.replace(/([^.\-\d]|^\..*)/g, "");
          //3、负号后不能出现非数字
          let minusBehindNoNumber = /^-[^\d].*/g;
          if (minusBehindNoNumber.test(input.value)) {
            input.value = "-";
          }
          //4、不能出现第二个点
          let findDotNumber = input.value.match(/\./g) || [];
          let findDot = new RegExp("\\.", "g");
          if (findDotNumber.length > 1) {
            findDot.exec(input.value);
            findDot.exec(input.value);
            input.value = input.value.slice(0, findDot.lastIndex - 1);
          }
          //5、以0或-0开头后边必须是点
          if (/(^0[^.]|^-0[^.])/g.test(input.value)) {
            input.value = input.value[0] === "-" ? "-0" : "0";
          }
          // 6、以0.或-0.开头后边不能出现非数字
          if (/(^0\.[^\d]|^-0\.[^\d])/g.test(input.value)) {
            input.value = input.value[0] === "-" ? "-0." : "0.";
          }
          // 7、数字和小数点后边不能出现其他符号
          if (/(\d[^\d^.].*|\.[^\d^.].*)/g.test(input.value)) {
            let index = /(\d[^\d^.].*|\.[^\d^.].*)/g.exec(input.value).index;
            input.value = input.value.slice(0, index + 1);
          }
          if (this.thousandMark) input.value = this.addThousandMark(input.value);
          if (input.value === this.oldValue) return;
        }
        this.handleInput(input);
        /**
         * 正则表达式内含有大量后行断言,IE、Edge、Safari等不支持
         * new RegExp可以打包成功,但是很多浏览器会报错
         * 直接用正则表达式替换,npm run dev正常运行,打包会报错
         */
        // let reg = new RegExp('([^\\-^\\.^\\d]|^\\..*|(?<=^\\-)\\D.*|(?<=\\..*)[\\.].*|(?<=(^0|^\\-0))[^\\.].*|(?<=(^0\\.\\d*|^\\-0\\.\\d*))[^\\d].*|(?<=(\\d|\\.))[\\-].*)','g')
        // this.moneyNumber = inputValue.replace(reg, '');
        // this.moneyNumber = inputValue.replace(/([^\-^\.^\d]|^\..*|(?<=^\-)\D.*|(?<=\..*)[\.].*|(?<=(^0|^\-0))[^\.].*|(?<=(^0\.\d*|^\-0\.\d*))[^\d].*|(?<=(\d|\.))[\-].*)/g, '');
      },
      addThousandMark(value) {
        return value.replace(/(\d)(?=(\d{3})+($|\.))/g, ",")
      },
      removeThousandMark(value) {
        return value.replace(/,/g, "")
      }
    },
    mounted() {
      this.setNativeInputValue();
    }
  };
</script>

less

2、设计思路

1、为什么不选用类型为Number的Input?

首先在有输入法且为中文的情况下,它可以输入字母和中文。其次maxlength无效

2、事件

首先输入法输入时不触发oninput,所以使用compositionstart和compositionend修改状态

其次输入框值发送变化就应该做校验,所以用oninput

3、正则(3、4、5、6、7为后行断言-->大多数浏览器不支持,8为先行断言)

1、数字只有负号、小数点、数字组成
'asd123--4541.-'.replace(/[^\-^\.^\d]/g,'')
2、小数点不能出现在第一位
'.5464'.replace(/^\..*/g,'')
3、负号后边不能出现非数字
'-.5656'.replace(/(?<=^\-)\D.*/g,'')
4、不能出现第二个点
'123.s.s13132.1365..5156'.replace(/(?<=\..*)[\.].*/g,'')
5、以0或-0开头后边必须是点
'-012313'.replace(/(?<=(^0|^\-0))[^\.].*/g,'')
6、以0.或-0.开头后边不能出现非数字
'0.5-.asd123'.replace(/(?<=(^0\.\d*|^\-0\.\d*))[^\d].*/g,'')
7、数字和小数点后边不能出现其他符号
'-12.-'.replace(/(?<=(\d|\.))[\-].*/g,'')
8、千分符
'13256645'.replace(/(\d)(?=(\d{3})+($|\.))/g, '$1,')

4、解决方案

1、matchAll(截止目前IE、Edge、Safari等浏览器不支持matchAll)
let matchIterator = '23.3211-'.matchAll(/(\d[^\d^\.].*|\.[^\d^\.].*)/g)
let index = matchIterator.next().value.index
console.log('23.3123-'.slice(0,index+1))

matchAll

2、exec
let index = /(\d[^\d^.].*|\.[^\d^.].*)/g.exec('12.-56').index
console.log('12.-56'.slice(0, index + 1))
let findDotNumber = '13.156.4d-'.match(/\./g) || [];
let findDot = new RegExp('\\.', 'g');
if (findDotNumber.length > 1) {
  findDot.exec('13.156.4d-');
  findDot.exec('13.156.4d-');
  console.log('13.156.4d-'.slice(0, findDot.lastIndex - 1))
}

总结:

在开发此类组件时,我们需要不断观察原生输入框的优缺点,在写组件过程中,才可以完善原生输入框的不足,继承原生输入框的优点

未上过生产,如出现问题,请私信联系

转发请附原文地址