【筆記】移動端H5數字鍵盤input type=number的處理(IOS和Android)


在Vue中的項目,基於VUX-UI開發,一個常見的需求:

1、金額輸入框

2、彈出數字鍵盤

3、僅支持輸入兩位小數,限制最大11位數,不允許0開頭  

 

后續:與UI溝通后, 思路調整為限制輸入,並減少正則替換輸入值出現的閃動。后續改動如下,注意點如下:

1、處理思路

  A。在用戶輸入的鍵盤事件中,對於不符合的輸入,阻止默認行為和事件冒泡。

    不符合輸入的規則如下:

    1)當前輸入框中的長度大於等於配置的max

    2)非數字和小數點

    3)當前輸入框中已存在小數點,或第一位輸入小數點

  B。在獲取值后,對於不符合兩位小數的值,用watch正則替換后,再下一次渲染(會出現先12.000到12.00的閃動)

 

2、阻止鍵盤事件在哪個階段?

  keypress。

  因為keydown和keyup得到的是keyEvent中鍵值是原始的組合鍵值,需要判斷不同環境和瀏覽器對keycode的實現不同以及是否有shift/alt等。比如在IOS中keydown,對於字符$ @,keycode都是0;中文鍵盤和英文鍵盤中的數字keycode不一致。

  而kepress得到的是組合解析后的實際值,android和ios大部分表現一致。

3、Android的數字鍵盤中的小數點的特殊處理

  調試發現,安卓的數字鍵盤中,小數點做了特殊處理:

    1)無法捕獲到keypress事件

    2)keydown事件中keEvent的keycode是0,無法用於判斷

    3)keydown事件中keEvent的keyIdentifier === 'U+0000'

    4)在keydown事件以及keyuup或其它事件中, 用preventDefault和stopPropagation阻止默認行為和事件冒泡,不能阻止input框輸入小數點.

    所以對這個問題處理,只能沿用之前用在watch中處理空值問題的思路。

 

4、最終效果

  IOS中默認拉起含特殊字符的數字鍵盤,對於非法輸入不會出現任何閃動,對於長度越界的會出現閃動

  Andriod中默認拉起九宮格數字鍵盤,沒有特殊字符,小數點會出現閃動,對於長度越界的會出現閃動

  

<template>
  <XInput
    :title="title"
    :max="currentMax"
    :min="currentMin"
    :type="type"
    v-model="currentValue"
    @on-focus="onFoucus()"
    @on-blur="onBlur()"
    :show-clear="showClear"
    :placeholder="placeholder"
    ref="xinput">
    <template v-if="$slots.label" slot="label"><slot name="label"></slot></template>
    <template v-if="$slots.right" slot="right"><slot name="right"></slot></template>
  </XInput>
</template>

<script>

