js input千分符分隔金额

吐槽君 分类:javascript

背景

前段时间在项目中遇到一个需求,就是对输入的金额用千分符分隔。我当时感觉这个功能挺应该简单的,然而……在开发的过程中还是遇到了一些问题。

初版代码

<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。如下图所示:

GIF 2021-06-03 10-58-03.gif

同理,当我输入12345678.90后,光标移动至8后面,想删除字符8和7时,会在删除字符8后,光标移动至末尾,导致末尾字符0被删除,此时输入框的值是1234567.9,而不是我们希望的123456.90。如下图所示:

GIF 2021-06-03 11-04-31.gif

解决思路

正常情况下,输入框在输入、插入或者删除字符时,光标的位置是正确的,但对输入框重新赋值时(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;
}
 

以上代码对于输入字符、插入字符、删除单个字符、选中多个字符删除、向前删除、向后删除都能很好的保证光标位置不出错。如下图所示:

GIF 2021-06-03 15-33-57.gif

在线demo jsdemo.codeman.top/html/inputF…

写在最后

给大家推荐一个生成gif图的软件GifCam,该软件大小只有1.58MB,非常小巧,简单易用。文中的gif图全部由此软件制作。

网站出售中,有意者加微信:javadudu

回复

我来回复
  • 暂无回复内容