1. 背景
先看一张图
类似这样的功能相信绝大部分人都遇到过,用vue
之类响应式框架来搞很简单,但是老项目基于JQuery的就似乎没看到过什么现成的组件是可以拿来就用的,所以只能自己搞一个
2. 思路
类似Excel的设置单元格格式,这里就是设置输入框格式
- 响应焦点移入/移出事件
- 用起来要方便、直观
- 扩展要简单
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);
}
});
})();