export default {
  data() {
    return {
      currentValue: this.value,
    };
  },
  computed: {
    currentMax() {
      return (this.type === 'number') ? undefined : this.max;
    },
    currentMin() {
      return (this.type === 'number') ? undefined : this.min;
    }
  },
  props: {
    title: String,
    max: Number,
    min: Number,
    type: String,
    showClear: {
      type: Boolean,
      default: true,
    },
    placeholder: String,
    value: [String, Number],
    filter: {
      type: Function,
      default: (value) => {
        let formattedValue = '';
        const match = value.match(/^([1-9]\d*(\.[\d]{0,2})?|0(\.[\d]{0,2})?)[\d.]*/);
        if (match) {
          formattedValue = match[1];
        }
        return formattedValue;
      },
    }
  },
  watch: {
    currentValue(val, oldVal) {
      // 調用filter過濾數據
      let formattedValue = this.filter(val);
      if (this.type === 'number') {
        formattedValue = this.typeNumberFilter(formattedValue, oldVal);
      }
      if (val !== formattedValue || val === '') {
        setTimeout(() => {
          this.currentValue = formattedValue;
        }, 0);
      }
      this.$emit('input', formattedValue);
    },
    value(value) {
      this.currentValue = value;
    },
  },
  methods: {
    blur() {
      this.$refs.xinput.blur();
    },
    focus() {
      this.$refs.xinput.focus();
    },
    onFoucus() {
      this.$emit('on-focus');
    },
    onBlur() {
      this.$emit('on-blur');
    },
    typeNumberFilter(val, oldVal) {
      const inputEle = this.$refs.xinput.$refs.input;
      let formattedValue = val;
      // TODO: 待大范圍驗證:Android處理連續輸入..后,type=number的input框會把值修改為'',這里手動替換為上次的currentValue
      // 問題描述: 1.00. 不會觸發值改變,1.00.不會觸發值改變,1.00.【\d\.】都會把值修改為空字符串''。hack處理的條件說明如下:
      // 1、輸入框拿到的是空值(因input=number導致輸入框立即被賦予空值。點擊清除按鈕時,這里input輸入框還是上次的值)
      // 2、上次輸入值有效
      if (inputEle.value === '' && oldVal && oldVal.match(/^(\d)[\d.]+/)) {
        formattedValue = oldVal;
      }
      return formattedValue;
    },
    isBackspace(keyCode) {
      return keyCode === 8;
    },
    isDot(keyCode) {
      return keyCode === 46 || keyCode === 190;
    },
    isNumber(keyCode) {
      return (keyCode >= 48 && keyCode <= 57);
    },
    isNotNumberKeycode(keyCode) {
      return !this.isBackspace(keyCode) && !this.isDot(keyCode) && !this.isNumber(keyCode);
    },
    isDotStart(keyCode, inputVal) {
      return this.isDot(keyCode) && (!inputVal || inputVal === '' || /\./.test(inputVal));
    },
    isFinalInput(inputVal) {
      return inputVal.length >= this.max;
    }
  },
  mounted() {
    if (this.type === 'number') {
      const inputEle = this.$refs.xinput.$refs.input;
      inputEle.onkeydown = (e) => {
        // Android小數點特殊處理
        const inputVal = inputEle.value;
        if (e.keyIdentifier === 'U+0000' && (!inputVal || inputVal === '')) {
          inputEle.value = '';
        }
      };
      // eslint-disable-next-line
      inputEle.onkeypress = (e) => {
        const keyCode = e.keyCode;
        const inputVal = inputEle.value;
        if (this.isNotNumberKeycode(keyCode) || this.isDotStart(keyCode, inputVal)
          || this.isFinalInput(inputVal)) {
          e.preventDefault();
          e.stopPropagation();
          return false;
        }
      };
    }
  }
};
</script>
View Code

 

第一,首先想到額就是在VUX-UI中制定type=number。--不可行

  VUX中的文檔和代碼說明,type=number不支持maxLength,會報錯,而且沒有正則替換的處理或者鈎子函數,只有輸入后提示校驗信息。

第二,基於VUX中XInput封裝,有如下問題

  1)兩層v-model,正則替換的值不會觸發input框渲染

  解決:currentValue賦值為foramttedValue,放入setTimeout(func ,0)中,讓input框先渲染為正則替換前的值,再渲染為替換后的值

    currentValue(val, oldVal) {
      // 調用filter過濾數據
      let formattedValue = this.filter(val);
      if (this.type === 'number') {
        formattedValue = this.typeNumberFilter(formattedValue, oldVal);
      }
      if (val !== formattedValue || val === '') {
        setTimeout(() => {
          this.currentValue = formattedValue;
        }, 0);
      }
      this.$emit('input', formattedValue);
    },
View Code

  2)數字鍵盤input type=number,會導致maxlength失效,無法限制長度

  解決:用slice(0, max)處理

      if (formattedValue.length > this.max) {
        formattedValue = formattedValue.slice(0, this.max);
      }
View Code

  3)數字鍵盤input type=number ,連續輸入小數點...導致實際值和顯示值不一致

  解決:用原生的 inputElement.value = oldValue處理

      const inputEle = this.$children[0].$refs.input;
      // TODO: 待大范圍驗證:處理連續輸入..后,type=number的input框會把值修改為''的問題;fastclick導致type=number報錯
      // 問題描述: 1.00. 不會觸發值改變,1.00.不會觸發值改變,1.00.【\d\.】都會把值修改為空字符串''。hack處理的條件說明如下:
      // 1、當校驗后是空值,(因input=number,formattedValue為''表明 原始newVal也為'')
      // 2、輸入框拿到的是空值(因input=number導致輸入框立即被賦予空值。點擊清除按鈕時,這里input輸入框還是上次的值)
      // 3、上次輸入大於兩位(避免最后一位無法刪除的問題。最后一位刪除時,oldVal.length === 1)
      if (formattedValue === '' && inputEle.value === '' && oldVal && oldVal.match(/^(\d)[\d.]+/)) {
        formattedValue = oldVal;
      }
      setTimeout(() => {
        inputEle.value = formattedValue;
      }, 0);
