【JQuery】input format 输入框内容格式化

1. 背景

先看一张图


类似这样的功能相信绝大部分人都遇到过,用vue之类响应式框架来搞很简单,但是老项目基于JQuery的就似乎没看到过什么现成的组件是可以拿来就用的,所以只能自己搞一个

2. 思路

类似Excel的设置单元格格式,这里就是设置输入框格式

  1. 响应焦点移入/移出事件
  2. 用起来要方便、直观
  3. 扩展要简单

3. 编码

3.1. 响应焦点移入/移出事件

要支持所有input,包括动态产生的,所以要使用on文档)来绑定

$(document).on("blur", ":input", e => {
    const jq = $(e.target);
    // 处理格式化逻辑
});
$(document).on("focus", ":input", e => {
    const jq = $(e.target);
    // 处理反格式化逻辑
});

3.2. 用起来要方便、直观

使用input自定义属性来定义格式,格式化支持多种规则顺序执行,反格式化支持单一规则
格式化:对应format属性和blur事件
反格式化:对应unformat属性和focus事件

<!-- 格式化:去空格 -->
<input type="text" format="trim">
<!-- 格式化:去空格 → 转数字 → 保留4位小数 → 加千分位符号 → 转为百分比 -->
<!-- 反格式化:百分比转数字 -->
<input type="text" format="trim|number|float:4|thous|percent" unformat="percent">

3.3. 扩展要简单

使用$.formats来定义所有格式化规则,$.unformats来定义所有反格式化规则

$.extend($.formats, {
    /** 
     * 格式化规则:去空格
     * @param {JQuery} jquery对象
     * @param {String|null} 表示input输入的值或经过其他格式化规则处理后的值
     * @param {String} 表示格式化规则参数 
     * @returns {String|undefined} 表示格式化之后的值, 如返回 undefined 表示无法处理
     */
    trim(jq, value, args) { return value == null ? value : value.trim() },
    /** 
     * 格式化规则:保留小数位
     * @param {JQuery} jquery对象
     * @param {String|null} 表示input输入的值或经过其他格式化规则处理后的值
     * @param {String} 表示保留几位小数, 默认2 <input format='float:2' >
     * @returns {String|undefined} 表示格式化之后的值, 如返回 undefined 表示无法处理
     */
    float(jq, value, args) { isFinite(value) && Number(value).toFixed(args == "" ?  2 : args) },
    number(){...},
    thous(){...},
    percent(){...},
    ['.']: "float",  // 别名规则,方便调用
    [',']: "thous",
    ['%']: "percent",
});

别名规则可以用别名调用,如:

<input type="text" format="trim|number|float:4|thous|percent" unformat="percent" />
<input type="text" format="trim|number|.:4|,|%" unformat="%" />

3.4. 封装JQuery插件函数

function handler(functions, text) {
    const colon = text.indexOf(":");
    const arg = colon < 0 ? "" : text.substring(colon + 1);
    const name = colon < 0 ? text : text.substring(0, colon);
    let func = functions[name];
    while (func instanceof Function === false) {
        if (func == null) {
            return (_, value) => value;
        }
        func = functions[func];
    }
    return (jq, value) => {
        try {
            var ret = func.apply(functions, [jq, value, arg]);
            if (ret !== undefined) {
                return ret;
            }
            return value;
        } catch (e) {
            console.error("format/unformat [" + name + "] error:", e);
            return value;
        }
    }
}
$.fn.extend({
    format() {
        if (!this.is("[format]")) {
            return this.val();
        }
        const formats = (this.attr("format") || "").split("|");
        let value = this.val();
        for (var i = 0; i < formats.length; i++) {
            value = handler($.formats, formats[i])(this, value);
        }
        return value;
    },
    unformat() {
        const unformat = this.attr("unformat");
        if (!unformat) {
            return this.val();
        }
        return handler($.unformats, unformat)(this, this.val());
    },
});

3.5. 完善各种规则

