text-input-guard 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -129
- package/dist/cjs/text-input-guard.cjs +126 -87
- package/dist/cjs/text-input-guard.min.cjs +6 -0
- package/dist/esm/text-input-guard.js +126 -87
- package/dist/esm/text-input-guard.min.js +6 -0
- package/dist/umd/text-input-guard.js +126 -87
- package/dist/umd/text-input-guard.min.js +1 -1
- package/package.json +16 -6
|
@@ -200,7 +200,7 @@ class InputGuard {
|
|
|
200
200
|
|
|
201
201
|
const kind = detectKind(element);
|
|
202
202
|
if (!kind) {
|
|
203
|
-
throw new TypeError("[
|
|
203
|
+
throw new TypeError("[text-input-guard] attach() expects an <input> or <textarea> element.");
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
/**
|
|
@@ -432,7 +432,7 @@ class InputGuard {
|
|
|
432
432
|
}
|
|
433
433
|
|
|
434
434
|
if (this.kind !== "input") {
|
|
435
|
-
warnLog('[
|
|
435
|
+
warnLog('[text-input-guard] separateValue.mode="swap" is not supported for <textarea>. ignored.', this.warn);
|
|
436
436
|
return;
|
|
437
437
|
}
|
|
438
438
|
|
|
@@ -593,7 +593,7 @@ class InputGuard {
|
|
|
593
593
|
|
|
594
594
|
if (!supports) {
|
|
595
595
|
warnLog(
|
|
596
|
-
`[
|
|
596
|
+
`[text-input-guard] Rule "${rule.name}" is not supported for <${this.kind}>. skipped.`,
|
|
597
597
|
this.warn
|
|
598
598
|
);
|
|
599
599
|
continue;
|
|
@@ -678,9 +678,7 @@ class InputGuard {
|
|
|
678
678
|
// 連鎖防止(次の処理に持ち越さない)
|
|
679
679
|
this.revertRequest = null;
|
|
680
680
|
|
|
681
|
-
if (this.warn)
|
|
682
|
-
console.log(`[jp-input-guard] reverted: ${req.reason}`, req.detail);
|
|
683
|
-
}
|
|
681
|
+
if (this.warn) ;
|
|
684
682
|
}
|
|
685
683
|
|
|
686
684
|
/**
|
|
@@ -824,7 +822,7 @@ class InputGuard {
|
|
|
824
822
|
* @returns {void}
|
|
825
823
|
*/
|
|
826
824
|
onCompositionStart() {
|
|
827
|
-
console.log("[
|
|
825
|
+
// console.log("[text-input-guard] compositionstart");
|
|
828
826
|
this.composing = true;
|
|
829
827
|
}
|
|
830
828
|
|
|
@@ -834,7 +832,7 @@ class InputGuard {
|
|
|
834
832
|
* @returns {void}
|
|
835
833
|
*/
|
|
836
834
|
onCompositionEnd() {
|
|
837
|
-
console.log("[
|
|
835
|
+
// console.log("[text-input-guard] compositionend");
|
|
838
836
|
this.composing = false;
|
|
839
837
|
|
|
840
838
|
// compositionend後に input が来ない環境向けのフォールバック
|
|
@@ -854,7 +852,7 @@ class InputGuard {
|
|
|
854
852
|
* @returns {void}
|
|
855
853
|
*/
|
|
856
854
|
onInput() {
|
|
857
|
-
console.log("[
|
|
855
|
+
// console.log("[text-input-guard] input");
|
|
858
856
|
// compositionend後に input が来た場合、フォールバックを無効化
|
|
859
857
|
this.pendingCompositionCommit = false;
|
|
860
858
|
this.evaluateInput();
|
|
@@ -865,7 +863,7 @@ class InputGuard {
|
|
|
865
863
|
* @returns {void}
|
|
866
864
|
*/
|
|
867
865
|
onBlur() {
|
|
868
|
-
console.log("[
|
|
866
|
+
// console.log("[text-input-guard] blur");
|
|
869
867
|
this.evaluateCommit();
|
|
870
868
|
}
|
|
871
869
|
|
|
@@ -1114,6 +1112,63 @@ class InputGuard {
|
|
|
1114
1112
|
* The MIT license https://opensource.org/licenses/MIT
|
|
1115
1113
|
*/
|
|
1116
1114
|
|
|
1115
|
+
/**
|
|
1116
|
+
* datasetのboolean値を解釈する
|
|
1117
|
+
* - 未指定なら undefined
|
|
1118
|
+
* - "" / "true" / "1" / "yes" / "on" は true
|
|
1119
|
+
* - "false" / "0" / "no" / "off" は false
|
|
1120
|
+
* @param {string|undefined} v
|
|
1121
|
+
* @returns {boolean|undefined}
|
|
1122
|
+
*/
|
|
1123
|
+
function parseDatasetBool(v) {
|
|
1124
|
+
if (v == null) { return; }
|
|
1125
|
+
const s = String(v).trim().toLowerCase();
|
|
1126
|
+
if (s === "" || s === "true" || s === "1" || s === "yes" || s === "on") { return true; }
|
|
1127
|
+
if (s === "false" || s === "0" || s === "no" || s === "off") { return false; }
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* datasetのnumber値を解釈する(整数想定)
|
|
1133
|
+
* - 未指定/空なら undefined
|
|
1134
|
+
* - 数値でなければ undefined
|
|
1135
|
+
* @param {string|undefined} v
|
|
1136
|
+
* @returns {number|undefined}
|
|
1137
|
+
*/
|
|
1138
|
+
function parseDatasetNumber(v) {
|
|
1139
|
+
if (v == null) { return; }
|
|
1140
|
+
const s = String(v).trim();
|
|
1141
|
+
if (s === "") { return; }
|
|
1142
|
+
const n = Number(s);
|
|
1143
|
+
return Number.isFinite(n) ? n : undefined;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* enumを解釈する(未指定なら undefined)
|
|
1148
|
+
* @template {string} T
|
|
1149
|
+
* @param {string|undefined} v
|
|
1150
|
+
* @param {readonly T[]} allowed
|
|
1151
|
+
* @returns {T|undefined}
|
|
1152
|
+
*/
|
|
1153
|
+
function parseDatasetEnum(v, allowed) {
|
|
1154
|
+
if (v == null) { return; }
|
|
1155
|
+
const s = String(v).trim();
|
|
1156
|
+
if (s === "") { return; }
|
|
1157
|
+
// 大文字小文字を区別したいならここを変える(今は厳密一致)
|
|
1158
|
+
return /** @type {T|undefined} */ (allowed.includes(/** @type {any} */ (s)) ? s : undefined);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* The script is part of TextInputGuard.
|
|
1163
|
+
*
|
|
1164
|
+
* AUTHOR:
|
|
1165
|
+
* natade-jp (https://github.com/natade-jp)
|
|
1166
|
+
*
|
|
1167
|
+
* LICENSE:
|
|
1168
|
+
* The MIT license https://opensource.org/licenses/MIT
|
|
1169
|
+
*/
|
|
1170
|
+
|
|
1171
|
+
|
|
1117
1172
|
/**
|
|
1118
1173
|
* @typedef {GuardGroup} GuardGroup
|
|
1119
1174
|
* @typedef {Guard} Guard
|
|
@@ -1128,19 +1183,6 @@ class InputGuard {
|
|
|
1128
1183
|
* @property {(dataset: DOMStringMap, el: HTMLInputElement|HTMLTextAreaElement) => Rule|null} fromDataset
|
|
1129
1184
|
*/
|
|
1130
1185
|
|
|
1131
|
-
/**
|
|
1132
|
-
* Boolean系のdata値を解釈する(未指定なら undefined を返す)
|
|
1133
|
-
* @param {string|undefined} v
|
|
1134
|
-
* @returns {boolean|undefined}
|
|
1135
|
-
*/
|
|
1136
|
-
function parseBool(v) {
|
|
1137
|
-
if (v == null) { return; }
|
|
1138
|
-
const s = String(v).trim().toLowerCase();
|
|
1139
|
-
if (s === "" || s === "true" || s === "1" || s === "yes" || s === "on") { return true; }
|
|
1140
|
-
if (s === "false" || s === "0" || s === "no" || s === "off") { return false; }
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
1186
|
/**
|
|
1145
1187
|
* separate mode を解釈する(未指定は "auto")
|
|
1146
1188
|
* @param {string|undefined} v
|
|
@@ -1244,7 +1286,7 @@ class InputGuardAutoAttach {
|
|
|
1244
1286
|
const options = {};
|
|
1245
1287
|
|
|
1246
1288
|
// warn / invalidClass
|
|
1247
|
-
const warn =
|
|
1289
|
+
const warn = parseDatasetBool(ds.tigWarn);
|
|
1248
1290
|
if (warn != null) { options.warn = warn; }
|
|
1249
1291
|
|
|
1250
1292
|
if (ds.tigInvalidClass != null && String(ds.tigInvalidClass).trim() !== "") {
|
|
@@ -1264,7 +1306,7 @@ class InputGuardAutoAttach {
|
|
|
1264
1306
|
} catch (e) {
|
|
1265
1307
|
const w = options.warn ?? true;
|
|
1266
1308
|
if (w) {
|
|
1267
|
-
console.warn(`[
|
|
1309
|
+
console.warn(`[text-input-guard] autoAttach: rule "${fac.name}" fromDataset() threw an error.`, e);
|
|
1268
1310
|
}
|
|
1269
1311
|
}
|
|
1270
1312
|
}
|
|
@@ -1290,62 +1332,6 @@ class InputGuardAutoAttach {
|
|
|
1290
1332
|
}
|
|
1291
1333
|
}
|
|
1292
1334
|
|
|
1293
|
-
/**
|
|
1294
|
-
* The script is part of TextInputGuard.
|
|
1295
|
-
*
|
|
1296
|
-
* AUTHOR:
|
|
1297
|
-
* natade-jp (https://github.com/natade-jp)
|
|
1298
|
-
*
|
|
1299
|
-
* LICENSE:
|
|
1300
|
-
* The MIT license https://opensource.org/licenses/MIT
|
|
1301
|
-
*/
|
|
1302
|
-
|
|
1303
|
-
/**
|
|
1304
|
-
* datasetのboolean値を解釈する
|
|
1305
|
-
* - 未指定なら undefined
|
|
1306
|
-
* - "" / "true" / "1" / "yes" / "on" は true
|
|
1307
|
-
* - "false" / "0" / "no" / "off" は false
|
|
1308
|
-
* @param {string|undefined} v
|
|
1309
|
-
* @returns {boolean|undefined}
|
|
1310
|
-
*/
|
|
1311
|
-
function parseDatasetBool(v) {
|
|
1312
|
-
if (v == null) { return; }
|
|
1313
|
-
const s = String(v).trim().toLowerCase();
|
|
1314
|
-
if (s === "" || s === "true" || s === "1" || s === "yes" || s === "on") { return true; }
|
|
1315
|
-
if (s === "false" || s === "0" || s === "no" || s === "off") { return false; }
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
/**
|
|
1320
|
-
* datasetのnumber値を解釈する(整数想定)
|
|
1321
|
-
* - 未指定/空なら undefined
|
|
1322
|
-
* - 数値でなければ undefined
|
|
1323
|
-
* @param {string|undefined} v
|
|
1324
|
-
* @returns {number|undefined}
|
|
1325
|
-
*/
|
|
1326
|
-
function parseDatasetNumber(v) {
|
|
1327
|
-
if (v == null) { return; }
|
|
1328
|
-
const s = String(v).trim();
|
|
1329
|
-
if (s === "") { return; }
|
|
1330
|
-
const n = Number(s);
|
|
1331
|
-
return Number.isFinite(n) ? n : undefined;
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
/**
|
|
1335
|
-
* enumを解釈する(未指定なら undefined)
|
|
1336
|
-
* @template {string} T
|
|
1337
|
-
* @param {string|undefined} v
|
|
1338
|
-
* @param {readonly T[]} allowed
|
|
1339
|
-
* @returns {T|undefined}
|
|
1340
|
-
*/
|
|
1341
|
-
function parseDatasetEnum(v, allowed) {
|
|
1342
|
-
if (v == null) { return; }
|
|
1343
|
-
const s = String(v).trim();
|
|
1344
|
-
if (s === "") { return; }
|
|
1345
|
-
// 大文字小文字を区別したいならここを変える(今は厳密一致)
|
|
1346
|
-
return /** @type {T|undefined} */ (allowed.includes(/** @type {any} */ (s)) ? s : undefined);
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
1335
|
/**
|
|
1350
1336
|
* The script is part of TextInputGuard.
|
|
1351
1337
|
*
|
|
@@ -1363,6 +1349,7 @@ function parseDatasetEnum(v, allowed) {
|
|
|
1363
1349
|
* @property {boolean} [allowFullWidth=true] - 全角数字/記号を許可して半角へ正規化する
|
|
1364
1350
|
* @property {boolean} [allowMinus=false] - マイナス記号を許可する(先頭のみ)
|
|
1365
1351
|
* @property {boolean} [allowDecimal=false] - 小数点を許可する(1つだけ)
|
|
1352
|
+
* @property {boolean} [allowEmpty=true] - 空文字を許可するか
|
|
1366
1353
|
*/
|
|
1367
1354
|
|
|
1368
1355
|
/**
|
|
@@ -1378,7 +1365,8 @@ function numeric(options = {}) {
|
|
|
1378
1365
|
const opt = {
|
|
1379
1366
|
allowFullWidth: options.allowFullWidth ?? true,
|
|
1380
1367
|
allowMinus: options.allowMinus ?? false,
|
|
1381
|
-
allowDecimal: options.allowDecimal ?? false
|
|
1368
|
+
allowDecimal: options.allowDecimal ?? false,
|
|
1369
|
+
allowEmpty: options.allowEmpty ?? true
|
|
1382
1370
|
};
|
|
1383
1371
|
|
|
1384
1372
|
/** @type {Set<string>} */
|
|
@@ -1536,9 +1524,14 @@ function numeric(options = {}) {
|
|
|
1536
1524
|
fix(value) {
|
|
1537
1525
|
let v = String(value);
|
|
1538
1526
|
|
|
1527
|
+
// 空文字の扱い
|
|
1528
|
+
if (v === "") {
|
|
1529
|
+
return opt.allowEmpty ? "" : "0";
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1539
1532
|
// 未完成な数値は空にする
|
|
1540
1533
|
if (v === "-" || v === "." || v === "-.") {
|
|
1541
|
-
return "";
|
|
1534
|
+
return opt.allowEmpty ? "" : "0";
|
|
1542
1535
|
}
|
|
1543
1536
|
|
|
1544
1537
|
// "-.1" → "-0.1"
|
|
@@ -1610,6 +1603,7 @@ function numeric(options = {}) {
|
|
|
1610
1603
|
* - data-tig-rules-numeric-allow-full-width -> dataset.tigRulesNumericAllowFullWidth
|
|
1611
1604
|
* - data-tig-rules-numeric-allow-minus -> dataset.tigRulesNumericAllowMinus
|
|
1612
1605
|
* - data-tig-rules-numeric-allow-decimal -> dataset.tigRulesNumericAllowDecimal
|
|
1606
|
+
* - data-tig-rules-numeric-allow-empty -> dataset.tigRulesNumericAllowEmpty
|
|
1613
1607
|
*
|
|
1614
1608
|
* @param {DOMStringMap} dataset
|
|
1615
1609
|
* @param {HTMLInputElement|HTMLTextAreaElement} _el
|
|
@@ -1642,6 +1636,12 @@ numeric.fromDataset = function fromDataset(dataset, _el) {
|
|
|
1642
1636
|
options.allowDecimal = allowDecimal;
|
|
1643
1637
|
}
|
|
1644
1638
|
|
|
1639
|
+
// data-tig-rules-numeric-allow-empty(未指定なら numeric側デフォルト true)
|
|
1640
|
+
const allowEmpty = parseDatasetBool(dataset.tigRulesNumericAllowEmpty);
|
|
1641
|
+
if (allowEmpty != null) {
|
|
1642
|
+
options.allowEmpty = allowEmpty;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
1645
|
return numeric(options);
|
|
1646
1646
|
};
|
|
1647
1647
|
|
|
@@ -1666,6 +1666,7 @@ numeric.fromDataset = function fromDataset(dataset, _el) {
|
|
|
1666
1666
|
* @property {"none"|"truncate"|"round"} [fixFracOnBlur="none"] - blur時の小数部補正
|
|
1667
1667
|
* @property {"none"|"block"} [overflowInputInt="none"] - 入力中:整数部が最大桁を超える入力をブロックする
|
|
1668
1668
|
* @property {"none"|"block"} [overflowInputFrac="none"] - 入力中:小数部が最大桁を超える入力をブロックする
|
|
1669
|
+
* @property {boolean} [forceFracOnBlur=false] - blur時に小数部を必ず表示(frac桁まで0埋め)
|
|
1669
1670
|
*/
|
|
1670
1671
|
|
|
1671
1672
|
/**
|
|
@@ -1806,7 +1807,8 @@ function digits(options = {}) {
|
|
|
1806
1807
|
fixIntOnBlur: options.fixIntOnBlur ?? "none",
|
|
1807
1808
|
fixFracOnBlur: options.fixFracOnBlur ?? "none",
|
|
1808
1809
|
overflowInputInt: options.overflowInputInt ?? "none",
|
|
1809
|
-
overflowInputFrac: options.overflowInputFrac ?? "none"
|
|
1810
|
+
overflowInputFrac: options.overflowInputFrac ?? "none",
|
|
1811
|
+
forceFracOnBlur: options.forceFracOnBlur ?? false
|
|
1810
1812
|
};
|
|
1811
1813
|
|
|
1812
1814
|
return {
|
|
@@ -1923,14 +1925,39 @@ function digits(options = {}) {
|
|
|
1923
1925
|
}
|
|
1924
1926
|
}
|
|
1925
1927
|
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
+
if (opt.forceFracOnBlur && typeof opt.frac === "number" && opt.frac > 0) {
|
|
1929
|
+
const limit = opt.frac;
|
|
1930
|
+
// "." が無いなら作る(12 → 12.00)
|
|
1931
|
+
if (!hasDot) {
|
|
1932
|
+
fracPart = "";
|
|
1933
|
+
}
|
|
1934
|
+
// 足りない分を 0 埋め(12.3 → 12.30 / 12. → 12.00)
|
|
1935
|
+
const f = fracPart ?? "";
|
|
1936
|
+
if (f.length < limit) {
|
|
1937
|
+
fracPart = f + "0".repeat(limit - f.length);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// 組み立て
|
|
1942
|
+
if (typeof opt.frac !== "number") {
|
|
1943
|
+
// frac未指定なら、dot があっても digits は触らず intだけ返す方針(現状維持)
|
|
1928
1944
|
return `${sign}${intPart}`;
|
|
1929
1945
|
}
|
|
1946
|
+
|
|
1930
1947
|
if (opt.frac === 0) {
|
|
1948
|
+
// 小数0桁なら常に整数表示
|
|
1931
1949
|
return `${sign}${intPart}`;
|
|
1932
1950
|
}
|
|
1933
|
-
|
|
1951
|
+
|
|
1952
|
+
// frac 指定あり(1以上)
|
|
1953
|
+
if (hasDot || (opt.forceFracOnBlur && opt.frac > 0)) {
|
|
1954
|
+
// "." が無いけど forceFracOnBlur の場合もここに来る
|
|
1955
|
+
const f = fracPart ?? "";
|
|
1956
|
+
return `${sign}${intPart}.${f}`;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// "." が無くて force もしないなら整数表示
|
|
1960
|
+
return `${sign}${intPart}`;
|
|
1934
1961
|
}
|
|
1935
1962
|
};
|
|
1936
1963
|
}
|
|
@@ -1949,6 +1976,7 @@ function digits(options = {}) {
|
|
|
1949
1976
|
* - data-tig-rules-digits-fix-frac-on-blur -> dataset.tigRulesDigitsFixFracOnBlur
|
|
1950
1977
|
* - data-tig-rules-digits-overflow-input-int -> dataset.tigRulesDigitsOverflowInputInt
|
|
1951
1978
|
* - data-tig-rules-digits-overflow-input-frac -> dataset.tigRulesDigitsOverflowInputFrac
|
|
1979
|
+
* - data-tig-rules-digits-force-frac-on-blur -> dataset.tigRulesDigitsForceFracOnBlur
|
|
1952
1980
|
*
|
|
1953
1981
|
* @param {DOMStringMap} dataset
|
|
1954
1982
|
* @param {HTMLInputElement|HTMLTextAreaElement} _el
|
|
@@ -2011,6 +2039,12 @@ digits.fromDataset = function fromDataset(dataset, _el) {
|
|
|
2011
2039
|
options.overflowInputFrac = ovFrac;
|
|
2012
2040
|
}
|
|
2013
2041
|
|
|
2042
|
+
// forceFracOnBlur
|
|
2043
|
+
const forceFrac = parseDatasetBool(dataset.tigRulesDigitsForceFracOnBlur);
|
|
2044
|
+
if (forceFrac != null) {
|
|
2045
|
+
options.forceFracOnBlur = forceFrac;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2014
2048
|
return digits(options);
|
|
2015
2049
|
};
|
|
2016
2050
|
|
|
@@ -2036,6 +2070,11 @@ function comma() {
|
|
|
2036
2070
|
|
|
2037
2071
|
/**
|
|
2038
2072
|
* 表示整形(確定時のみ)
|
|
2073
|
+
*
|
|
2074
|
+
* 前提:
|
|
2075
|
+
* - numeric / digits 等で正規化済みの数値文字列が渡される
|
|
2076
|
+
* - 整数部・小数部・符号のみを含む(カンマは含まない想定)
|
|
2077
|
+
*
|
|
2039
2078
|
* @param {string} value
|
|
2040
2079
|
* @returns {string}
|
|
2041
2080
|
*/
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* TextInputGuard
|
|
3
|
+
* AUTHOR: natade (https://github.com/natade-jp/)
|
|
4
|
+
* LICENSE: MIT https://opensource.org/licenses/MIT
|
|
5
|
+
*/
|
|
6
|
+
function t(t,e){e&&console.warn(t)}function e(t,e={}){const i=new n(t,e);return i.init(),i.toGuard()}function i(t,i={}){const n=[];for(const r of t)n.push(e(r,i));return{detach:()=>{for(const t of n)t.detach()},isValid:()=>n.every(t=>t.isValid()),getErrors:()=>n.flatMap(t=>t.getErrors()),getGuards:()=>n}}class n{constructor(t,e){this.originalElement=t,this.options=e;const i=(n=t)instanceof HTMLInputElement?"input":n instanceof HTMLTextAreaElement?"textarea":null;var n;if(!i)throw new TypeError("[text-input-guard] attach() expects an <input> or <textarea> element.");this.kind=i,this.warn=e.warn??!0,this.invalidClass=e.invalidClass??"is-invalid",this.rules=Array.isArray(e.rules)?e.rules:[],this.hostElement=t,this.displayElement=t,this.rawElement=null,this.composing=!1,this.errors=[],this.normalizeCharRules=[],this.normalizeStructureRules=[],this.validateRules=[],this.fixRules=[],this.formatRules=[],this.onCompositionStart=this.onCompositionStart.bind(this),this.onCompositionEnd=this.onCompositionEnd.bind(this),this.onInput=this.onInput.bind(this),this.onBlur=this.onBlur.bind(this),this.onFocus=this.onFocus.bind(this),this.onSelectionChange=this.onSelectionChange.bind(this),this.swapState=null,this.pendingCompositionCommit=!1,this.lastAcceptedValue="",this.lastAcceptedSelection={start:null,end:null,direction:null},this.revertRequest=null}init(){this.buildPipeline(),this.applySeparateValue(),this.bindEvents(),this.evaluateInput()}readSelection(t){return{start:t.selectionStart,end:t.selectionEnd,direction:t.selectionDirection}}writeSelection(t,e){if(null!=e.start&&null!=e.end)try{e.direction?t.setSelectionRange(e.start,e.end,e.direction):t.setSelectionRange(e.start,e.end)}catch(t){}}applySeparateValue(){const e=this.options.separateValue?.mode??"auto";if("swap"!==("auto"===e?this.formatRules.length>0?"swap":"off":e))return;if("input"!==this.kind)return void t('[text-input-guard] separateValue.mode="swap" is not supported for <textarea>. ignored.',this.warn);const i=this.originalElement;this.swapState={originalType:i.type,originalId:i.getAttribute("id"),originalName:i.getAttribute("name"),originalClass:i.className,createdDisplay:null},i.type="hidden",i.removeAttribute("id"),i.dataset.tigRole="raw",this.swapState.originalId&&(i.dataset.tigOriginalId=this.swapState.originalId),this.swapState.originalName&&(i.dataset.tigOriginalName=this.swapState.originalName);const n=document.createElement("input");n.type="text",n.dataset.tigRole="display",this.swapState.originalId&&(n.id=this.swapState.originalId),n.removeAttribute("name"),n.className=this.swapState.originalClass,i.className="",n.value=i.value,i.after(n),this.hostElement=i,this.displayElement=n,this.rawElement=i,this.swapState.createdDisplay=n,this.lastAcceptedValue=n.value,this.lastAcceptedSelection=this.readSelection(n)}restoreSeparateValue(){if(!this.swapState)return;const t=this.swapState,e=this.hostElement,i=t.createdDisplay;if(i)try{e.value=e.value||i.value}catch(t){}i&&i.parentNode&&i.parentNode.removeChild(i),e.type=t.originalType,t.originalId?e.setAttribute("id",t.originalId):e.removeAttribute("id"),t.originalName?e.setAttribute("name",t.originalName):e.removeAttribute("name"),e.className=t.originalClass??"",delete e.dataset.tigRole,delete e.dataset.tigOriginalId,delete e.dataset.tigOriginalName,this.hostElement=this.originalElement,this.displayElement=this.originalElement,this.rawElement=null}detach(){this.unbindEvents(),this.restoreSeparateValue(),this.swapState=null}buildPipeline(){this.normalizeCharRules=[],this.normalizeStructureRules=[],this.validateRules=[],this.fixRules=[],this.formatRules=[];for(const e of this.rules){"input"===this.kind&&e.targets.includes("input")||"textarea"===this.kind&&e.targets.includes("textarea")?(e.normalizeChar&&this.normalizeCharRules.push(e),e.normalizeStructure&&this.normalizeStructureRules.push(e),e.validate&&this.validateRules.push(e),e.fix&&this.fixRules.push(e),e.format&&this.formatRules.push(e)):t(`[text-input-guard] Rule "${e.name}" is not supported for <${this.kind}>. skipped.`,this.warn)}}bindEvents(){this.displayElement.addEventListener("compositionstart",this.onCompositionStart),this.displayElement.addEventListener("compositionend",this.onCompositionEnd),this.displayElement.addEventListener("input",this.onInput),this.displayElement.addEventListener("blur",this.onBlur),this.displayElement.addEventListener("focus",this.onFocus),this.displayElement.addEventListener("keyup",this.onSelectionChange),this.displayElement.addEventListener("mouseup",this.onSelectionChange),this.displayElement.addEventListener("select",this.onSelectionChange),this.displayElement.addEventListener("focus",this.onSelectionChange)}unbindEvents(){this.displayElement.removeEventListener("compositionstart",this.onCompositionStart),this.displayElement.removeEventListener("compositionend",this.onCompositionEnd),this.displayElement.removeEventListener("input",this.onInput),this.displayElement.removeEventListener("blur",this.onBlur),this.displayElement.removeEventListener("focus",this.onFocus),this.displayElement.removeEventListener("keyup",this.onSelectionChange),this.displayElement.removeEventListener("mouseup",this.onSelectionChange),this.displayElement.removeEventListener("select",this.onSelectionChange),this.displayElement.removeEventListener("focus",this.onSelectionChange)}revertDisplay(t){const e=this.displayElement;e.value=this.lastAcceptedValue,this.writeSelection(e,this.lastAcceptedSelection),this.syncRaw(this.lastAcceptedValue),this.clearErrors(),this.applyInvalidClass(),this.revertRequest=null,this.warn}createCtx(){return{hostElement:this.hostElement,displayElement:this.displayElement,rawElement:this.rawElement,kind:this.kind,warn:this.warn,invalidClass:this.invalidClass,composing:this.composing,pushError:t=>this.errors.push(t),requestRevert:t=>{this.revertRequest||(this.revertRequest=t)}}}clearErrors(){this.errors=[]}runNormalizeChar(t,e){let i=t;for(const t of this.normalizeCharRules)i=t.normalizeChar?t.normalizeChar(i,e):i;return i}runNormalizeStructure(t,e){let i=t;for(const t of this.normalizeStructureRules)i=t.normalizeStructure?t.normalizeStructure(i,e):i;return i}runValidate(t,e){for(const i of this.validateRules)i.validate&&i.validate(t,e)}runFix(t,e){let i=t;for(const t of this.fixRules)i=t.fix?t.fix(i,e):i;return i}runFormat(t,e){let i=t;for(const t of this.formatRules)i=t.format?t.format(i,e):i;return i}applyInvalidClass(){const t=this.displayElement;this.errors.length>0?t.classList.add(this.invalidClass):t.classList.remove(this.invalidClass)}syncRaw(t){this.rawElement&&(this.rawElement.value=t)}syncDisplay(t){(this.displayElement instanceof HTMLInputElement||this.displayElement instanceof HTMLTextAreaElement)&&(this.displayElement.value=t)}onCompositionStart(){this.composing=!0}onCompositionEnd(){this.composing=!1,this.pendingCompositionCommit=!0,queueMicrotask(()=>{this.pendingCompositionCommit&&(this.pendingCompositionCommit=!1,this.evaluateInput())})}onInput(){this.pendingCompositionCommit=!1,this.evaluateInput()}onBlur(){this.evaluateCommit()}onFocus(){if(this.composing)return;const t=this.displayElement,e=t.value,i=this.createCtx();let n=e;n=this.runNormalizeChar(n,i),n=this.runNormalizeStructure(n,i),n!==e&&(this.setDisplayValuePreserveCaret(t,n,i),this.syncRaw(n)),this.lastAcceptedValue=n,this.lastAcceptedSelection=this.readSelection(t),this.onSelectionChange()}onSelectionChange(){if(this.composing)return;const t=this.displayElement;this.lastAcceptedSelection=this.readSelection(t)}setDisplayValuePreserveCaret(t,e,i){const n=t.value;if(n===e)return;const r=t.selectionStart,s=t.selectionEnd;if(null==r||null==s)return void(t.value=e);let a=n.slice(0,r);a=this.runNormalizeChar(a,i),a=this.runNormalizeStructure(a,i),t.value=e;const l=Math.min(a.length,e.length);try{t.setSelectionRange(l,l)}catch(t){}}evaluateInput(){if(this.composing)return;this.clearErrors(),this.revertRequest=null;const t=this.displayElement,e=t.value,i=this.createCtx();let n=e;n=this.runNormalizeChar(n,i),n=this.runNormalizeStructure(n,i),n!==e&&this.setDisplayValuePreserveCaret(t,n,i),this.runValidate(n,i),this.revertRequest?this.revertDisplay(this.revertRequest):(this.syncRaw(n),this.applyInvalidClass(),this.lastAcceptedValue=n,this.lastAcceptedSelection=this.readSelection(t))}evaluateCommit(){if(this.composing)return;this.clearErrors(),this.revertRequest=null;const t=this.displayElement,e=this.createCtx();let i=t.value;if(i=this.runNormalizeChar(i,e),i=this.runNormalizeStructure(i,e),this.runValidate(i,e),this.revertRequest)return void this.revertDisplay(this.revertRequest);if(i=this.runFix(i,e),this.clearErrors(),this.revertRequest=null,this.runValidate(i,e),this.revertRequest)return void this.revertDisplay(this.revertRequest);this.syncRaw(i);let n=i;n=this.runFormat(n,e),this.syncDisplay(n),this.applyInvalidClass(),this.lastAcceptedValue=i,this.lastAcceptedSelection=this.readSelection(t)}isValid(){return 0===this.errors.length}getErrors(){return this.errors.slice()}getRawValue(){return this.rawElement?this.rawElement.value:this.displayElement.value}toGuard(){return{detach:()=>this.detach(),isValid:()=>this.isValid(),getErrors:()=>this.getErrors(),getRawValue:()=>this.getRawValue(),getDisplayElement:()=>this.displayElement}}}function r(t){if(null==t)return;const e=String(t).trim().toLowerCase();return""===e||"true"===e||"1"===e||"yes"===e||"on"===e||"false"!==e&&"0"!==e&&"no"!==e&&"off"!==e&&void 0}function s(t){if(null==t)return;const e=String(t).trim();if(""===e)return;const i=Number(e);return Number.isFinite(i)?i:void 0}function a(t,e){if(null==t)return;const i=String(t).trim();return""!==i&&e.includes(i)?i:void 0}function l(t){if(null==t||""===String(t).trim())return"auto";const e=String(t).trim().toLowerCase();return"auto"===e||"swap"===e||"off"===e?e:"auto"}function o(t){if(null!=t.tigSeparate)return!0;if(null!=t.tigWarn)return!0;if(null!=t.tigInvalidClass)return!0;for(const e in t)if(e.startsWith("tigRules"))return!0;return!1}function u(t={}){const e=t.allowFullWidth??!0,i=t.allowMinus??!1,n=t.allowDecimal??!1,r=t.allowEmpty??!0,s=new Set(["ー","-","−","‐","-","‒","–","—","―"]),a=new Set([".","。","。"]);function l(t){if(t>="0"&&t<="9")return t;if(e){const e=function(t){const e=t.charCodeAt(0);return 65296<=e&&e<=65305?String.fromCharCode(e-65296+48):null}(t);if(e)return e}return"."===t||e&&a.has(t)?n?".":"":"-"===t?i?"-":"":e&&s.has(t)&&i?"-":""}return{name:"numeric",targets:["input"],normalizeChar(t){let e=String(t);e=e.replace(/,/g,"");let i="";for(const t of e)i+=l(t);return i},normalizeStructure(t){let e="",r=!1,s=!1;for(const a of String(t))a>="0"&&a<="9"?e+=a:"-"===a&&i?r||0!==e.length||(e+="-",r=!0):"."===a&&n&&(s||(e+=".",s=!0));return e},fix(t){let e=String(t);if(""===e)return r?"":"0";if("-"===e||"."===e||"-."===e)return r?"":"0";e.startsWith("-.")&&(e="-0"+e.slice(1)),e.startsWith(".")&&(e="0"+e),e.endsWith(".")&&(e=e.slice(0,-1));let i="";e.startsWith("-")&&(i="-",e=e.slice(1));const n=e.indexOf(".");let s=n>=0?e.slice(0,n):e;const a=n>=0?e.slice(n+1):"";return s=s.replace(/^0+/,""),""===s&&(s="0"),"-"!==i||"0"!==s||a&&!/^0*$/.test(a)||(i=""),n>=0?`${i}${s}.${a}`:`${i}${s}`},validate(t,e){}}}function c(t){let e="",i=String(t);i.startsWith("-")&&(e="-",i=i.slice(1));const n=i.indexOf(".");if(!(n>=0))return{sign:e,intPart:i,fracPart:"",hasDot:!1};return{sign:e,intPart:i.slice(0,n),fracPart:i.slice(n+1),hasDot:!0}}function h(t){let e=1;const i=t.split("");for(let t=i.length-1;t>=0;t--){const n=i[t].charCodeAt(0)-48+e;if(!(n>=10)){i[t]=String.fromCharCode(48+n),e=0;break}i[t]="0",e=1}return 1===e&&i.unshift("1"),i.join("")}function d(t={}){const e={int:"number"==typeof t.int?t.int:void 0,frac:"number"==typeof t.frac?t.frac:void 0,countLeadingZeros:t.countLeadingZeros??!0,fixIntOnBlur:t.fixIntOnBlur??"none",fixFracOnBlur:t.fixFracOnBlur??"none",overflowInputInt:t.overflowInputInt??"none",overflowInputFrac:t.overflowInputFrac??"none",forceFracOnBlur:t.forceFracOnBlur??!1};return{name:"digits",targets:["input"],validate(t,i){const n=String(t);if(""===n||"-"===n||"."===n||"-."===n)return;const{intPart:r,fracPart:s}=c(n);if("number"==typeof e.int){const t=function(t,e){const i=t??"";if(0===i.length)return 0;if(e)return i.length;const n=i.replace(/^0+/,"");return 0===n.length?1:n.length}(r,e.countLeadingZeros);if(t>e.int){if("block"===e.overflowInputInt)return void i.requestRevert({reason:"digits.int_overflow",detail:{limit:e.int,actual:t}});i.pushError({code:"digits.int_overflow",rule:"digits",phase:"validate",detail:{limit:e.int,actual:t}})}}if("number"==typeof e.frac){const t=(s??"").length;if(t>e.frac){if("block"===e.overflowInputFrac)return void i.requestRevert({reason:"digits.frac_overflow",detail:{limit:e.frac,actual:t}});i.pushError({code:"digits.frac_overflow",rule:"digits",phase:"validate",detail:{limit:e.frac,actual:t}})}}},fix(t,i){const n=String(t);if(""===n||"-"===n||"."===n||"-."===n)return n;const r=c(n);let{intPart:s,fracPart:a}=r;const{sign:l,hasDot:o}=r;if("number"==typeof e.int&&"none"!==e.fixIntOnBlur){(s??"").length>e.int&&("truncateLeft"===e.fixIntOnBlur?s=s.slice(s.length-e.int):"truncateRight"===e.fixIntOnBlur?s=s.slice(0,e.int):"clamp"===e.fixIntOnBlur&&(s="9".repeat(e.int)))}if("number"==typeof e.frac&&"none"!==e.fixFracOnBlur&&o){const t=e.frac,i=a??"";if(i.length>t)if("truncate"===e.fixFracOnBlur)a=i.slice(0,t);else if("round"===e.fixFracOnBlur){const e=function(t,e,i){const n=e??"";if(n.length<=i)return{intPart:t,fracPart:n};const r=n.slice(0,i);if(n.charCodeAt(i)-48<5)return{intPart:t,fracPart:r};if(0===i)return{intPart:h(t.length?t:"0"),fracPart:""};let s=1;const a=r.split("");for(let t=a.length-1;t>=0;t--){const e=a[t].charCodeAt(0)-48+s;if(!(e>=10)){a[t]=String.fromCharCode(48+e),s=0;break}a[t]="0",s=1}const l=a.join("");let o=t;return 1===s&&(o=h(t.length?t:"0")),{intPart:o,fracPart:l}}(s,i,t);s=e.intPart,a=e.fracPart}}if(e.forceFracOnBlur&&"number"==typeof e.frac&&e.frac>0){const t=e.frac;o||(a="");const i=a??"";i.length<t&&(a=i+"0".repeat(t-i.length))}if("number"!=typeof e.frac)return`${l}${s}`;if(0===e.frac)return`${l}${s}`;if(o||e.forceFracOnBlur&&e.frac>0){return`${l}${s}.${a??""}`}return`${l}${s}`}}}function m(){return{name:"comma",targets:["input"],format(t){const e=String(t);if(""===e||"-"===e||"."===e||"-."===e)return e;let i="",n=e;n.startsWith("-")&&(i="-",n=n.slice(1));const r=n.indexOf("."),s=r>=0?n.slice(0,r):n,a=r>=0?n.slice(r+1):null,l=s.replace(/\B(?=(\d{3})+(?!\d))/g,",");return null!=a?`${i}${l}.${a}`:`${i}${l}`}}}u.fromDataset=function(t,e){if(null==t.tigRulesNumeric)return null;const i={},n=r(t.tigRulesNumericAllowFullWidth);null!=n&&(i.allowFullWidth=n);const s=r(t.tigRulesNumericAllowMinus);null!=s&&(i.allowMinus=s);const a=r(t.tigRulesNumericAllowDecimal);null!=a&&(i.allowDecimal=a);const l=r(t.tigRulesNumericAllowEmpty);return null!=l&&(i.allowEmpty=l),u(i)},d.fromDataset=function(t,e){if(null==t.tigRulesDigits)return null;const i={},n=s(t.tigRulesDigitsInt);null!=n&&(i.int=n);const l=s(t.tigRulesDigitsFrac);null!=l&&(i.frac=l);const o=r(t.tigRulesDigitsCountLeadingZeros);null!=o&&(i.countLeadingZeros=o);const u=a(t.tigRulesDigitsFixIntOnBlur,["none","truncateLeft","truncateRight","clamp"]);null!=u&&(i.fixIntOnBlur=u);const c=a(t.tigRulesDigitsFixFracOnBlur,["none","truncate","round"]);null!=c&&(i.fixFracOnBlur=c);const h=a(t.tigRulesDigitsOverflowInputInt,["none","block"]);null!=h&&(i.overflowInputInt=h);const m=a(t.tigRulesDigitsOverflowInputFrac,["none","block"]);null!=m&&(i.overflowInputFrac=m);const f=r(t.tigRulesDigitsForceFracOnBlur);return null!=f&&(i.forceFracOnBlur=f),d(i)},m.fromDataset=function(t,e){return null==t.tigRulesComma?null:m()};const f=new class{constructor(t,e){this.attachFn=t,this.ruleFactories=Array.isArray(e)?e:[]}register(t){this.ruleFactories.push(t)}autoAttach(t=document){const e=[],i=[];if(t.querySelectorAll){const e=t.querySelectorAll("input, textarea");for(const t of e)(t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement)&&i.push(t)}(t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement)&&(i.includes(t)||i.push(t));for(const t of i){const i=t.dataset;if("true"===i.tigAttached)continue;if(!o(i))continue;const n={},s=r(i.tigWarn);null!=s&&(n.warn=s),null!=i.tigInvalidClass&&""!==String(i.tigInvalidClass).trim()&&(n.invalidClass=String(i.tigInvalidClass)),n.separateValue={mode:l(i.tigSeparate)};const a=[];for(const e of this.ruleFactories)try{const n=e.fromDataset(i,t);n&&a.push(n)}catch(t){(n.warn??!0)&&console.warn(`[text-input-guard] autoAttach: rule "${e.name}" fromDataset() threw an error.`,t)}if(a.length>0&&(n.rules=a),!n.rules||0===n.rules.length)continue;const u=this.attachFn(t,n);e.push(u),t.dataset.tigAttached="true"}return{detach:()=>{for(const t of e)t.detach()},isValid:()=>e.every(t=>t.isValid()),getErrors:()=>e.flatMap(t=>t.getErrors()),getGuards:()=>e}}}(e,[{name:"numeric",fromDataset:u.fromDataset},{name:"digits",fromDataset:d.fromDataset},{name:"comma",fromDataset:m.fromDataset}]),p=t=>f.autoAttach(t),g={numeric:u,digits:d,comma:m},v="0.0.1";export{e as attach,i as attachAll,p as autoAttach,m as comma,d as digits,u as numeric,g as rules,v as version};
|