<template>
<div class="ion-input" :class="[modeClass,inputClass,{'clearInput':clearInput}]" @click="$_clickToFocus($event)">
<div class="input-inner-wrap">
<input ref="input"
:class="[textInputClass]"
:value="inputValue"
:type="typeValue"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:max="max"
:min="min"
:step="step"
:autofocus="autofocus"
@keyup="$_inputKeyUp($event)"
@blur="$_inputBlurred($event)"
@focus="$_inputFocused($event)"
@input="$_inputChanged($event)"
@keydown="$_inputKeyDown($event)">
</div>
<vm-button v-if="clearInput && hasValue"
clear
class="text-input-clear-icon"
@click="$_clearTextInput()"></vm-button>
</div>
</template>
<style lang="scss" src="./style.scss"></style>
<script type="text/javascript">
/**
* @component Input
* @description
*
* ## 表单组件 Input输入框
*
* ### 注意
*
* Input组件只能对以下类型的type作出相应 : `text`,`password`, `email`, `number`, `search`, `tel`, and `url`. 但是不适用一下类型: `checkbox`, `radio`, `toggle`, `range`, `select`, etc.
*
* ### 如何引入
* ```
* // 引入
* import { Input } from 'vimo'
* // 安装
* Vue.component(Input.name, Input)
* // 或者
* export default{
* components: {
* Input
* }
* }
* ```
*
* ### 关于输入验证
*
* - 只在blur阶段才进行
* - ```check```默认关闭, ```check```只是作为内部正误标示, 只有开启了检查, 才会发出```onValid```和```onInvalid```两个事件, 外部提交判断需要额外代码判断(内部```isValid```变量).
* - 如果```check```开启, 但是regex无值, 则使用内置判断(如下).
* - 如果```regex```有值, 则自动开启```check```, 支持的regex可以使正则, 也可以是返回Boolena的函数, 传入参数为传入的value
* - 内部验证的typ如下
*
* ### 内置的验证type
*
* 名称 | 类型 |内部类型 | 说明
* ------|-----------------|------------------------------------------------------------------------------
* 整数 | integer | number| 整数
* 正整数 | positiveInteger |number| 正整数
* 负整数 | negativeInteger |number| 负整数
* 邮箱 | email |email| 电子邮件
* IP地址 | ip |number| IP地址
* 身份证 | idCard | text| 严格验证
* 密码 | password | password|密码需6-18位,以字母开头可含数字
* 国内电话 | tel | tel|正确格式为:XXXX-XXXXXXX,XXXX- XXXXXXXX,XXX-XXXXXXX,XXX-XXXXXXXX,XXXXXXX,XXXXXXXX。
* 国内手机号 | mobile | tel|13/14/15/18/17开头
* 验证汉字 | cn |text|
* 验证码 | securityCode | number|至少4位数字
* 昵称 | nickName | text|可由中英文字母、数字、"-"、"_"组成。
* QQ号码 | qq | number|qq: 1-9开头,最少5位。
* 网址URL | url | url|网址URL, 必须以(https,http,ftp,rtsp,mms)开头
*
* @props {Boolean} [clearInput] - 如果为true, 当输入值的时候一个清除按钮会在input右边出现, 点击按钮则清除输入.
* @props {Boolean} [clearOnEdit] - 如果为true, 当再次输入的时候会清空上次的输入, 如果type为password时默认为true, 其余情况默认为false, 默认值的变更, 需要js控制
* @props {Boolean} [disabled] - 如果为true, 用户无法输入
* @props {Boolean} [showFocusHighlight] - focus时底部是否 highlight 显示
* @props {Boolean} [showValidHighlight] - 验证成功是否显示 highlight 显示
* @props {Boolean} [showInvalidHighlight] - 验证失败是否显示 highlight 显示
* @props {Number} [max] - 设置最大值, 1. type=number时限制输入数字的大小; 2. type=text时限制输入字符的长度
* @props {Number} [min] - 设置最小值, 1. type=number时限制输入数字的大小; 2. type=text时限制输入字符的长度
* @props {Number} [decimal=2] - 设置数字的小数位, 默认为2
* @props {Number} [step] - 设置数字变化的阶梯值, 只对type=number有效
* @props {String} [mode='ios'] - 当前平台
* @props {String} [placeholder] - 占位文字
* @props {Boolean} [readonly] - 只读模式, 不能修改
* @props {String} [type='text'] - 输入的类型: "text", "password", "email", "number", "search", "tel", or "url"
* @props {*} [value] - 内容输入值
* @props {Number} [debounce=0] - 触发间隔
* @props {RegExp} [regex] - 自定义正则
* @props {Boolean} [check] - 是否check输入结果, 如果regex有值, 则开启, 否则关闭. 如果check开启, 但是regex无值, 则使用内置判断. 默认关闭check, check只是作为内部正误标示, 对外提交不起作用, 如果点击能知道各个input的状态, 需要在dom中search'ng-invalid'类名, 这样的话, 验证位置就会统一.
*
* @fires component:Input#onBlur
* @fires component:Input#onFocus
* @fires component:Input#onInput
* @fires component:Input#onKeyup
* @fires component:Input#onKeydown
* @fires component:Input#onValid
* @fires component:Input#onInvalid
*
* @demo #/input
* @usage
* <Input placeholder="Text Input">
* <Input placeholder="Clear Input" clearInput></Input>
* <Input placeholder="请输入手机号" type="mobile" check clearInput></Input>
* <Input placeholder="请输入至少4位" type="securityCode" check clearInput></Input>
* <Input placeholder="XX-XX-XXX格式" type="text" check :regex=/\d{2}-\d{2}-\d{3}/ clearInput></Input>
* */
import { hasFocus } from '../../util/util'
import Button from '../button'
import REGEXP from '../../util/regexp'
import debounce from 'lodash.debounce'
import { isBlank, isFunction, isObject, isPresent, isRegexp } from '../../util/type'
export default {
name: 'Input',
inject: {
itemComponent: {
from: 'itemComponent',
default: null
}
},
components: {
'vm-button': Button
},
props: {
/**
* focus时, 下划线是否高亮
* */
showFocusHighlight: Boolean,
/**
* 验证成功是否显示 highlight
* */
showValidHighlight: Boolean,
/**
* 验证失败是否显示 highlight
* */
showInvalidHighlight: Boolean,
/**
* 如果为true, 当输入值的时候一个清除按钮会在input右边出现, 点击按钮则清除输入
* */
clearInput: Boolean,
/**
* 如果为true, 当再次输入的时候会清空上次的输入, 如果type为password时默认为true, 其余情况默认为false
* 默认值的变更, 需要js控制
* */
clearOnEdit: Boolean,
/**
* 如果为true, 用户无法输入
* */
disabled: Boolean,
/**
* 设置最大值, 用于text/number等类型的长度限制
* */
max: Number,
/**
* 设置最小值
* */
min: Number,
/**
* 设置type=number的小数位
* */
decimal: {
type: Number,
validator (val) {
return val >= 0
},
default: 2
},
/**
* 自动focus
* */
autofocus: Boolean,
/**
* 设置数字变化的阶梯值, 只对type=number有效
* */
step: Number,
/**
* 当前平台
* */
mode: {
type: String,
default () { return this.$config && this.$config.get('mode', 'ios') || 'ios' }
},
placeholder: String,
/**
* 只读模式, 不能修改
* */
readonly: Boolean,
/**
* 输入的类型: "text", "password", "email", "number", "search", "tel", or "url"
* */
type: {
type: String,
default: 'text'
},
/**
* 内容输入值
* */
value: [String, Number, Object, Function],
debounce: {
type: Number,
default: 0
},
// 自定义输入结果验证的正则表达式
regex: RegExp,
/**
* 是否check输入结果, 如果regex有值, 则开启, 否自关闭.
* 如果check开启, 但是regex无值, 则使用内置判断
* 默认关闭check
* check只是作为内部正误标示, 对外提交不起作用
* 如果点击能知道各个input的状态, 需要在dom中search'ng-invalid'类名
* 这样的话, 验证位置就会统一.
* */
check: Boolean
},
data () {
return {
oldInputValue: null, // 内部value值
inputValue: this.value, // 内部value值
typeValue: this.type, // 内部type值
checkValue: this.check || this.regex, // 内部check值, 判断是否需要验证结果
isValid: false, // 验证结果
executeEmit () {},
clearOnEditValue: this.clearOnEdit, // 内部维护的clearOnEdit副本, 因为会修改的
didBlurAfterEdit: false, // clearOnEdit状态唤起的标志
shouldBlur: true, // 点击清楚按钮时使用
inputClass: {
'input-has-value': false,
'input-has-focus': false
}
}
},
watch: {
value (val) {
this.inputValue = val
},
inputValue (newVal, oldVal) {
}
},
computed: {
modeClass () {
return `input-${this.mode}`
},
textInputClass () {
return `text-input text-input-${this.mode}`
},
inputElement () {
return this.$refs.input
},
hasValue () {
const inputValue = this.inputValue
return (inputValue !== null && inputValue !== undefined && inputValue !== '')
}
},
methods: {
/**
* 设置当前组件为focus状态
* @public
* */
setFocus () {
if (!hasFocus(this.inputElement)) {
this.inputElement.focus()
}
},
/**
* 边界检查
* @param {String|Number} val - 数值
* @param {String} text - 对应的string
* @private
* */
$_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) > this.max) {
resetValue = this.oldInputValue
}
if (isPresent(this.min) && parseFloat(inputText) < 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
},
/**
* @event component:Input#onKeyup
* @description keyup事件
* @private
*/
$_inputKeyUp ($event) {
this.$emit('onKeyup', $event)
},
/**
* @event component:Input#onKeydown
* @description keydown事件
* @private
*/
$_inputKeyDown ($event) {
this.$emit('onKeydown', $event)
if (this.clearOnEditValue) {
this.$_checkClearOnEdit()
}
this.oldInputValue = this.inputValue
},
/**
* 执行验证, 如果错误则设置ng-invalid, 正确则设置ng-valid
* @private
* */
$_verification () {
// 只有开启才检查
if (!this.checkValue) return
this.isValid = this.$_getVerifyResult(this.inputValue, this.type)
if (this.isValid) {
/**
* @event component:Input#onValid
* @description 验证通过, 只在check开启或者有regex时判断
* @property {*} value - 当前检查的value
* @property {string} type - 当前检查的value的类型
*/
this.$emit('onValid', this.inputValue, this.type)
if (this.itemComponent) {
this.itemComponent.inputClass['ng-valid'] = true
this.itemComponent.inputClass['ng-invalid'] = false
}
} else {
/**
* @event component:Input#onInvalid
* @description 验证失败, 只在check开启或者有regex时判断
* @property {*} value - 当前检查的value
* @property {string} type - 当前检查的value的类型
*/
this.$emit('onInvalid', this.inputValue, this.type)
if (this.itemComponent) {
this.itemComponent.inputClass['ng-valid'] = false
this.itemComponent.inputClass['ng-invalid'] = true
}
}
},
/**
* 获取验证结果
* @param {*} value - 待验证的值
* @param {String} type - 待验证的值的类型
* @return Boolean
* @private
* */
$_getVerifyResult (value, type = 'text') {
if (!isPresent(value)) {
// '当前没有值, 验证跳过, 返回 false!'
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)) {
// '未找到匹配type:' + type + '的regex, 验证跳过, 返回 false!'
return false
}
// 如果是函数则执行判断
if (isFunction(_regex)) {
return _regex(value)
}
// 判断是不是正则
if (isRegexp(_regex)) {
return _regex.test(value)
}
// 'regex:' + _regex + '不是正则/函数, 验证跳过, 返回 false!'
return false
},
/**
* 当该组件被点击的时候触发, 扩大focus触发范围
* @private
*/
$_clickToFocus () {
this.setFocus()
},
/**
* 监听并发送blur事件
* @private
*/
$_inputBlurred () {
// debug: clearInput会在onBlur之后,造成blur后点击clearInput失效, 故需要延迟blur
window.setTimeout(() => {
if (this.shouldBlur) {
// 向父组件Item添加标记
this.$_setItemHasFocusClass(false)
/**
* @event component:Input#onBlur
* @description blur事件
*/
this.$emit('onBlur')
// 如果是clearOnEdit模式, blur时还有值的情况下,定一个flag
if (this.clearOnEditValue && this.hasValue) {
this.didBlurAfterEdit = true
}
// 验证输入结果
this.$_verification()
} else {
this.shouldBlur = true
}
}, 16 * 2)
},
/**
* 监听并发送focus事件
* @private
*/
$_inputFocused () {
// 向父组件Item添加标记
this.$_setItemHasFocusClass(true)
this.setFocus()
/**
* @event component:Input#onFocus
* @description focus事件
*/
this.$emit('onFocus')
if (this.itemComponent) {
this.itemComponent.inputClass['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()
},
$_initDebounce () {
if (this.debounce > 0) {
return debounce(function () {
this.$_emitChange()
}, 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)
},
/**
* @private
* */
$_checkClearOnEdit () {
if (!this.clearOnEditValue) {
return
}
// clearOnEdit模式激活,并且input有值
if (this.didBlurAfterEdit && this.hasValue) {
this.$_inputChanged()
}
// 重置标记
this.didBlurAfterEdit = false
},
/**
* 点击清除输入项
* @private
* */
$_clearTextInput () {
this.inputValue = ''
this.$_inputChanged()
this.shouldBlur = false
this.setFocus()
this.$_setItemHasFocusClass(true)
},
/**
* 设置父组件Item被点中时的class
* @private
*/
$_setItemHasFocusClass (isFocus) {
if (this.itemComponent) {
this.itemComponent.inputClass['input-has-focus'] = isFocus
}
this.inputClass['input-has-focus'] = isFocus
},
/**
* 设置父组件Item有值时的class
* @private
*/
$_setItemHasValueClass () {
if (this.itemComponent) {
this.itemComponent.inputClass['input-has-value'] = !!this.hasValue
}
this.inputClass['input-has-value'] = !!this.hasValue
}
},
created () {
// 默认情况下, 如果password有值, 则点击执行清空
if (this.type === 'password') {
this.clearOnEditValue = true
this.typeValue = 'password'
}
// 根据 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.inputClass['item-input'] = true
this.itemComponent.inputClass['show-focus-highlight'] = this.showFocusHighlight
this.itemComponent.inputClass['show-valid-highlight'] = this.showValidHighlight
this.itemComponent.inputClass['show-invalid-highlight'] = this.showInvalidHighlight
}
// 初始化时,判断是否有value
this.$_setItemHasValueClass()
// 手动focus
if (this.autofocus) {
window.setTimeout(() => {
this.setFocus()
}, 16 * 3)
}
}
}
</script>