4. 完整代码

    (function () {
            /**
             * 字符串转数字
             * @para
             */
            function parseNumber(str) {
                if (str == null || str === "") {
                    return null;
                }
                if (typeof str === "number" && !isFinite(str)) {
                    return str;
                }
                str = str.toString().trim().replace(/([, _](?=\d{3}))|(^[^\d+-]+\s?)|((\s?[\D]+$))/g, "");
                if (!/^[+-]?[0-9]+(\.([0-9])*)?(e[+-]\d+)?$/.test(str)) {
                    return null;
                }
                var number = parseFloat(str);
                if (isNaN(number) || !isFinite(number)) {
                    return null;
                }
                return number;
            }

            /**
            * 移动小数点
            * @param {Number} number 数字
            * @param {Number} move 小数点移动位数
            */
            function movePoint(number, move) {

                move = parseInt(move);
                if (isNaN(move) || move < -20 || move > 20) {
                    throw Error("move不能大于20或小于-20")
                }

                var str = overflowString(number, 20);
                var point = str.indexOf(".");
                var arr = str.split("");
                arr.splice(point, 1);
                arr.splice(point + move, 0, ".");
                return parseNumber(arr.join(""));
            }

            /**
             * 字符串前后补0,方便后续操作
             * @param {Number} number 需要补0的数字
             * @param {Number|String|null} zeroLength 前后补0的个数
             */
            function overflowString(number, zeroLength) {
                zeroLength = parseNumber(zeroLength) || 10;
                if (zeroLength < 0) {
                    zeroLength = 10;
                }
                var symbol = number < 0 ? '-' : "";
                number = Math.abs(number);
                // 将20位以内的科学计数法数字转为纯数字
                var str = number.toLocaleString("zh-CN", { maximumFractionDigits: 20 }).replace(/[,]/g, "");
                var zero = new Array(zeroLength).fill("0").join("");
                var point = str.indexOf(".") < 0 ? "." : ""; // 添加小数点
                return symbol + zero + str + point + zero;
            }

            if ($.formats == null) {
                $.formats = {};
            }

            $.extend($.formats, {
                trim(_, value, args) {
                    if (!value) {
                        return value;
                    }
                    switch ((args || "all").trim().toLowerCase()) {
                        case "right":
                        case "r":
                            return value.trimRight();
                        case "left":
                        case "l":
                            return value.trimLeft();
                        default:
                            return value.trim();
                    }
                },
                number(ele, value, def) {
                    const number = parseNumber(value);
                    if (number == null) {
                        return ele[0].lastNumber || def || "";
                    }
                    ele[0].lastNumber = number;
                    return number;
                },
                float(_, value, digits) {
                    const number = parseNumber(value);
                    if (number != null) {
                        return number.toFixed(digits);
                    }
                },
                money(_, value, space) {
                    space = parseNumber(space);
                    if (space > 0) {
                        return "$" + new Array(space + 1).join(" ") + value;
                    }
                    return "$" + value;
                },
                thous(_, value, flag) {
                    const number = parseNumber(value);
                    if (number != null) {
                        const digits = value.toString().length - value.toString().indexOf(".") - 1;
                        const ret = number.toLocaleString("zh-CN", { maximumFractionDigits: 20, minimumFractionDigits: digits });
                        if (flag) {
                            return ret.replace(',', flag);
                        }
                    }
                },
                percent(jq, value) {
                    const number = parseNumber(value);
                    if (number != null) {
                        return jq.val().endsWith("%") ? number + "%" : movePoint(number, 2) + "%";
                    }
                },
                milli(_, value) {
                    const number = parseNumber(value);
                    if (number != null) {
                        return movePoint(number, 3) + "‰";
                    }
                },
                ['$']: "money",
                [',']: "thous",
                ['%']: "percent",
                ['‰']: "milli",
            });

            if ($.unformats == null) {
                $.unformats = {};
            }

            $.extend($.unformats, {
                number(_, value, arg) {
                    const number = parseNumber(value);
                    if (number == null) {
                        return (arg + "") === "true" ? null : str;
                    }
                    return number;
                },
                percent(_, value, arg) {
                    const number = parseNumber(value);
                    if (number == null) {
                        return value;
                    }
                    return value.endsWith("%") ? movePoint(number, -2) : number;
                },
                milli(_, value, arg) {
                    const number = parseNumber(value);
                    if (number == null) {
                        return value;
                    }
                    return value.endsWith("‰") ? movePoint(number, -3) : number;
                },
                ['%']: "percent",
                ['‰']: "milli",
            });


            if ($.fn.format) {
                return;
            }

            function handler(functions, text) {
                const colon = text.indexOf(":");
                const arg = colon < 0 ? "" : text.substring(colon + 1);
                const name = colon < 0 ? text : text.substring(0, colon);
                let func = functions[name];
                while (func instanceof Function === false) {
                    if (func == null) {
                        return (_, value) => value;
                    }
                    func = functions[func];
                }
                return (jq, value) => {
                    try {
                        var ret = func.apply(functions, [jq, value, arg]);
                        if (ret !== undefined) {
                            return ret;
                        }
                        return value;
                    } catch (e) {
                        console.error("format/unformat [" + name + "] error:", e);
                        return value;
                    }
                }
            }

            $.fn.extend({
                format() {
                    if (!this.is("[format]")) {
                        return this.val();
                    }
                    const formats = (this.attr("format") || "").split("|");
                    let value = this.val();
                    for (var i = 0; i < formats.length; i++) {
                        value = handler($.formats, formats[i])(this, value);
                    }
                    return value;
                },
                unformat() {
                    const unformat = this.attr("unformat");
                    if (!unformat) {
                        return this.val();
                    }
                    return handler($.unformats, unformat)(this, this.val());
                },
            });


            $(document).on("blur", ":input[format]", e => {
                const jq = $(e.target);
                const value = jq.format();
                if (value != jq.val()) {
                    jq.val(value);
                }
            });

            $(document).on("focus", ":input[unformat]", e => {
                const jq = $(e.target);
                const value = jq.unformat();
                if (value != jq.val()) {
                    jq.val(value);
                }
            });
        })();
   

5. demo

JQuery input format demo源码预览- JSRUN

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容