input/input.vue

<template>
  <div class="ion-input" :class="[modeClass,{'clearInput':isClearInput}]" @click="setFocus($event)">
    <div class="input-inner-wrap">
      <input ref="input"
        :class="['text-input', 'text-input-'+mode]"
        :value="inputValue"
        :type="typeValue"
        :placeholder="placeholder"
        :disabled="isDisabled"
        :readonly="isReadonly"
        :max="max"
        :min="min"
        :step="step"
        :autofocus="autofocus"
        :autocorrect="autocorrect"
        :autocomplete="autocomplete"
        :autocapitalize="autocapitalize"
        @keyup="inputKeyUp($event)"
        @blur="inputBlurred($event)"
        @focus="inputFocused($event)"
        @input="inputChanged($event)"
        @keydown="inputKeyDown($event)">
    </div>
    <vm-button v-if="isClearInput && hasValue"
      clear
      class="text-input-clear-icon"
      @click="clearTextInput()"></vm-button>
  </div>
</template>

<script type="text/javascript">
import REGEXP from '../../util/regexp'
import debounce from 'lodash.debounce'
import {isTrueProperty, isBlank, isFunction, isObject, isPresent, isRegExp} from '../../util/util'
import {hasFocus} from '../../util/dom'
import ModeMixins from '../../themes/theme.mixins'
import VmButton from '../button'

