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 CHANGED
@@ -4,134 +4,8 @@ TextInputGuard は、**開発中**の日本向けの入力欄ガードライブ
4
4
 
5
5
  `<input>` / `<textarea>` に対して、数値入力や日本語特有の制約(全角混在、桁数、表示整形など)を扱いやすい形で提供します。
6
6
 
7
- 利用方法は次の通りです。
7
+ シンプルな設計のため、機能追加も簡単に行えます。
8
8
 
9
- - `attach()`:1要素ずつ明示的に有効化する
10
- - `attachAll()`:まとめて有効化し、まとめて `detach()` する
11
- - `autoAttach()`:`data-tig-*` の指定から自動で `attach()` する
9
+ 詳細は以下をご確認ください
12
10
 
13
- ## インストール
14
-
15
- ```bash
16
- npm i text-input-guard
17
- ```
18
-
19
- ## 使い方
20
-
21
- ### 1) attach(単一要素に適用)
22
-
23
- 最も基本の使い方です。1つの要素に対してガードを適用します。
24
-
25
- ```js
26
- import { attach, rules } from "text-input-guard";
27
-
28
- const input = document.querySelector("#price");
29
-
30
- const guard = attach(input, {
31
- rules: [
32
- rules.numeric({ allowFullWidth: true, allowMinus: true, allowDecimal: true }),
33
- rules.digits({
34
- int: 6,
35
- frac: 2,
36
- overflowInputInt: "block",
37
- overflowInputFrac: "block",
38
- fixFracOnBlur: "round"
39
- }),
40
- rules.comma()
41
- ]
42
- });
43
-
44
- // 解除
45
- // guard.detach();
46
- ```
47
-
48
- `attach()` が返す `Guard` は、次のメソッドを持ちます。
49
-
50
- - `detach()`:解除(イベント削除・swap復元)
51
- - `isValid()`:現在エラーが無いか
52
- - `getErrors()`:エラー一覧
53
- - `getRawValue()`:送信用の正規化済み値
54
-
55
- ### 2) attachAll(複数要素にまとめて適用)
56
-
57
- 通常は入力欄が複数あるため、`querySelectorAll()` の戻り値に対してまとめて適用できます。
58
-
59
- ```js
60
- import { attachAll, rules } from "text-input-guard";
61
-
62
- const group = attachAll(document.querySelectorAll(".tig-price"), {
63
- rules: [
64
- rules.numeric({ allowFullWidth: true, allowMinus: true, allowDecimal: true }),
65
- rules.digits({ int: 6, frac: 2 }),
66
- rules.comma()
67
- ]
68
- });
69
-
70
- // まとめて解除
71
- // group.detach();
72
-
73
- // 全部 valid なら true
74
- // group.isValid();
75
-
76
- // 全部のエラーを集約して取得
77
- // group.getErrors();
78
-
79
- // 個別 Guard 配列が欲しい場合
80
- // const guards = group.getGuards();
81
- ```
82
-
83
- `attachAll()` は `GuardGroup` を返します。
84
-
85
- - `detach()`:全て解除
86
- - `isValid()`:全て valid なら true
87
- - `getErrors()`:全てのエラーを集約
88
- - `getGuards()`:個別の `Guard[]` を返す
89
-
90
- ### 3) autoAttach(data属性から自動で適用)
91
-
92
- HTML側に `data-tig-*` を書いておき、JS側では `autoAttach()` を呼ぶだけで適用できます。
93
-
94
- ```html
95
- <input
96
- class="price"
97
- name="price"
98
- data-tig-rules-numeric
99
- data-tig-rules-numeric-allow-full-width="true"
100
- data-tig-rules-numeric-allow-minus="true"
101
- data-tig-rules-numeric-allow-decimal="true"
102
- data-tig-rules-digits
103
- data-tig-rules-digits-int="6"
104
- data-tig-rules-digits-frac="2"
105
- data-tig-rules-digits-overflow-input-int="block"
106
- data-tig-rules-digits-overflow-input-frac="block"
107
- data-tig-rules-digits-fix-frac-on-blur="round"
108
- data-tig-rules-comma
109
- />
110
- ```
111
-
112
- ```js
113
- import { autoAttach } from "text-input-guard";
114
-
115
- // document 全体を対象に自動適用
116
- const guards = autoAttach();
117
-
118
- // 動的追加したコンテナだけ適用したい場合
119
- // autoAttach(container);
120
- ```
121
-
122
- `autoAttach()` は attachした `GuardGroup` を返します。
123
-
124
- - 既に `data-tig-attached` が付いている要素はスキップします
125
- - `data-tig-rules-*` を読み取って `rules` に変換します
126
-
127
- ## ルール
128
-
129
- 現在のルール例(公開API:`rules.xxx(...)`):
130
-
131
- - `rules.numeric(...)`:数値入力の正規化(全角→半角、記号統一、不要文字除去)
132
- - `rules.digits(...)`:整数部/小数部の桁数チェック、確定時の穏やか補正、入力ブロック
133
- - `rules.comma()`:確定時のカンマ付与(表示整形)
134
-
135
- ## ライセンス
136
-
137
- MIT
11
+ ### **[TextInputGuard 紹介](https://natade-jp.github.io/text-input-guard/)**
@@ -202,7 +202,7 @@ class InputGuard {
202
202
 
203
203
  const kind = detectKind(element);
204
204
  if (!kind) {
205
- throw new TypeError("[jp-input-guard] attach() expects an <input> or <textarea> element.");
205
+ throw new TypeError("[text-input-guard] attach() expects an <input> or <textarea> element.");
206
206
  }
207
207
 
208
208
  /**
@@ -434,7 +434,7 @@ class InputGuard {
434
434
  }
435
435
 
436
436
  if (this.kind !== "input") {
437
- warnLog('[jp-input-guard] separateValue.mode="swap" is not supported for <textarea>. ignored.', this.warn);
437
+ warnLog('[text-input-guard] separateValue.mode="swap" is not supported for <textarea>. ignored.', this.warn);
438
438
  return;
439
439
  }
440
440
 
@@ -595,7 +595,7 @@ class InputGuard {
595
595
 
596
596
  if (!supports) {
597
597
  warnLog(
598
- `[jp-input-guard] Rule "${rule.name}" is not supported for <${this.kind}>. skipped.`,
598
+ `[text-input-guard] Rule "${rule.name}" is not supported for <${this.kind}>. skipped.`,
599
599
  this.warn
600
600
  );
601
601
  continue;
@@ -680,9 +680,7 @@ class InputGuard {
680
680
  // 連鎖防止(次の処理に持ち越さない)
681
681
  this.revertRequest = null;
682
682
 
683
- if (this.warn) {
684
- console.log(`[jp-input-guard] reverted: ${req.reason}`, req.detail);
685
- }
683
+ if (this.warn) ;
686
684
  }
687
685
 
688
686
  /**
@@ -826,7 +824,7 @@ class InputGuard {
826
824
  * @returns {void}
827
825
  */
828
826
  onCompositionStart() {
829
- console.log("[jp-input-guard] compositionstart");
827
+ // console.log("[text-input-guard] compositionstart");
830
828
  this.composing = true;
831
829
  }
832
830
 
@@ -836,7 +834,7 @@ class InputGuard {
836
834
  * @returns {void}
837
835
  */
838
836
  onCompositionEnd() {
839
- console.log("[jp-input-guard] compositionend");
837
+ // console.log("[text-input-guard] compositionend");
840
838
  this.composing = false;
841
839
 
842
840
  // compositionend後に input が来ない環境向けのフォールバック
@@ -856,7 +854,7 @@ class InputGuard {
856
854
  * @returns {void}
857
855
  */
858
856
  onInput() {
859
- console.log("[jp-input-guard] input");
857
+ // console.log("[text-input-guard] input");
860
858
  // compositionend後に input が来た場合、フォールバックを無効化
861
859
  this.pendingCompositionCommit = false;
862
860
  this.evaluateInput();
@@ -867,7 +865,7 @@ class InputGuard {
867
865
  * @returns {void}
868
866
  */
869
867
  onBlur() {
870
- console.log("[jp-input-guard] blur");
868
+ // console.log("[text-input-guard] blur");
871
869
  this.evaluateCommit();
872
870
  }
873
871
 
@@ -1116,6 +1114,63 @@ class InputGuard {
1116
1114
  * The MIT license https://opensource.org/licenses/MIT
1117
1115
  */
1118
1116
 
1117
+ /**
1118
+ * datasetのboolean値を解釈する
1119
+ * - 未指定なら undefined
1120
+ * - "" / "true" / "1" / "yes" / "on" は true
1121
+ * - "false" / "0" / "no" / "off" は false
1122
+ * @param {string|undefined} v
1123
+ * @returns {boolean|undefined}
1124
+ */
1125
+ function parseDatasetBool(v) {
1126
+ if (v == null) { return; }
1127
+ const s = String(v).trim().toLowerCase();
1128
+ if (s === "" || s === "true" || s === "1" || s === "yes" || s === "on") { return true; }
1129
+ if (s === "false" || s === "0" || s === "no" || s === "off") { return false; }
1130
+ return;
1131
+ }
1132
+
1133
+ /**
1134
+ * datasetのnumber値を解釈する(整数想定)
1135
+ * - 未指定/空なら undefined
1136
+ * - 数値でなければ undefined
1137
+ * @param {string|undefined} v
1138
+ * @returns {number|undefined}
1139
+ */
1140
+ function parseDatasetNumber(v) {
1141
+ if (v == null) { return; }
1142
+ const s = String(v).trim();
1143
+ if (s === "") { return; }
1144
+ const n = Number(s);
1145
+ return Number.isFinite(n) ? n : undefined;
1146
+ }
1147
+
1148
+ /**
1149
+ * enumを解釈する(未指定なら undefined)
1150
+ * @template {string} T
1151
+ * @param {string|undefined} v
1152
+ * @param {readonly T[]} allowed
1153
+ * @returns {T|undefined}
1154
+ */
1155
+ function parseDatasetEnum(v, allowed) {
1156
+ if (v == null) { return; }
1157
+ const s = String(v).trim();
1158
+ if (s === "") { return; }
1159
+ // 大文字小文字を区別したいならここを変える(今は厳密一致)
1160
+ return /** @type {T|undefined} */ (allowed.includes(/** @type {any} */ (s)) ? s : undefined);
1161
+ }
1162
+
1163
+ /**
1164
+ * The script is part of TextInputGuard.
1165
+ *
1166
+ * AUTHOR:
1167
+ * natade-jp (https://github.com/natade-jp)
1168
+ *
1169
+ * LICENSE:
1170
+ * The MIT license https://opensource.org/licenses/MIT
1171
+ */
1172
+
1173
+
1119
1174
  /**
1120
1175
  * @typedef {GuardGroup} GuardGroup
1121
1176
  * @typedef {Guard} Guard
@@ -1130,19 +1185,6 @@ class InputGuard {
1130
1185
  * @property {(dataset: DOMStringMap, el: HTMLInputElement|HTMLTextAreaElement) => Rule|null} fromDataset
1131
1186
  */
1132
1187
 
1133
- /**
1134
- * Boolean系のdata値を解釈する(未指定なら undefined を返す)
1135
- * @param {string|undefined} v
1136
- * @returns {boolean|undefined}
1137
- */
1138
- function parseBool(v) {
1139
- if (v == null) { return; }
1140
- const s = String(v).trim().toLowerCase();
1141
- if (s === "" || s === "true" || s === "1" || s === "yes" || s === "on") { return true; }
1142
- if (s === "false" || s === "0" || s === "no" || s === "off") { return false; }
1143
- return;
1144
- }
1145
-
1146
1188
  /**
1147
1189
  * separate mode を解釈する(未指定は "auto")
1148
1190
  * @param {string|undefined} v
@@ -1246,7 +1288,7 @@ class InputGuardAutoAttach {
1246
1288
  const options = {};
1247
1289
 
1248
1290
  // warn / invalidClass
1249
- const warn = parseBool(ds.tigWarn);
1291
+ const warn = parseDatasetBool(ds.tigWarn);
1250
1292
  if (warn != null) { options.warn = warn; }
1251
1293
 
1252
1294
  if (ds.tigInvalidClass != null && String(ds.tigInvalidClass).trim() !== "") {
@@ -1266,7 +1308,7 @@ class InputGuardAutoAttach {
1266
1308
  } catch (e) {
1267
1309
  const w = options.warn ?? true;
1268
1310
  if (w) {
1269
- console.warn(`[jp-input-guard] autoAttach: rule "${fac.name}" fromDataset() threw an error.`, e);
1311
+ console.warn(`[text-input-guard] autoAttach: rule "${fac.name}" fromDataset() threw an error.`, e);
1270
1312
  }
1271
1313
  }
1272
1314
  }
@@ -1292,62 +1334,6 @@ class InputGuardAutoAttach {
1292
1334
  }
1293
1335
  }
1294
1336
 
1295
- /**
1296
- * The script is part of TextInputGuard.
1297
- *
1298
- * AUTHOR:
1299
- * natade-jp (https://github.com/natade-jp)
1300
- *
1301
- * LICENSE:
1302
- * The MIT license https://opensource.org/licenses/MIT
1303
- */
1304
-
1305
- /**
1306
- * datasetのboolean値を解釈する
1307
- * - 未指定なら undefined
1308
- * - "" / "true" / "1" / "yes" / "on" は true
1309
- * - "false" / "0" / "no" / "off" は false
1310
- * @param {string|undefined} v
1311
- * @returns {boolean|undefined}
1312
- */
1313
- function parseDatasetBool(v) {
1314
- if (v == null) { return; }
1315
- const s = String(v).trim().toLowerCase();
1316
- if (s === "" || s === "true" || s === "1" || s === "yes" || s === "on") { return true; }
1317
- if (s === "false" || s === "0" || s === "no" || s === "off") { return false; }
1318
- return;
1319
- }
1320
-
1321
- /**
1322
- * datasetのnumber値を解釈する(整数想定)
1323
- * - 未指定/空なら undefined
1324
- * - 数値でなければ undefined
1325
- * @param {string|undefined} v
1326
- * @returns {number|undefined}
1327
- */
1328
- function parseDatasetNumber(v) {
1329
- if (v == null) { return; }
1330
- const s = String(v).trim();
1331
- if (s === "") { return; }
1332
- const n = Number(s);
1333
- return Number.isFinite(n) ? n : undefined;
1334
- }
1335
-
1336
- /**
1337
- * enumを解釈する(未指定なら undefined)
1338
- * @template {string} T
1339
- * @param {string|undefined} v
1340
- * @param {readonly T[]} allowed
1341
- * @returns {T|undefined}
1342
- */
1343
- function parseDatasetEnum(v, allowed) {
1344
- if (v == null) { return; }
1345
- const s = String(v).trim();
1346
- if (s === "") { return; }
1347
- // 大文字小文字を区別したいならここを変える(今は厳密一致)
1348
- return /** @type {T|undefined} */ (allowed.includes(/** @type {any} */ (s)) ? s : undefined);
1349
- }
1350
-
1351
1337
  /**
1352
1338
  * The script is part of TextInputGuard.
1353
1339
  *
@@ -1365,6 +1351,7 @@ function parseDatasetEnum(v, allowed) {
1365
1351
  * @property {boolean} [allowFullWidth=true] - 全角数字/記号を許可して半角へ正規化する
1366
1352
  * @property {boolean} [allowMinus=false] - マイナス記号を許可する(先頭のみ)
1367
1353
  * @property {boolean} [allowDecimal=false] - 小数点を許可する(1つだけ)
1354
+ * @property {boolean} [allowEmpty=true] - 空文字を許可するか
1368
1355
  */
1369
1356
 
1370
1357
  /**
@@ -1380,7 +1367,8 @@ function numeric(options = {}) {
1380
1367
  const opt = {
1381
1368
  allowFullWidth: options.allowFullWidth ?? true,
1382
1369
  allowMinus: options.allowMinus ?? false,
1383
- allowDecimal: options.allowDecimal ?? false
1370
+ allowDecimal: options.allowDecimal ?? false,
1371
+ allowEmpty: options.allowEmpty ?? true
1384
1372
  };
1385
1373
 
1386
1374
  /** @type {Set<string>} */
@@ -1538,9 +1526,14 @@ function numeric(options = {}) {
1538
1526
  fix(value) {
1539
1527
  let v = String(value);
1540
1528
 
1529
+ // 空文字の扱い
1530
+ if (v === "") {
1531
+ return opt.allowEmpty ? "" : "0";
1532
+ }
1533
+
1541
1534
  // 未完成な数値は空にする
1542
1535
  if (v === "-" || v === "." || v === "-.") {
1543
- return "";
1536
+ return opt.allowEmpty ? "" : "0";
1544
1537
  }
1545
1538
 
1546
1539
  // "-.1" → "-0.1"
@@ -1612,6 +1605,7 @@ function numeric(options = {}) {
1612
1605
  * - data-tig-rules-numeric-allow-full-width -> dataset.tigRulesNumericAllowFullWidth
1613
1606
  * - data-tig-rules-numeric-allow-minus -> dataset.tigRulesNumericAllowMinus
1614
1607
  * - data-tig-rules-numeric-allow-decimal -> dataset.tigRulesNumericAllowDecimal
1608
+ * - data-tig-rules-numeric-allow-empty -> dataset.tigRulesNumericAllowEmpty
1615
1609
  *
1616
1610
  * @param {DOMStringMap} dataset
1617
1611
  * @param {HTMLInputElement|HTMLTextAreaElement} _el
@@ -1644,6 +1638,12 @@ numeric.fromDataset = function fromDataset(dataset, _el) {
1644
1638
  options.allowDecimal = allowDecimal;
1645
1639
  }
1646
1640
 
1641
+ // data-tig-rules-numeric-allow-empty(未指定なら numeric側デフォルト true)
1642
+ const allowEmpty = parseDatasetBool(dataset.tigRulesNumericAllowEmpty);
1643
+ if (allowEmpty != null) {
1644
+ options.allowEmpty = allowEmpty;
1645
+ }
1646
+
1647
1647
  return numeric(options);
1648
1648
  };
1649
1649
 
@@ -1668,6 +1668,7 @@ numeric.fromDataset = function fromDataset(dataset, _el) {
1668
1668
  * @property {"none"|"truncate"|"round"} [fixFracOnBlur="none"] - blur時の小数部補正
1669
1669
  * @property {"none"|"block"} [overflowInputInt="none"] - 入力中:整数部が最大桁を超える入力をブロックする
1670
1670
  * @property {"none"|"block"} [overflowInputFrac="none"] - 入力中:小数部が最大桁を超える入力をブロックする
1671
+ * @property {boolean} [forceFracOnBlur=false] - blur時に小数部を必ず表示(frac桁まで0埋め)
1671
1672
  */
1672
1673
 
1673
1674
  /**
@@ -1808,7 +1809,8 @@ function digits(options = {}) {
1808
1809
  fixIntOnBlur: options.fixIntOnBlur ?? "none",
1809
1810
  fixFracOnBlur: options.fixFracOnBlur ?? "none",
1810
1811
  overflowInputInt: options.overflowInputInt ?? "none",
1811
- overflowInputFrac: options.overflowInputFrac ?? "none"
1812
+ overflowInputFrac: options.overflowInputFrac ?? "none",
1813
+ forceFracOnBlur: options.forceFracOnBlur ?? false
1812
1814
  };
1813
1815
 
1814
1816
  return {
@@ -1925,14 +1927,39 @@ function digits(options = {}) {
1925
1927
  }
1926
1928
  }
1927
1929
 
1928
- // 組み立て(frac=0 のときは "." を残すか?は方針次第だが、ここでは消す)
1929
- if (!hasDot || typeof opt.frac !== "number") {
1930
+ if (opt.forceFracOnBlur && typeof opt.frac === "number" && opt.frac > 0) {
1931
+ const limit = opt.frac;
1932
+ // "." が無いなら作る(12 → 12.00)
1933
+ if (!hasDot) {
1934
+ fracPart = "";
1935
+ }
1936
+ // 足りない分を 0 埋め(12.3 → 12.30 / 12. → 12.00)
1937
+ const f = fracPart ?? "";
1938
+ if (f.length < limit) {
1939
+ fracPart = f + "0".repeat(limit - f.length);
1940
+ }
1941
+ }
1942
+
1943
+ // 組み立て
1944
+ if (typeof opt.frac !== "number") {
1945
+ // frac未指定なら、dot があっても digits は触らず intだけ返す方針(現状維持)
1930
1946
  return `${sign}${intPart}`;
1931
1947
  }
1948
+
1932
1949
  if (opt.frac === 0) {
1950
+ // 小数0桁なら常に整数表示
1933
1951
  return `${sign}${intPart}`;
1934
1952
  }
1935
- return `${sign}${intPart}.${fracPart}`;
1953
+
1954
+ // frac 指定あり(1以上)
1955
+ if (hasDot || (opt.forceFracOnBlur && opt.frac > 0)) {
1956
+ // "." が無いけど forceFracOnBlur の場合もここに来る
1957
+ const f = fracPart ?? "";
1958
+ return `${sign}${intPart}.${f}`;
1959
+ }
1960
+
1961
+ // "." が無くて force もしないなら整数表示
1962
+ return `${sign}${intPart}`;
1936
1963
  }
1937
1964
  };
1938
1965
  }
@@ -1951,6 +1978,7 @@ function digits(options = {}) {
1951
1978
  * - data-tig-rules-digits-fix-frac-on-blur -> dataset.tigRulesDigitsFixFracOnBlur
1952
1979
  * - data-tig-rules-digits-overflow-input-int -> dataset.tigRulesDigitsOverflowInputInt
1953
1980
  * - data-tig-rules-digits-overflow-input-frac -> dataset.tigRulesDigitsOverflowInputFrac
1981
+ * - data-tig-rules-digits-force-frac-on-blur -> dataset.tigRulesDigitsForceFracOnBlur
1954
1982
  *
1955
1983
  * @param {DOMStringMap} dataset
1956
1984
  * @param {HTMLInputElement|HTMLTextAreaElement} _el
@@ -2013,6 +2041,12 @@ digits.fromDataset = function fromDataset(dataset, _el) {
2013
2041
  options.overflowInputFrac = ovFrac;
2014
2042
  }
2015
2043
 
2044
+ // forceFracOnBlur
2045
+ const forceFrac = parseDatasetBool(dataset.tigRulesDigitsForceFracOnBlur);
2046
+ if (forceFrac != null) {
2047
+ options.forceFracOnBlur = forceFrac;
2048
+ }
2049
+
2016
2050
  return digits(options);
2017
2051
  };
2018
2052
 
@@ -2038,6 +2072,11 @@ function comma() {
2038
2072
 
2039
2073
  /**
2040
2074
  * 表示整形(確定時のみ)
2075
+ *
2076
+ * 前提:
2077
+ * - numeric / digits 等で正規化済みの数値文字列が渡される
2078
+ * - 整数部・小数部・符号のみを含む(カンマは含まない想定)
2079
+ *
2041
2080
  * @param {string} value
2042
2081
  * @returns {string}
2043
2082
  */
@@ -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
+ "use strict";function t(t,e){e&&console.warn(t)}function e(t,e={}){const n=new i(t,e);return n.init(),n.toGuard()}class i{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 n(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 r(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 s(t,e){if(null==t)return;const i=String(t).trim();return""!==i&&e.includes(i)?i:void 0}function a(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 l(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 o(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 u(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 c(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 h(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}=u(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=u(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:c(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=c(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 d(){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}`}}}o.fromDataset=function(t,e){if(null==t.tigRulesNumeric)return null;const i={},r=n(t.tigRulesNumericAllowFullWidth);null!=r&&(i.allowFullWidth=r);const s=n(t.tigRulesNumericAllowMinus);null!=s&&(i.allowMinus=s);const a=n(t.tigRulesNumericAllowDecimal);null!=a&&(i.allowDecimal=a);const l=n(t.tigRulesNumericAllowEmpty);return null!=l&&(i.allowEmpty=l),o(i)},h.fromDataset=function(t,e){if(null==t.tigRulesDigits)return null;const i={},a=r(t.tigRulesDigitsInt);null!=a&&(i.int=a);const l=r(t.tigRulesDigitsFrac);null!=l&&(i.frac=l);const o=n(t.tigRulesDigitsCountLeadingZeros);null!=o&&(i.countLeadingZeros=o);const u=s(t.tigRulesDigitsFixIntOnBlur,["none","truncateLeft","truncateRight","clamp"]);null!=u&&(i.fixIntOnBlur=u);const c=s(t.tigRulesDigitsFixFracOnBlur,["none","truncate","round"]);null!=c&&(i.fixFracOnBlur=c);const d=s(t.tigRulesDigitsOverflowInputInt,["none","block"]);null!=d&&(i.overflowInputInt=d);const m=s(t.tigRulesDigitsOverflowInputFrac,["none","block"]);null!=m&&(i.overflowInputFrac=m);const p=n(t.tigRulesDigitsForceFracOnBlur);return null!=p&&(i.forceFracOnBlur=p),h(i)},d.fromDataset=function(t,e){return null==t.tigRulesComma?null:d()};const m=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(!l(i))continue;const r={},s=n(i.tigWarn);null!=s&&(r.warn=s),null!=i.tigInvalidClass&&""!==String(i.tigInvalidClass).trim()&&(r.invalidClass=String(i.tigInvalidClass)),r.separateValue={mode:a(i.tigSeparate)};const o=[];for(const e of this.ruleFactories)try{const n=e.fromDataset(i,t);n&&o.push(n)}catch(t){(r.warn??!0)&&console.warn(`[text-input-guard] autoAttach: rule "${e.name}" fromDataset() threw an error.`,t)}if(o.length>0&&(r.rules=o),!r.rules||0===r.rules.length)continue;const u=this.attachFn(t,r);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:o.fromDataset},{name:"digits",fromDataset:h.fromDataset},{name:"comma",fromDataset:d.fromDataset}]),p={numeric:o,digits:h,comma:d};exports.attach=e,exports.attachAll=function(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}},exports.autoAttach=t=>m.autoAttach(t),exports.comma=d,exports.digits=h,exports.numeric=o,exports.rules=p,exports.version="0.0.1";