前言:
在实际开发过程中,输入框输入正确且合理的数字是一个比较头疼的事情,我见过最多的解决方案是输入完成后,当用户点击提交提示用户输入有误,这样的处理方式对于用户来说是不合理的。在以前的开发过程中,为了缩短开发周期我也是这样做的,直到有一次和朋友聊天,他说他需求里需要一个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>
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))
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))
}
总结:
在开发此类组件时,我们需要不断观察原生输入框的优缺点,在写组件过程中,才可以完善原生输入框的不足,继承原生输入框的优点
未上过生产,如出现问题,请私信联系
转发请附原文地址