export default {
  name: 'vm-input',
  mixins: [ModeMixins],
  inject: {
    itemComponent: {
      from: 'itemComponent',
      default: null
    }
  },
  components: {
    VmButton
  },
  props: {
    /**
     * @input {string} The type of control to display. The default type is text.
     * Possible values are: `"text"`, `"password"`, `"email"`, `"number"`, `"search"`, `"tel"`, or `"url"`.
     */
    type: {
      type: String,
      default: 'text'
    },

    value: {
      type: [String, Number],
      required: false
    },

    /**
     * @input {boolean} If true, a clear icon will appear in the input when there is a value. Clicking it clears the input.
     */
    clearInput: {
      type: [String, Boolean],
      default: false
    },

    /**
     * @input {boolean} If true, the user cannot modify the value.
     */
    readonly: {
      type: [String, Boolean],
      default: false
    },

    /**
     * @input {boolean} If true, the user cannot modify the value.
     */
    disabled: {
      type: [String, Boolean],
      default: false
    },

    autocapitalize: String,
    autocomplete: String,
    autocorrect: String,
    autofocus: [Boolean, String],

    /**
     * @input {string} Instructional text that shows before the input has a value.
     */
    placeholder: String,

    /**
     * @input {any} The minimum value, which must not be greater than its maximum (max attribute) value.
     */
    min: {
      type: [String, Number],
      validator (val) {
        return parseInt(val) >= 0
      }
    },

    /**
     * @input {any} The maximum value, which must not be less than its minimum (min attribute) value.
     */
    max: {
      type: [String, Number],
      validator (val) {
        return parseInt(val) >= 0
      }
    },

    /**
     * @input {any} Works with the min and max attributes to limit the increments at which a value can be set.
     */
    step: {
      type: [String, Number],
      validator (val) {
        return parseInt(val) >= 0
      }
    },

    /**
     * focus时, 下划线是否高亮
     */
    showFocusHighlight: Boolean,

    /**
     * 验证成功是否显示 highlight
     */
    showValidHighlight: Boolean,

    /**
     * 验证失败是否显示 highlight
     */
    showInvalidHighlight: Boolean,

    /**
     * 设置type=number的小数位
     */
    decimal: {
      type: [String, Number],
      validator (val) {
        return parseInt(val) >= 0
      },
      default: 2
    },

    debounce: {
      type: [String, Number],
      validator (val) {
        return parseInt(val) >= 0
      },
      default: 10
    },

    // 自定义输入结果验证的正则表达式
    regex: RegExp,

    /**
     * 是否valid输入结果, 如果regex有值, 则开启, 否自关闭.
     * 如果valid开启, 但是regex无值, 则使用内置判断
     * 默认关闭valid
     */
    valid: [Boolean, String]
  },
  data () {
    return {
      oldInputValue: null, // 内部value值
      inputValue: this.value, // 内部value值
      typeValue: this.type, // 内部type值

      isValid: isTrueProperty(this.valid) || this.regex, // 内部valid值, 判断是否需要验证结果
      isReadonly: isTrueProperty(this.readonly),
      isDisabled: isTrueProperty(this.disabled),
      isAutofocus: isTrueProperty(this.autofocus),
      isClearInput: isTrueProperty(this.clearInput),

      executeEmit () {},

      shouldBlur: true // 点击清楚按钮时使用
    }
  },
  watch: {
    value (val) {
      this.inputValue = val
      this.setItemHasValueClass()
    }
  },
  computed: {
    inputElement () {
      return this.$refs.input
    },
    hasValue () {
      const inputValue = this.inputValue
      return (inputValue !== null && inputValue !== undefined && inputValue !== '')
    }
  },
  created () {
    // 根据 REGEXP 匹配 type 的真正规则
    if (isObject(REGEXP[this.type]) && isPresent(REGEXP[this.type].type)) {
      this.typeValue = REGEXP[this.type].type
    } else {
      this.typeValue = this.type
    }

    // 生成emit执行体
    this.executeEmit = this.initDebounce()
  },
  mounted () {
    // 找到外部item实例
    if (this.itemComponent) {
      this.itemComponent.setElementClass('item-input', true)
      this.itemComponent.setElementClass('show-focus-highlight', this.showFocusHighlight)
      this.itemComponent.setElementClass('show-valid-highlight', this.showValidHighlight)
      this.itemComponent.setElementClass('show-invalid-highlight', this.showInvalidHighlight)
    }

    // 初始化时,判断是否有value
    this.setItemHasValueClass()

    // 手动focus
    if (this.isAutofocus) {
      window.setTimeout(() => {
        this.setFocus()
      }, 16 * 3)
    }
  },
  methods: {
    setFocus () {
      if (!hasFocus(this.inputElement)) {
        this.inputElement.focus()
      }
    },

    clearTextInput () {
      this.inputValue = ''
      this.inputChanged()
      this.shouldBlur = false

      this.setFocus()
      this.setItemHasFocusClass(true)
    },

    checkBoundary ($event) {
      let inputText = $event.target.value // text
      let resetValue = null

      // 数字边界限制
      // 这段代码已在很卡顿的安卓机上试验过了, 之所以不在watch阶段重置, 是因为在较慢的安卓机上有数字抖动的情况
      // 现在已能很好的处理
      if (this.typeValue === 'number') {
        resetValue = inputText
        if (isPresent(inputText)) {
          if (isPresent(this.max) && parseFloat(inputText) > parseInt(this.max)) {
            resetValue = this.oldInputValue
          }

          if (isPresent(this.min) && parseFloat(inputText) < parseInt(this.min)) {
            resetValue = this.min
          }

          // 小数点检查, 使用string的方式, number的方式会有奇怪的问题, 比如: 222.22 -> 222.19
          let int = resetValue.toString().split('.')[0]
          let decimals = resetValue.toString().split('.')[1]

          if (decimals && this.decimal > 0) {
            if (decimals.length > this.decimal) {
              decimals = decimals.substr(0, this.decimal)
              resetValue = `${int}.${decimals}`
            }
          }
        }

        if (resetValue !== inputText) {
          $event.target.value = resetValue
        }
      } else {
        resetValue = inputText
        // 非数字 且有 最大长度限制
        if (isPresent(this.max) && isPresent(inputText) && inputText.toString().length > this.max) {
          resetValue = this.oldInputValue
          // 重置 input 输入框
          $event.target.value = resetValue
        }
      }

      return resetValue
    },

    inputKeyUp ($event) {
      this.$emit('onKeyup', $event)
    },

    /**
     * @event component:Input#onKeydown
     * @description keydown事件
     * @private
     */
    inputKeyDown ($event) {
      this.$emit('onKeydown', $event)

      this.oldInputValue = this.inputValue
    },

    /**
     * 监听并发送blur事件
     * @private
     */
    inputBlurred () {
      // debug: clearInput会在onBlur之后,造成blur后点击clearInput失效, 故需要延迟blur
      window.setTimeout(() => {
        if (this.shouldBlur) {
          // 向父组件Item添加标记
          this.setItemHasFocusClass(false)

          this.$emit('onBlur')

          // 验证输入结果
          this.validation()
        } else {
          this.shouldBlur = true
        }
      }, 16 * 2)
    },

    /**
     * 监听并发送focus事件
     * @private
     */
    inputFocused () {
      // 向父组件Item添加标记
      this.setItemHasFocusClass(true)
      this.setFocus()

      this.$emit('onFocus')
      this.itemComponent && this.itemComponent.setElementClass('ng-touched', true)
    },

    /**
     * 监听input事件, 更新input的value(inputValue)
     * @param {Event} [$event] - 事件(可选)
     * @private
     */
    inputChanged ($event) {
      if ($event && $event.target) {
        // 输入限制检查
        this.inputValue = this.checkBoundary($event)
        // debounce
        this.executeEmit()
      } else {
        // clear的情况
        // 需要同步设置input元素的值
        this.inputElement.value = null
        this.inputValue = null
        // 立即发送变化, 不需要debounce
        this.emitChange()
      }

      this.setItemHasValueClass()
    },

    /**
     * 执行验证, 如果错误则设置ng-invalid, 正确则设置ng-valid
     * @private
     */
    validation () {
      // 只有开启才检查
      if (!this.isValid) return

      console.log(this.inputValue, this.type)
      let isValid = this.getVerifyResult(this.inputValue, this.type)
      if (isValid) {
        this.$emit('onValid', this.inputValue, this.type)
        if (this.itemComponent) {
          this.itemComponent.setElementClass('ng-valid', true)
          this.itemComponent.setElementClass('ng-invalid', false)
        }
      } else {
        this.$emit('onInvalid', this.inputValue, this.type)
        if (this.itemComponent) {
          this.itemComponent.setElementClass('ng-valid', false)
          this.itemComponent.setElementClass('ng-invalid', true)
        }
      }
    },

    /**
     * 获取验证结果
     * @param {*} value - 待验证的值
     * @param {String} type - 待验证的值的类型
     * @return Boolean
     * @private
     */
    getVerifyResult (value, type = 'text') {
      if (!isPresent(value)) {
        return false
      }

      let _regex = this.regex
      if (isBlank(_regex)) {
        let regexpInfo = REGEXP[type]
        if (regexpInfo && (isRegExp(regexpInfo) || isFunction(regexpInfo))) {
          _regex = regexpInfo
        } else if (regexpInfo && (isRegExp(regexpInfo.regexp) || isFunction(regexpInfo.regexp))) {
          _regex = regexpInfo.regexp
        }
      }

      // 如果没有正则信息则返回true, 表示不验证
      if (!isPresent(_regex)) {
        return false
      }

      // 如果是函数则执行判断
      if (isFunction(_regex)) {
        return _regex(value)
      }

      // 判断是不是正则
      if (isRegExp(_regex)) {
        return _regex.test(value)
      }

      return false
    },

    initDebounce () {
      if (parseInt(this.debounce) > 0) {
        return debounce(function () {
          this.emitChange()
        }, parseInt(this.debounce))
      } else {
        return () => {
          this.emitChange()
        }
      }
    },

    emitChange () {
      /**
       * @event  component:Input#onInput
       * @description input事件
       * @property {*} value - 当前输入的值
       */
      this.$emit('onInput', this.inputValue)
      this.$emit('input', this.inputValue)
    },

    setItemHasFocusClass (isFocused) {
      this.itemComponent && this.itemComponent.setElementClass('item-input-has-focus', isFocused)
    },

    setItemHasValueClass () {
      this.itemComponent && this.itemComponent.setElementClass('item-input-has-value', !!this.hasValue)
    }
  }
}
</script>

<style lang="scss">
  @import "input";
  @import "input.ios";
  @import "input.md";
</style>