View Code

   4)IOS中數字鍵盤有%$*等特殊字符

  解決:用原生的 inputElement.onkeydown監聽事件,非數字和退格和小數點直接return事件

  mounted() {
    if (this.type === 'number') {
      const inputEle = this.$refs.xinput.$refs.input;
      // eslint-disable-next-line
      inputEle.onkeydown = (e) => {
        const keyCode = e.keyCode;
        if (!this.isBackspace(keyCode) && !this.isDot(keyCode) && !this.isNumber(keyCode)) {
          // 其他按鍵
          e.preventDefault();
          e.stopPropagation();
          return false;
        }
      };
    }
  }
View Code

 

第三,其他說明

  為什么不用 type=tel?

    type=tel在ios中沒有小數點

 

第四,全部代碼

 

<template>
  <XInput
    :title="title"
    :max="currentMax"
    :min="currentMin"
    :type="type"
    v-model="currentValue"
    @on-focus="onFoucus()"
    @on-blur="onBlur()"
    :show-clear="showClear"
    :placeholder="placeholder"
    ref="xinput">
    <template v-if="$slots.label" slot="label"><slot name="label"></slot></template>
    <template v-if="$slots.right" slot="right"><slot name="right"></slot></template>
  </XInput>
</template>

<script>

export default {
  data() {
    return {
      currentValue: this.value,
    };
  },
  computed: {
    currentMax() {
      return (this.type === 'number') ? undefined : this.max;
    },
    currentMin() {
      return (this.type === 'number') ? undefined : this.min;
    }
  },
  props: {
    title: String,
    max: Number,
    min: Number,
    type: String,
    showClear: {
      type: Boolean,
      default: true,
    },
    placeholder: String,
    value: [String, Number],
    filter: {
      type: Function,
      default: (value) => {
        let formattedValue = '';
        const match = value.match(/^([1-9]\d*(\.[\d]{0,2})?|0(\.[\d]{0,2})?)[\d.]*/);
        if (match) {
          formattedValue = match[1];
        }
        return formattedValue;
      },
    }
  },
  watch: {
    currentValue(val, oldVal) {
      // 調用filter過濾數據
      let formattedValue = this.filter(val);
      if (this.type === 'number') {
        formattedValue = this.typeNumberFilter(formattedValue, oldVal);
      }
      if (val !== formattedValue || val === '') {
        setTimeout(() => {
          this.currentValue = formattedValue;
        }, 0);
      }
      this.$emit('input', formattedValue);
    },
    value(value) {
      this.currentValue = value;
    },
  },
  methods: {
    onFoucus() {
      this.$emit('on-focus');
    },
    onBlur() {
      this.$emit('on-blur');
    },
    typeNumberFilter(val, oldVal) {
      const inputEle = this.$refs.xinput.$refs.input;
      let formattedValue = val;
      // 由於type=number不支持maxLength,用slice模擬
      if (formattedValue.length > this.max) {
        formattedValue = formattedValue.slice(0, this.max);
      }
      // TODO: 待大范圍驗證:處理連續輸入..后,type=number的input框會把值修改為''的問題;fastclick導致type=number報錯
      // 問題描述: 1.00. 不會觸發值改變,1.00.不會觸發值改變,1.00.【\d\.】都會把值修改為空字符串''。hack處理的條件說明如下:
      // 1、當校驗后是空值,(因input=number,formattedValue為''表明 原始newVal也為'')
      // 2、輸入框拿到的是空值(因input=number導致輸入框立即被賦予空值。點擊清除按鈕時,這里input輸入框還是上次的值)
      // 3、上次輸入大於兩位(避免最后一位無法刪除的問題。最后一位刪除時,oldVal.length === 1)
      if (formattedValue === '' && inputEle.value === '' && oldVal && oldVal.match(/^(\d)[\d.]+/)) {
        formattedValue = oldVal;
      }
      setTimeout(() => {
        inputEle.value = formattedValue;
      }, 0);
      return formattedValue;
    },
    isBackspace(keyCode) {
      return keyCode === 8;
    },
    isDot(keyCode) {
      return keyCode === 46 || keyCode === 110 || keyCode === 190;
    },
    isNumber(keyCode) {
      return (keyCode >= 48 && keyCode <= 57) || (keyCode >= 96 && keyCode <= 105);
    },
  },
  mounted() {
    if (this.type === 'number') {
      const inputEle = this.$refs.xinput.$refs.input;
      // eslint-disable-next-line
      inputEle.onkeydown = (e) => {
        const keyCode = e.keyCode;
        if (!this.isBackspace(keyCode) && !this.isDot(keyCode) && !this.isNumber(keyCode)) {
          // 其他按鍵
          e.preventDefault();
          e.stopPropagation();
          return false;
        }
      };
    }
  }
};
</script>
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM