背景
前段时间在项目中遇到一个需求,就是对输入的金额用千分符分隔。我当时感觉这个功能挺应该简单的,然而……在开发的过程中还是遇到了一些问题。
初版代码
<input type="text" id="input">
const inputDom = document.getElementById('input');
const separator = ',';
const reg = new RegExp(separator, 'g');
// 监听input事件
inputDom.addEventListener('input', function (e) {
const value = inputDom.value;
if (value[value.length - 1] === '.') {
return;
}
inputDom.value = format(value.replace(reg, ''));
});
// 格式化输入内容
function format(string) {
let amount = '';
// 小数点左边
let leftOfPoint = replaceValue;
// 小数点右边
let rightOfPoint = '';
if (replaceValue.includes('.')) {
[leftOfPoint, rightOfPoint] = replaceValue.split('.');
}
const length = leftOfPoint.length;
// 余数
const n = length % 3;
// 我这里使用循环遍历字符串添加千分符,大家也可以使用正则表达式
if (length > 3) {
for (let i = 0; i < length; i++) {
amount += leftOfPoint[i];
// 判断条件说明:
// 如果leftOfPoint = '1234578',则n = 2
// 当i = 1,i + 1 = 2 = n,amount = 12,345678
// 当i = 4,i + 1 = 5 > n,(i + 1 - n) % 3 = 0,amount = 12,345,678
if (i + 1 === n || i + 1 > n && i + 1 < length && (i + 1 - n) % 3 == 0) {
amount += separator;
}
}
} else {
amount = leftOfPoint;
}
// 如果有小数
if (rightOfPoint) {
amount += '.' + rightOfPoint;
}
return amount;
}
复制代码
遇到的问题
正常输入没有问题,光标一直在末尾显示。当连续插入或删除字符时,光标的位置就会在插入或删除一个字符后,移动到末尾,导致数据错误。举个例子:
当我输入123456.90后,想在6后面插入字符7和8时,会在插入字符7后,光标移动至末尾,导致字符8被插入到末尾,此时输入框的值是1234567.908,而不是我们希望的12345678.90。如下图所示:
同理,当我输入12345678.90后,光标移动至8后面,想删除字符8和7时,会在删除字符8后,光标移动至末尾,导致末尾字符0被删除,此时输入框的值是1234567.9,而不是我们希望的123456.90。如下图所示:
解决思路
正常情况下,输入框在输入、插入或者删除字符时,光标的位置是正确的,但对输入框重新赋值时(HTMLInputElement.value = ‘xxx’),就会导致光标的相对位置丢失,此时浏览器则会默认将光标显示在末尾。
那么问题的核心就在于如何记住光标的位置。
我的想法是,每一次操作(输入、插入、删除)时,记住光标前一位的字符,如果光标前一位刚好是分隔符,就顺延到下一位。但是数字只有0-9这十个啊,有重复的怎么办呢?对于重复的数,我们需要额外记录一下,光标在第几个重复的数后面不就行了嘛。结合到实际生活中,这不就和排队类似嘛!排队打疫苗的时候只需要记住你前面一个人是谁就行了,至于其他的不重要,如果队伍中程序员比较多,大家都穿格子衫,你就记住你排在第几个穿格子衫的人后面就行了。
设置光标位置主要涉及到HTMLInputElement.selectionStart、HTMLInputElement.selectionEnd、HTMLInputElement.setSelectionRange()。
input输入事件对象涉及到InputEvent、InputEvent.data、InputEvent.inputType。
实现
纸上得来终觉浅,绝知此事要躬行
话不多说,直接上代码
<input type="text" id="input">
const separator = ',';
const reg = new RegExp(separator, 'g');
let lastValue = '';
// 监听input事件
inputDom.addEventListener('input', function (e) {
let value = inputDom.value;
// 直接输入小数点
if (value === '.') {
inputDom.value = '';
return;
}
let cursorPosition = inputDom.selectionStart;
// 如果输入2个小数点或输入分隔符
if (value.indexOf('.') !== value.lastIndexOf('.') || e.data === separator) {
let leftOfCursor = value.substring(0, cursorPosition - 1);
let rightOfCursor = value.substring(cursorPosition);
inputDom.value = leftOfCursor + rightOfCursor;
inputDom.selectionStart = cursorPosition - 1;
inputDom.selectionEnd = cursorPosition - 1;
return;
} else if (value[value.length - 1] === '.') { // 末尾输入小数点
return;
}
let formatValue = format(value.replace(reg, ''));
// 由于delete键是向后删除的,所以需要判断被删除的字符是不是分隔符,如果是,则光标向后移动一位
if (e.inputType === 'deleteContentForward' && getDeletedString(lastValue, value) === separator) {
cursorPosition += 1;
} else {
cursorPosition = getCursorPosition(formatValue, value);
}
inputDom.value = formatValue;
inputDom.selectionStart = cursorPosition;
inputDom.selectionEnd = cursorPosition;
lastValue = formatValue;
});
// format函数同上
function format(string){
// ...
}
// 获取被删除的字符串
function getDeletedString(lastString, string) {
let deletedString = '', count = 0;
for (let i = 0; i < lastString.length; i++) {
if (lastString[i] === string[count]) {
if (deletedString) {
break;
}
count++;
} else {
deletedString += lastString[i];
}
}
return deletedString;
}
// 获取光标位置
function getCursorPosition(formatString, string) {
let cursorPosition = inputDom.selectionStart;
let index = cursorPosition - 1;
// 光标前一个字符如果是分隔符
if (string[index] === separator) {
index -= 1;
}
// 计算光标前一个字符重复了几次
let count = 0;
for (let i = 0; i < index; i++) {
if (string[i] === string[index]) {
count++;
}
}
// 计算光标位置
let n = 0;
for (let j = 0; j < formatString.length; j++) {
if (formatString[j] === string[index]) {
if (n === count) {
cursorPosition = j + 1;
break;
}
n++;
}
}
return cursorPosition;
}
复制代码
以上代码对于输入字符、插入字符、删除单个字符、选中多个字符删除、向前删除、向后删除都能很好的保证光标位置不出错。如下图所示:
在线demo jsdemo.codeman.top/html/inputF…
写在最后
给大家推荐一个生成gif图的软件GifCam,该软件大小只有1.58MB,非常小巧,简单易用。文中的gif图全部由此软件制作。