text-input-guard 0.0.1 → 0.1.1

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.
@@ -1,3 +1,246 @@
1
+ /**
2
+ * The script is part of TextInputGuard.
3
+ *
4
+ * AUTHOR:
5
+ * natade-jp (https://github.com/natade-jp)
6
+ *
7
+ * LICENSE:
8
+ * The MIT license https://opensource.org/licenses/MIT
9
+ */
10
+
11
+ /**
12
+ * SwapState
13
+ *
14
+ * separateValue.mode="swap" のときに使用する
15
+ * 元 input 要素の状態スナップショットおよび復元ロジックを管理するクラス
16
+ *
17
+ * 役割
18
+ * - swap前の input の属性状態を保持する
19
+ * - raw化および display生成時に必要な属性を適用する
20
+ * - detach時に元の状態へ復元する
21
+ *
22
+ * 設計方針
23
+ * - 送信用属性は raw に残す
24
+ * - UIおよびアクセシビリティ属性は display に適用する
25
+ * - tig内部用の data-* は display にコピーしない
26
+ */
27
+ class SwapState {
28
+ /**
29
+ * 元 input の type 属性
30
+ * detach時に復元するため保持する
31
+ * @type {string}
32
+ */
33
+ originalType;
34
+
35
+ /**
36
+ * 元 input の id 属性
37
+ * swap時に display へ移し detach時に rawへ戻す
38
+ * @type {string|null}
39
+ */
40
+ originalId;
41
+
42
+ /**
43
+ * 元 input の name 属性
44
+ * 送信用属性のため raw側に残すが
45
+ * detach時の整合性のため保持する
46
+ * @type {string|null}
47
+ */
48
+ originalName;
49
+
50
+ /**
51
+ * 元 input の class 属性
52
+ * swap時に display へ移す
53
+ * @type {string}
54
+ */
55
+ originalClass;
56
+
57
+ /**
58
+ * UI系属性のスナップショット
59
+ * placeholder inputmode required などを保持する
60
+ *
61
+ * key 属性名
62
+ * value 属性値 未指定の場合は null
63
+ *
64
+ * @type {Object.<string, string|null>}
65
+ */
66
+ originalUiAttrs;
67
+
68
+ /**
69
+ * aria-* 属性のスナップショット
70
+ * アクセシビリティ維持のため display に適用する
71
+ *
72
+ * key aria属性名 例 aria-label
73
+ * value 属性値
74
+ *
75
+ * @type {Object.<string, string>}
76
+ */
77
+ originalAriaAttrs;
78
+
79
+ /**
80
+ * tig 以外の data-* 属性のスナップショット
81
+ * swap後も display へ引き継ぐ
82
+ *
83
+ * key datasetキー camelCase
84
+ * value 属性値
85
+ *
86
+ * @type {Object.<string, string>}
87
+ */
88
+ originalDataset;
89
+
90
+ /**
91
+ * swap時に生成された display 用 input 要素
92
+ * detach時に削除するため保持する
93
+ *
94
+ * @type {HTMLInputElement|null}
95
+ */
96
+ createdDisplay;
97
+
98
+ /**
99
+ * @param {HTMLInputElement} input
100
+ * swap前の元 input 要素
101
+ */
102
+ constructor(input) {
103
+ this.originalType = input.type;
104
+ this.originalId = input.getAttribute("id");
105
+ this.originalName = input.getAttribute("name");
106
+ this.originalClass = input.className;
107
+
108
+ this.originalUiAttrs = {};
109
+ this.originalAriaAttrs = {};
110
+ this.originalDataset = {};
111
+ this.createdDisplay = null;
112
+
113
+ const UI_ATTRS = [
114
+ "placeholder",
115
+ "inputmode",
116
+ "autocomplete",
117
+ "required",
118
+ "minlength",
119
+ "maxlength",
120
+ "pattern",
121
+ "title",
122
+ "tabindex"
123
+ ];
124
+
125
+ for (const name of UI_ATTRS) {
126
+ this.originalUiAttrs[name] =
127
+ input.hasAttribute(name) ? input.getAttribute(name) : null;
128
+ }
129
+
130
+ for (const attr of input.attributes) {
131
+ if (attr.name.startsWith("aria-")) {
132
+ this.originalAriaAttrs[attr.name] = attr.value ?? "";
133
+ }
134
+ }
135
+
136
+ for (const [k, v] of Object.entries(input.dataset)) {
137
+ if (k.startsWith("tig")) { continue; }
138
+ this.originalDataset[k] = v;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * raw 元input を hidden 化する
144
+ * 送信担当要素として扱う
145
+ *
146
+ * @param {HTMLInputElement} input
147
+ * @returns {void}
148
+ */
149
+ applyToRaw(input) {
150
+ // raw化(送信担当)
151
+ input.type = "hidden";
152
+ input.removeAttribute("id");
153
+ input.className = "";
154
+ input.dataset.tigRole = "raw";
155
+
156
+ // 元idのメタを残す(デバッグ/参照用)
157
+ if (this.originalId) {
158
+ input.dataset.tigOriginalId = this.originalId;
159
+ }
160
+ if (this.originalName) {
161
+ input.dataset.tigOriginalName = this.originalName;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * display用 input を生成し UI属性 aria属性 data属性を適用
167
+ *
168
+ * @param {HTMLInputElement} raw hidden化された元input
169
+ * @returns {HTMLInputElement}
170
+ */
171
+ createDisplay(raw) {
172
+ const display = document.createElement("input");
173
+ display.type = "text";
174
+ display.dataset.tigRole = "display";
175
+
176
+ if (this.originalId) {
177
+ display.id = this.originalId;
178
+ }
179
+
180
+ display.className = this.originalClass ?? "";
181
+ display.value = raw.value;
182
+
183
+ for (const [name, v] of Object.entries(this.originalUiAttrs)) {
184
+ if (v == null) {
185
+ display.removeAttribute(name);
186
+ } else {
187
+ display.setAttribute(name, v);
188
+ }
189
+ }
190
+
191
+ for (const [name, v] of Object.entries(this.originalAriaAttrs)) {
192
+ display.setAttribute(name, v);
193
+ }
194
+
195
+ for (const [k, v] of Object.entries(this.originalDataset)) {
196
+ display.dataset[k] = v;
197
+ }
198
+
199
+ this.createdDisplay = display;
200
+ return display;
201
+ }
202
+
203
+ /**
204
+ * detach時に display 要素を削除する
205
+ *
206
+ * @returns {void}
207
+ */
208
+ removeDisplay() {
209
+ if (this.createdDisplay?.parentNode) {
210
+ this.createdDisplay.parentNode.removeChild(this.createdDisplay);
211
+ }
212
+ this.createdDisplay = null;
213
+ }
214
+
215
+ /**
216
+ * raw hidden化された元input を元の状態へ復元する
217
+ *
218
+ * @param {HTMLInputElement} raw
219
+ * @returns {void}
220
+ */
221
+ restoreRaw(raw) {
222
+ raw.type = this.originalType;
223
+
224
+ if (this.originalId) {
225
+ raw.setAttribute("id", this.originalId);
226
+ } else {
227
+ raw.removeAttribute("id");
228
+ }
229
+
230
+ if (this.originalName) {
231
+ raw.setAttribute("name", this.originalName);
232
+ } else {
233
+ raw.removeAttribute("name");
234
+ }
235
+
236
+ raw.className = this.originalClass ?? "";
237
+
238
+ delete raw.dataset.tigRole;
239
+ delete raw.dataset.tigOriginalId;
240
+ delete raw.dataset.tigOriginalName;
241
+ }
242
+ }
243
+
1
244
  /**
2
245
  * The script is part of JPInputGuard.
3
246
  *
@@ -8,6 +251,7 @@
8
251
  * The MIT license https://opensource.org/licenses/MIT
9
252
  */
10
253
 
254
+
11
255
  /**
12
256
  * 対象要素の種別(現在は input と textarea のみ対応)
13
257
  * @typedef {"input"|"textarea"} ElementKind
@@ -35,7 +279,9 @@
35
279
  * @property {() => boolean} isValid - 現在エラーが無いかどうか
36
280
  * @property {() => TigError[]} getErrors - エラー一覧を取得
37
281
  * @property {() => string} getRawValue - 送信用の正規化済み値を取得
38
- * @property {() => HTMLInputElement|HTMLTextAreaElement} getDisplayElement - ユーザーが実際に操作している要素(swap時はdisplay側)
282
+ * @property {() => string} getDisplayValue - ユーザーが実際に操作している要素の値を取得
283
+ * @property {() => HTMLInputElement|HTMLTextAreaElement} getRawElement - 送信用の正規化済み値の要素
284
+ * @property {() => HTMLInputElement|HTMLTextAreaElement} getDisplayElement - ユーザーが実際に操作している要素(swap時はdisplay専用)
39
285
  */
40
286
 
41
287
  /**
@@ -84,17 +330,6 @@
84
330
  * @property {SeparateValueOptions} [separateValue] - 表示値と内部値の分離設定
85
331
  */
86
332
 
87
- /**
88
- * swap時に退避する元inputの情報
89
- * detach時に元の状態へ復元するために使用する
90
- * @typedef {Object} SwapState
91
- * @property {string} originalType - 元のinput.type
92
- * @property {string|null} originalId - 元のid属性
93
- * @property {string|null} originalName - 元のname属性
94
- * @property {string} originalClass - 元のclass文字列
95
- * @property {HTMLInputElement} createdDisplay - 生成した表示用input
96
- */
97
-
98
333
  /**
99
334
  * selection(カーソル/選択範囲)の退避情報
100
335
  * @typedef {Object} SelectionState
@@ -200,7 +435,7 @@ class InputGuard {
200
435
 
201
436
  const kind = detectKind(element);
202
437
  if (!kind) {
203
- throw new TypeError("[jp-input-guard] attach() expects an <input> or <textarea> element.");
438
+ throw new TypeError("[text-input-guard] attach() expects an <input> or <textarea> element.");
204
439
  }
205
440
 
206
441
  /**
@@ -432,67 +667,25 @@ class InputGuard {
432
667
  }
433
668
 
434
669
  if (this.kind !== "input") {
435
- warnLog('[jp-input-guard] separateValue.mode="swap" is not supported for <textarea>. ignored.', this.warn);
670
+ warnLog('[text-input-guard] separateValue.mode="swap" is not supported for <textarea>. ignored.', this.warn);
436
671
  return;
437
672
  }
438
673
 
439
674
  const input = /** @type {HTMLInputElement} */ (this.originalElement);
440
675
 
441
- // 退避(detachで戻すため)
442
- /** @type {SwapState} */
443
- this.swapState = {
444
- originalType: input.type,
445
- originalId: input.getAttribute("id"),
446
- originalName: input.getAttribute("name"),
447
- originalClass: input.className,
448
- // 必要になったらここに placeholder/aria/data を追加していく
449
- createdDisplay: null
450
- };
451
-
452
- // raw化(送信担当)
453
- input.type = "hidden";
454
- input.removeAttribute("id"); // displayに引き継ぐため
455
- input.dataset.tigRole = "raw";
456
-
457
- // 元idのメタを残す(デバッグ/参照用)
458
- if (this.swapState.originalId) {
459
- input.dataset.tigOriginalId = this.swapState.originalId;
460
- }
461
-
462
- if (this.swapState.originalName) {
463
- input.dataset.tigOriginalName = this.swapState.originalName;
464
- }
465
-
466
- // display生成(ユーザー入力担当)
467
- const display = document.createElement("input");
468
- display.type = "text";
469
- display.dataset.tigRole = "display";
470
-
471
- // id は display に移す
472
- if (this.swapState.originalId) {
473
- display.id = this.swapState.originalId;
474
- }
475
-
476
- // name は付けない(送信しない)
477
- display.removeAttribute("name");
676
+ const state = new SwapState(input);
677
+ state.applyToRaw(input);
478
678
 
479
- // class display に
480
- display.className = this.swapState.originalClass;
481
- input.className = "";
482
-
483
- // value 初期同期
484
- display.value = input.value;
485
-
486
- // DOMに挿入(rawの直後)
679
+ const display = state.createDisplay(input);
487
680
  input.after(display);
488
681
 
682
+ this.swapState = state;
683
+
489
684
  // elements更新
490
- this.hostElement = input;
491
- this.displayElement = display;
685
+ this.hostElement = input; // raw
686
+ this.displayElement = display; // display
492
687
  this.rawElement = input;
493
688
 
494
- this.swapState.createdDisplay = display;
495
-
496
689
  // revert 機構
497
690
  this.lastAcceptedValue = display.value;
498
691
  this.lastAcceptedSelection = this.readSelection(display);
@@ -512,10 +705,10 @@ class InputGuard {
512
705
 
513
706
  // rawは元の input(hidden化されている)
514
707
  const raw = /** @type {HTMLInputElement} */ (this.hostElement);
515
- const display = state.createdDisplay;
516
708
 
517
709
  // displayが存在するなら、最新表示値をrawに同期してから消す(安全策)
518
710
  // ※ rawは常に正規化済みを持つ設計だけど、念のため
711
+ const display = state.createdDisplay;
519
712
  if (display) {
520
713
  try {
521
714
  raw.value = raw.value || display.value;
@@ -525,39 +718,18 @@ class InputGuard {
525
718
  }
526
719
 
527
720
  // display削除
528
- if (display && display.parentNode) {
529
- display.parentNode.removeChild(display);
530
- }
721
+ state.removeDisplay();
531
722
 
532
723
  // rawを元に戻す(type)
533
- raw.type = state.originalType;
534
-
535
- // id を戻す
536
- if (state.originalId) {
537
- raw.setAttribute("id", state.originalId);
538
- } else {
539
- raw.removeAttribute("id");
540
- }
541
-
542
- // name を戻す(swap中は残している想定だが、念のため)
543
- if (state.originalName) {
544
- raw.setAttribute("name", state.originalName);
545
- } else {
546
- raw.removeAttribute("name");
547
- }
548
-
549
- // class を戻す
550
- raw.className = state.originalClass ?? "";
551
-
552
- // data属性(tig用)は消しておく
553
- delete raw.dataset.tigRole;
554
- delete raw.dataset.tigOriginalId;
555
- delete raw.dataset.tigOriginalName;
724
+ state.restoreRaw(raw);
556
725
 
557
726
  // elements参照を original に戻す
558
727
  this.hostElement = this.originalElement;
559
728
  this.displayElement = this.originalElement;
560
729
  this.rawElement = null;
730
+
731
+ // swapState破棄
732
+ this.swapState = null;
561
733
  }
562
734
 
563
735
  /**
@@ -569,8 +741,6 @@ class InputGuard {
569
741
  this.unbindEvents();
570
742
  // swap復元
571
743
  this.restoreSeparateValue();
572
- // swapState破棄
573
- this.swapState = null;
574
744
  // 以後このインスタンスは利用不能にしてもいいが、今回は明示しない
575
745
  }
576
746
 
@@ -593,7 +763,7 @@ class InputGuard {
593
763
 
594
764
  if (!supports) {
595
765
  warnLog(
596
- `[jp-input-guard] Rule "${rule.name}" is not supported for <${this.kind}>. skipped.`,
766
+ `[text-input-guard] Rule "${rule.name}" is not supported for <${this.kind}>. skipped.`,
597
767
  this.warn
598
768
  );
599
769
  continue;
@@ -678,9 +848,7 @@ class InputGuard {
678
848
  // 連鎖防止(次の処理に持ち越さない)
679
849
  this.revertRequest = null;
680
850
 
681
- if (this.warn) {
682
- console.log(`[jp-input-guard] reverted: ${req.reason}`, req.detail);
683
- }
851
+ if (this.warn) ;
684
852
  }
685
853
 
686
854
  /**
@@ -824,7 +992,7 @@ class InputGuard {
824
992
  * @returns {void}
825
993
  */
826
994
  onCompositionStart() {
827
- console.log("[jp-input-guard] compositionstart");
995
+ // console.log("[text-input-guard] compositionstart");
828
996
  this.composing = true;
829
997
  }
830
998
 
@@ -834,7 +1002,7 @@ class InputGuard {
834
1002
  * @returns {void}
835
1003
  */
836
1004
  onCompositionEnd() {
837
- console.log("[jp-input-guard] compositionend");
1005
+ // console.log("[text-input-guard] compositionend");
838
1006
  this.composing = false;
839
1007
 
840
1008
  // compositionend後に input が来ない環境向けのフォールバック
@@ -854,7 +1022,7 @@ class InputGuard {
854
1022
  * @returns {void}
855
1023
  */
856
1024
  onInput() {
857
- console.log("[jp-input-guard] input");
1025
+ // console.log("[text-input-guard] input");
858
1026
  // compositionend後に input が来た場合、フォールバックを無効化
859
1027
  this.pendingCompositionCommit = false;
860
1028
  this.evaluateInput();
@@ -865,7 +1033,7 @@ class InputGuard {
865
1033
  * @returns {void}
866
1034
  */
867
1035
  onBlur() {
868
- console.log("[jp-input-guard] blur");
1036
+ // console.log("[text-input-guard] blur");
869
1037
  this.evaluateCommit();
870
1038
  }
871
1039
 
@@ -1082,9 +1250,14 @@ class InputGuard {
1082
1250
  * @returns {string}
1083
1251
  */
1084
1252
  getRawValue() {
1085
- if (this.rawElement) {
1086
- return this.rawElement.value;
1087
- }
1253
+ return /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.hostElement).value;
1254
+ }
1255
+
1256
+ /**
1257
+ * 表示用の値を返す(displayの値)
1258
+ * @returns {string}
1259
+ */
1260
+ getDisplayValue() {
1088
1261
  return /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.displayElement).value;
1089
1262
  }
1090
1263
 
@@ -1099,6 +1272,8 @@ class InputGuard {
1099
1272
  isValid: () => this.isValid(),
1100
1273
  getErrors: () => this.getErrors(),
1101
1274
  getRawValue: () => this.getRawValue(),
1275
+ getDisplayValue: () => this.getDisplayValue(),
1276
+ getRawElement: () => /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.hostElement),
1102
1277
  getDisplayElement: () => /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.displayElement)
1103
1278
  };
1104
1279
  }
@@ -1114,6 +1289,63 @@ class InputGuard {
1114
1289
  * The MIT license https://opensource.org/licenses/MIT
1115
1290
  */
1116
1291
 
1292
+ /**
1293
+ * datasetのboolean値を解釈する
1294
+ * - 未指定なら undefined
1295
+ * - "" / "true" / "1" / "yes" / "on" は true
1296
+ * - "false" / "0" / "no" / "off" は false
1297
+ * @param {string|undefined} v
1298
+ * @returns {boolean|undefined}
1299
+ */
1300
+ function parseDatasetBool(v) {
1301
+ if (v == null) { return; }
1302
+ const s = String(v).trim().toLowerCase();
1303
+ if (s === "" || s === "true" || s === "1" || s === "yes" || s === "on") { return true; }
1304
+ if (s === "false" || s === "0" || s === "no" || s === "off") { return false; }
1305
+ return;
1306
+ }
1307
+
1308
+ /**
1309
+ * datasetのnumber値を解釈する(整数想定)
1310
+ * - 未指定/空なら undefined
1311
+ * - 数値でなければ undefined
1312
+ * @param {string|undefined} v
1313
+ * @returns {number|undefined}
1314
+ */
1315
+ function parseDatasetNumber(v) {
1316
+ if (v == null) { return; }
1317
+ const s = String(v).trim();
1318
+ if (s === "") { return; }
1319
+ const n = Number(s);
1320
+ return Number.isFinite(n) ? n : undefined;
1321
+ }
1322
+
1323
+ /**
1324
+ * enumを解釈する(未指定なら undefined)
1325
+ * @template {string} T
1326
+ * @param {string|undefined} v
1327
+ * @param {readonly T[]} allowed
1328
+ * @returns {T|undefined}
1329
+ */
1330
+ function parseDatasetEnum(v, allowed) {
1331
+ if (v == null) { return; }
1332
+ const s = String(v).trim();
1333
+ if (s === "") { return; }
1334
+ // 大文字小文字を区別したいならここを変える(今は厳密一致)
1335
+ return /** @type {T|undefined} */ (allowed.includes(/** @type {any} */ (s)) ? s : undefined);
1336
+ }
1337
+
1338
+ /**
1339
+ * The script is part of TextInputGuard.
1340
+ *
1341
+ * AUTHOR:
1342
+ * natade-jp (https://github.com/natade-jp)
1343
+ *
1344
+ * LICENSE:
1345
+ * The MIT license https://opensource.org/licenses/MIT
1346
+ */
1347
+
1348
+
1117
1349
  /**
1118
1350
  * @typedef {GuardGroup} GuardGroup
1119
1351
  * @typedef {Guard} Guard
@@ -1128,19 +1360,6 @@ class InputGuard {
1128
1360
  * @property {(dataset: DOMStringMap, el: HTMLInputElement|HTMLTextAreaElement) => Rule|null} fromDataset
1129
1361
  */
1130
1362
 
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
1363
  /**
1145
1364
  * separate mode を解釈する(未指定は "auto")
1146
1365
  * @param {string|undefined} v
@@ -1244,7 +1463,7 @@ class InputGuardAutoAttach {
1244
1463
  const options = {};
1245
1464
 
1246
1465
  // warn / invalidClass
1247
- const warn = parseBool(ds.tigWarn);
1466
+ const warn = parseDatasetBool(ds.tigWarn);
1248
1467
  if (warn != null) { options.warn = warn; }
1249
1468
 
1250
1469
  if (ds.tigInvalidClass != null && String(ds.tigInvalidClass).trim() !== "") {
@@ -1264,7 +1483,7 @@ class InputGuardAutoAttach {
1264
1483
  } catch (e) {
1265
1484
  const w = options.warn ?? true;
1266
1485
  if (w) {
1267
- console.warn(`[jp-input-guard] autoAttach: rule "${fac.name}" fromDataset() threw an error.`, e);
1486
+ console.warn(`[text-input-guard] autoAttach: rule "${fac.name}" fromDataset() threw an error.`, e);
1268
1487
  }
1269
1488
  }
1270
1489
  }
@@ -1290,62 +1509,6 @@ class InputGuardAutoAttach {
1290
1509
  }
1291
1510
  }
1292
1511
 
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
1512
  /**
1350
1513
  * The script is part of TextInputGuard.
1351
1514
  *
@@ -1363,6 +1526,7 @@ function parseDatasetEnum(v, allowed) {
1363
1526
  * @property {boolean} [allowFullWidth=true] - 全角数字/記号を許可して半角へ正規化する
1364
1527
  * @property {boolean} [allowMinus=false] - マイナス記号を許可する(先頭のみ)
1365
1528
  * @property {boolean} [allowDecimal=false] - 小数点を許可する(1つだけ)
1529
+ * @property {boolean} [allowEmpty=true] - 空文字を許可するか
1366
1530
  */
1367
1531
 
1368
1532
  /**
@@ -1378,7 +1542,8 @@ function numeric(options = {}) {
1378
1542
  const opt = {
1379
1543
  allowFullWidth: options.allowFullWidth ?? true,
1380
1544
  allowMinus: options.allowMinus ?? false,
1381
- allowDecimal: options.allowDecimal ?? false
1545
+ allowDecimal: options.allowDecimal ?? false,
1546
+ allowEmpty: options.allowEmpty ?? true
1382
1547
  };
1383
1548
 
1384
1549
  /** @type {Set<string>} */
@@ -1536,9 +1701,14 @@ function numeric(options = {}) {
1536
1701
  fix(value) {
1537
1702
  let v = String(value);
1538
1703
 
1704
+ // 空文字の扱い
1705
+ if (v === "") {
1706
+ return opt.allowEmpty ? "" : "0";
1707
+ }
1708
+
1539
1709
  // 未完成な数値は空にする
1540
1710
  if (v === "-" || v === "." || v === "-.") {
1541
- return "";
1711
+ return opt.allowEmpty ? "" : "0";
1542
1712
  }
1543
1713
 
1544
1714
  // "-.1" → "-0.1"
@@ -1610,6 +1780,7 @@ function numeric(options = {}) {
1610
1780
  * - data-tig-rules-numeric-allow-full-width -> dataset.tigRulesNumericAllowFullWidth
1611
1781
  * - data-tig-rules-numeric-allow-minus -> dataset.tigRulesNumericAllowMinus
1612
1782
  * - data-tig-rules-numeric-allow-decimal -> dataset.tigRulesNumericAllowDecimal
1783
+ * - data-tig-rules-numeric-allow-empty -> dataset.tigRulesNumericAllowEmpty
1613
1784
  *
1614
1785
  * @param {DOMStringMap} dataset
1615
1786
  * @param {HTMLInputElement|HTMLTextAreaElement} _el
@@ -1642,6 +1813,12 @@ numeric.fromDataset = function fromDataset(dataset, _el) {
1642
1813
  options.allowDecimal = allowDecimal;
1643
1814
  }
1644
1815
 
1816
+ // data-tig-rules-numeric-allow-empty(未指定なら numeric側デフォルト true)
1817
+ const allowEmpty = parseDatasetBool(dataset.tigRulesNumericAllowEmpty);
1818
+ if (allowEmpty != null) {
1819
+ options.allowEmpty = allowEmpty;
1820
+ }
1821
+
1645
1822
  return numeric(options);
1646
1823
  };
1647
1824
 
@@ -1666,6 +1843,7 @@ numeric.fromDataset = function fromDataset(dataset, _el) {
1666
1843
  * @property {"none"|"truncate"|"round"} [fixFracOnBlur="none"] - blur時の小数部補正
1667
1844
  * @property {"none"|"block"} [overflowInputInt="none"] - 入力中:整数部が最大桁を超える入力をブロックする
1668
1845
  * @property {"none"|"block"} [overflowInputFrac="none"] - 入力中:小数部が最大桁を超える入力をブロックする
1846
+ * @property {boolean} [forceFracOnBlur=false] - blur時に小数部を必ず表示(frac桁まで0埋め)
1669
1847
  */
1670
1848
 
1671
1849
  /**
@@ -1806,7 +1984,8 @@ function digits(options = {}) {
1806
1984
  fixIntOnBlur: options.fixIntOnBlur ?? "none",
1807
1985
  fixFracOnBlur: options.fixFracOnBlur ?? "none",
1808
1986
  overflowInputInt: options.overflowInputInt ?? "none",
1809
- overflowInputFrac: options.overflowInputFrac ?? "none"
1987
+ overflowInputFrac: options.overflowInputFrac ?? "none",
1988
+ forceFracOnBlur: options.forceFracOnBlur ?? false
1810
1989
  };
1811
1990
 
1812
1991
  return {
@@ -1923,14 +2102,39 @@ function digits(options = {}) {
1923
2102
  }
1924
2103
  }
1925
2104
 
1926
- // 組み立て(frac=0 のときは "." を残すか?は方針次第だが、ここでは消す)
1927
- if (!hasDot || typeof opt.frac !== "number") {
2105
+ if (opt.forceFracOnBlur && typeof opt.frac === "number" && opt.frac > 0) {
2106
+ const limit = opt.frac;
2107
+ // "." が無いなら作る(12 → 12.00)
2108
+ if (!hasDot) {
2109
+ fracPart = "";
2110
+ }
2111
+ // 足りない分を 0 埋め(12.3 → 12.30 / 12. → 12.00)
2112
+ const f = fracPart ?? "";
2113
+ if (f.length < limit) {
2114
+ fracPart = f + "0".repeat(limit - f.length);
2115
+ }
2116
+ }
2117
+
2118
+ // 組み立て
2119
+ if (typeof opt.frac !== "number") {
2120
+ // frac未指定なら、dot があっても digits は触らず intだけ返す方針(現状維持)
1928
2121
  return `${sign}${intPart}`;
1929
2122
  }
2123
+
1930
2124
  if (opt.frac === 0) {
2125
+ // 小数0桁なら常に整数表示
1931
2126
  return `${sign}${intPart}`;
1932
2127
  }
1933
- return `${sign}${intPart}.${fracPart}`;
2128
+
2129
+ // frac 指定あり(1以上)
2130
+ if (hasDot || (opt.forceFracOnBlur && opt.frac > 0)) {
2131
+ // "." が無いけど forceFracOnBlur の場合もここに来る
2132
+ const f = fracPart ?? "";
2133
+ return `${sign}${intPart}.${f}`;
2134
+ }
2135
+
2136
+ // "." が無くて force もしないなら整数表示
2137
+ return `${sign}${intPart}`;
1934
2138
  }
1935
2139
  };
1936
2140
  }
@@ -1949,6 +2153,7 @@ function digits(options = {}) {
1949
2153
  * - data-tig-rules-digits-fix-frac-on-blur -> dataset.tigRulesDigitsFixFracOnBlur
1950
2154
  * - data-tig-rules-digits-overflow-input-int -> dataset.tigRulesDigitsOverflowInputInt
1951
2155
  * - data-tig-rules-digits-overflow-input-frac -> dataset.tigRulesDigitsOverflowInputFrac
2156
+ * - data-tig-rules-digits-force-frac-on-blur -> dataset.tigRulesDigitsForceFracOnBlur
1952
2157
  *
1953
2158
  * @param {DOMStringMap} dataset
1954
2159
  * @param {HTMLInputElement|HTMLTextAreaElement} _el
@@ -2011,6 +2216,12 @@ digits.fromDataset = function fromDataset(dataset, _el) {
2011
2216
  options.overflowInputFrac = ovFrac;
2012
2217
  }
2013
2218
 
2219
+ // forceFracOnBlur
2220
+ const forceFrac = parseDatasetBool(dataset.tigRulesDigitsForceFracOnBlur);
2221
+ if (forceFrac != null) {
2222
+ options.forceFracOnBlur = forceFrac;
2223
+ }
2224
+
2014
2225
  return digits(options);
2015
2226
  };
2016
2227
 
@@ -2036,6 +2247,11 @@ function comma() {
2036
2247
 
2037
2248
  /**
2038
2249
  * 表示整形(確定時のみ)
2250
+ *
2251
+ * 前提:
2252
+ * - numeric / digits 等で正規化済みの数値文字列が渡される
2253
+ * - 整数部・小数部・符号のみを含む(カンマは含まない想定)
2254
+ *
2039
2255
  * @param {string} value
2040
2256
  * @returns {string}
2041
2257
  */
@@ -2123,10 +2339,10 @@ const rules = {
2123
2339
 
2124
2340
  /**
2125
2341
  * バージョン(ビルド時に置換したいならここを差し替える)
2126
- * 例: rollup replace で ""0.0.1"" を package.json の version に置換
2342
+ * 例: rollup replace で ""0.1.0"" を package.json の version に置換
2127
2343
  */
2128
2344
  // @ts-ignore
2129
2345
  // eslint-disable-next-line no-undef
2130
- const version = "0.0.1" ;
2346
+ const version = "0.1.0" ;
2131
2347
 
2132
2348
  export { attach, attachAll, autoAttach, comma, digits, numeric, rules, version };