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.
@@ -4,6 +4,249 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.TextInputGuard = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
+ /**
8
+ * The script is part of TextInputGuard.
9
+ *
10
+ * AUTHOR:
11
+ * natade-jp (https://github.com/natade-jp)
12
+ *
13
+ * LICENSE:
14
+ * The MIT license https://opensource.org/licenses/MIT
15
+ */
16
+
17
+ /**
18
+ * SwapState
19
+ *
20
+ * separateValue.mode="swap" のときに使用する
21
+ * 元 input 要素の状態スナップショットおよび復元ロジックを管理するクラス
22
+ *
23
+ * 役割
24
+ * - swap前の input の属性状態を保持する
25
+ * - raw化および display生成時に必要な属性を適用する
26
+ * - detach時に元の状態へ復元する
27
+ *
28
+ * 設計方針
29
+ * - 送信用属性は raw に残す
30
+ * - UIおよびアクセシビリティ属性は display に適用する
31
+ * - tig内部用の data-* は display にコピーしない
32
+ */
33
+ class SwapState {
34
+ /**
35
+ * 元 input の type 属性
36
+ * detach時に復元するため保持する
37
+ * @type {string}
38
+ */
39
+ originalType;
40
+
41
+ /**
42
+ * 元 input の id 属性
43
+ * swap時に display へ移し detach時に rawへ戻す
44
+ * @type {string|null}
45
+ */
46
+ originalId;
47
+
48
+ /**
49
+ * 元 input の name 属性
50
+ * 送信用属性のため raw側に残すが
51
+ * detach時の整合性のため保持する
52
+ * @type {string|null}
53
+ */
54
+ originalName;
55
+
56
+ /**
57
+ * 元 input の class 属性
58
+ * swap時に display へ移す
59
+ * @type {string}
60
+ */
61
+ originalClass;
62
+
63
+ /**
64
+ * UI系属性のスナップショット
65
+ * placeholder inputmode required などを保持する
66
+ *
67
+ * key 属性名
68
+ * value 属性値 未指定の場合は null
69
+ *
70
+ * @type {Object.<string, string|null>}
71
+ */
72
+ originalUiAttrs;
73
+
74
+ /**
75
+ * aria-* 属性のスナップショット
76
+ * アクセシビリティ維持のため display に適用する
77
+ *
78
+ * key aria属性名 例 aria-label
79
+ * value 属性値
80
+ *
81
+ * @type {Object.<string, string>}
82
+ */
83
+ originalAriaAttrs;
84
+
85
+ /**
86
+ * tig 以外の data-* 属性のスナップショット
87
+ * swap後も display へ引き継ぐ
88
+ *
89
+ * key datasetキー camelCase
90
+ * value 属性値
91
+ *
92
+ * @type {Object.<string, string>}
93
+ */
94
+ originalDataset;
95
+
96
+ /**
97
+ * swap時に生成された display 用 input 要素
98
+ * detach時に削除するため保持する
99
+ *
100
+ * @type {HTMLInputElement|null}
101
+ */
102
+ createdDisplay;
103
+
104
+ /**
105
+ * @param {HTMLInputElement} input
106
+ * swap前の元 input 要素
107
+ */
108
+ constructor(input) {
109
+ this.originalType = input.type;
110
+ this.originalId = input.getAttribute("id");
111
+ this.originalName = input.getAttribute("name");
112
+ this.originalClass = input.className;
113
+
114
+ this.originalUiAttrs = {};
115
+ this.originalAriaAttrs = {};
116
+ this.originalDataset = {};
117
+ this.createdDisplay = null;
118
+
119
+ const UI_ATTRS = [
120
+ "placeholder",
121
+ "inputmode",
122
+ "autocomplete",
123
+ "required",
124
+ "minlength",
125
+ "maxlength",
126
+ "pattern",
127
+ "title",
128
+ "tabindex"
129
+ ];
130
+
131
+ for (const name of UI_ATTRS) {
132
+ this.originalUiAttrs[name] =
133
+ input.hasAttribute(name) ? input.getAttribute(name) : null;
134
+ }
135
+
136
+ for (const attr of input.attributes) {
137
+ if (attr.name.startsWith("aria-")) {
138
+ this.originalAriaAttrs[attr.name] = attr.value ?? "";
139
+ }
140
+ }
141
+
142
+ for (const [k, v] of Object.entries(input.dataset)) {
143
+ if (k.startsWith("tig")) { continue; }
144
+ this.originalDataset[k] = v;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * raw 元input を hidden 化する
150
+ * 送信担当要素として扱う
151
+ *
152
+ * @param {HTMLInputElement} input
153
+ * @returns {void}
154
+ */
155
+ applyToRaw(input) {
156
+ // raw化(送信担当)
157
+ input.type = "hidden";
158
+ input.removeAttribute("id");
159
+ input.className = "";
160
+ input.dataset.tigRole = "raw";
161
+
162
+ // 元idのメタを残す(デバッグ/参照用)
163
+ if (this.originalId) {
164
+ input.dataset.tigOriginalId = this.originalId;
165
+ }
166
+ if (this.originalName) {
167
+ input.dataset.tigOriginalName = this.originalName;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * display用 input を生成し UI属性 aria属性 data属性を適用
173
+ *
174
+ * @param {HTMLInputElement} raw hidden化された元input
175
+ * @returns {HTMLInputElement}
176
+ */
177
+ createDisplay(raw) {
178
+ const display = document.createElement("input");
179
+ display.type = "text";
180
+ display.dataset.tigRole = "display";
181
+
182
+ if (this.originalId) {
183
+ display.id = this.originalId;
184
+ }
185
+
186
+ display.className = this.originalClass ?? "";
187
+ display.value = raw.value;
188
+
189
+ for (const [name, v] of Object.entries(this.originalUiAttrs)) {
190
+ if (v == null) {
191
+ display.removeAttribute(name);
192
+ } else {
193
+ display.setAttribute(name, v);
194
+ }
195
+ }
196
+
197
+ for (const [name, v] of Object.entries(this.originalAriaAttrs)) {
198
+ display.setAttribute(name, v);
199
+ }
200
+
201
+ for (const [k, v] of Object.entries(this.originalDataset)) {
202
+ display.dataset[k] = v;
203
+ }
204
+
205
+ this.createdDisplay = display;
206
+ return display;
207
+ }
208
+
209
+ /**
210
+ * detach時に display 要素を削除する
211
+ *
212
+ * @returns {void}
213
+ */
214
+ removeDisplay() {
215
+ if (this.createdDisplay?.parentNode) {
216
+ this.createdDisplay.parentNode.removeChild(this.createdDisplay);
217
+ }
218
+ this.createdDisplay = null;
219
+ }
220
+
221
+ /**
222
+ * raw hidden化された元input を元の状態へ復元する
223
+ *
224
+ * @param {HTMLInputElement} raw
225
+ * @returns {void}
226
+ */
227
+ restoreRaw(raw) {
228
+ raw.type = this.originalType;
229
+
230
+ if (this.originalId) {
231
+ raw.setAttribute("id", this.originalId);
232
+ } else {
233
+ raw.removeAttribute("id");
234
+ }
235
+
236
+ if (this.originalName) {
237
+ raw.setAttribute("name", this.originalName);
238
+ } else {
239
+ raw.removeAttribute("name");
240
+ }
241
+
242
+ raw.className = this.originalClass ?? "";
243
+
244
+ delete raw.dataset.tigRole;
245
+ delete raw.dataset.tigOriginalId;
246
+ delete raw.dataset.tigOriginalName;
247
+ }
248
+ }
249
+
7
250
  /**
8
251
  * The script is part of JPInputGuard.
9
252
  *
@@ -14,6 +257,7 @@
14
257
  * The MIT license https://opensource.org/licenses/MIT
15
258
  */
16
259
 
260
+
17
261
  /**
18
262
  * 対象要素の種別(現在は input と textarea のみ対応)
19
263
  * @typedef {"input"|"textarea"} ElementKind
@@ -41,7 +285,9 @@
41
285
  * @property {() => boolean} isValid - 現在エラーが無いかどうか
42
286
  * @property {() => TigError[]} getErrors - エラー一覧を取得
43
287
  * @property {() => string} getRawValue - 送信用の正規化済み値を取得
44
- * @property {() => HTMLInputElement|HTMLTextAreaElement} getDisplayElement - ユーザーが実際に操作している要素(swap時はdisplay側)
288
+ * @property {() => string} getDisplayValue - ユーザーが実際に操作している要素の値を取得
289
+ * @property {() => HTMLInputElement|HTMLTextAreaElement} getRawElement - 送信用の正規化済み値の要素
290
+ * @property {() => HTMLInputElement|HTMLTextAreaElement} getDisplayElement - ユーザーが実際に操作している要素(swap時はdisplay専用)
45
291
  */
46
292
 
47
293
  /**
@@ -90,17 +336,6 @@
90
336
  * @property {SeparateValueOptions} [separateValue] - 表示値と内部値の分離設定
91
337
  */
92
338
 
93
- /**
94
- * swap時に退避する元inputの情報
95
- * detach時に元の状態へ復元するために使用する
96
- * @typedef {Object} SwapState
97
- * @property {string} originalType - 元のinput.type
98
- * @property {string|null} originalId - 元のid属性
99
- * @property {string|null} originalName - 元のname属性
100
- * @property {string} originalClass - 元のclass文字列
101
- * @property {HTMLInputElement} createdDisplay - 生成した表示用input
102
- */
103
-
104
339
  /**
105
340
  * selection(カーソル/選択範囲)の退避情報
106
341
  * @typedef {Object} SelectionState
@@ -206,7 +441,7 @@
206
441
 
207
442
  const kind = detectKind(element);
208
443
  if (!kind) {
209
- throw new TypeError("[jp-input-guard] attach() expects an <input> or <textarea> element.");
444
+ throw new TypeError("[text-input-guard] attach() expects an <input> or <textarea> element.");
210
445
  }
211
446
 
212
447
  /**
@@ -438,67 +673,25 @@
438
673
  }
439
674
 
440
675
  if (this.kind !== "input") {
441
- warnLog('[jp-input-guard] separateValue.mode="swap" is not supported for <textarea>. ignored.', this.warn);
676
+ warnLog('[text-input-guard] separateValue.mode="swap" is not supported for <textarea>. ignored.', this.warn);
442
677
  return;
443
678
  }
444
679
 
445
680
  const input = /** @type {HTMLInputElement} */ (this.originalElement);
446
681
 
447
- // 退避(detachで戻すため)
448
- /** @type {SwapState} */
449
- this.swapState = {
450
- originalType: input.type,
451
- originalId: input.getAttribute("id"),
452
- originalName: input.getAttribute("name"),
453
- originalClass: input.className,
454
- // 必要になったらここに placeholder/aria/data を追加していく
455
- createdDisplay: null
456
- };
457
-
458
- // raw化(送信担当)
459
- input.type = "hidden";
460
- input.removeAttribute("id"); // displayに引き継ぐため
461
- input.dataset.tigRole = "raw";
462
-
463
- // 元idのメタを残す(デバッグ/参照用)
464
- if (this.swapState.originalId) {
465
- input.dataset.tigOriginalId = this.swapState.originalId;
466
- }
467
-
468
- if (this.swapState.originalName) {
469
- input.dataset.tigOriginalName = this.swapState.originalName;
470
- }
471
-
472
- // display生成(ユーザー入力担当)
473
- const display = document.createElement("input");
474
- display.type = "text";
475
- display.dataset.tigRole = "display";
476
-
477
- // id は display に移す
478
- if (this.swapState.originalId) {
479
- display.id = this.swapState.originalId;
480
- }
481
-
482
- // name は付けない(送信しない)
483
- display.removeAttribute("name");
682
+ const state = new SwapState(input);
683
+ state.applyToRaw(input);
484
684
 
485
- // class display に
486
- display.className = this.swapState.originalClass;
487
- input.className = "";
488
-
489
- // value 初期同期
490
- display.value = input.value;
491
-
492
- // DOMに挿入(rawの直後)
685
+ const display = state.createDisplay(input);
493
686
  input.after(display);
494
687
 
688
+ this.swapState = state;
689
+
495
690
  // elements更新
496
- this.hostElement = input;
497
- this.displayElement = display;
691
+ this.hostElement = input; // raw
692
+ this.displayElement = display; // display
498
693
  this.rawElement = input;
499
694
 
500
- this.swapState.createdDisplay = display;
501
-
502
695
  // revert 機構
503
696
  this.lastAcceptedValue = display.value;
504
697
  this.lastAcceptedSelection = this.readSelection(display);
@@ -518,10 +711,10 @@
518
711
 
519
712
  // rawは元の input(hidden化されている)
520
713
  const raw = /** @type {HTMLInputElement} */ (this.hostElement);
521
- const display = state.createdDisplay;
522
714
 
523
715
  // displayが存在するなら、最新表示値をrawに同期してから消す(安全策)
524
716
  // ※ rawは常に正規化済みを持つ設計だけど、念のため
717
+ const display = state.createdDisplay;
525
718
  if (display) {
526
719
  try {
527
720
  raw.value = raw.value || display.value;
@@ -531,39 +724,18 @@
531
724
  }
532
725
 
533
726
  // display削除
534
- if (display && display.parentNode) {
535
- display.parentNode.removeChild(display);
536
- }
727
+ state.removeDisplay();
537
728
 
538
729
  // rawを元に戻す(type)
539
- raw.type = state.originalType;
540
-
541
- // id を戻す
542
- if (state.originalId) {
543
- raw.setAttribute("id", state.originalId);
544
- } else {
545
- raw.removeAttribute("id");
546
- }
547
-
548
- // name を戻す(swap中は残している想定だが、念のため)
549
- if (state.originalName) {
550
- raw.setAttribute("name", state.originalName);
551
- } else {
552
- raw.removeAttribute("name");
553
- }
554
-
555
- // class を戻す
556
- raw.className = state.originalClass ?? "";
557
-
558
- // data属性(tig用)は消しておく
559
- delete raw.dataset.tigRole;
560
- delete raw.dataset.tigOriginalId;
561
- delete raw.dataset.tigOriginalName;
730
+ state.restoreRaw(raw);
562
731
 
563
732
  // elements参照を original に戻す
564
733
  this.hostElement = this.originalElement;
565
734
  this.displayElement = this.originalElement;
566
735
  this.rawElement = null;
736
+
737
+ // swapState破棄
738
+ this.swapState = null;
567
739
  }
568
740
 
569
741
  /**
@@ -575,8 +747,6 @@
575
747
  this.unbindEvents();
576
748
  // swap復元
577
749
  this.restoreSeparateValue();
578
- // swapState破棄
579
- this.swapState = null;
580
750
  // 以後このインスタンスは利用不能にしてもいいが、今回は明示しない
581
751
  }
582
752
 
@@ -599,7 +769,7 @@
599
769
 
600
770
  if (!supports) {
601
771
  warnLog(
602
- `[jp-input-guard] Rule "${rule.name}" is not supported for <${this.kind}>. skipped.`,
772
+ `[text-input-guard] Rule "${rule.name}" is not supported for <${this.kind}>. skipped.`,
603
773
  this.warn
604
774
  );
605
775
  continue;
@@ -684,9 +854,7 @@
684
854
  // 連鎖防止(次の処理に持ち越さない)
685
855
  this.revertRequest = null;
686
856
 
687
- if (this.warn) {
688
- console.log(`[jp-input-guard] reverted: ${req.reason}`, req.detail);
689
- }
857
+ if (this.warn) ;
690
858
  }
691
859
 
692
860
  /**
@@ -830,7 +998,7 @@
830
998
  * @returns {void}
831
999
  */
832
1000
  onCompositionStart() {
833
- console.log("[jp-input-guard] compositionstart");
1001
+ // console.log("[text-input-guard] compositionstart");
834
1002
  this.composing = true;
835
1003
  }
836
1004
 
@@ -840,7 +1008,7 @@
840
1008
  * @returns {void}
841
1009
  */
842
1010
  onCompositionEnd() {
843
- console.log("[jp-input-guard] compositionend");
1011
+ // console.log("[text-input-guard] compositionend");
844
1012
  this.composing = false;
845
1013
 
846
1014
  // compositionend後に input が来ない環境向けのフォールバック
@@ -860,7 +1028,7 @@
860
1028
  * @returns {void}
861
1029
  */
862
1030
  onInput() {
863
- console.log("[jp-input-guard] input");
1031
+ // console.log("[text-input-guard] input");
864
1032
  // compositionend後に input が来た場合、フォールバックを無効化
865
1033
  this.pendingCompositionCommit = false;
866
1034
  this.evaluateInput();
@@ -871,7 +1039,7 @@
871
1039
  * @returns {void}
872
1040
  */
873
1041
  onBlur() {
874
- console.log("[jp-input-guard] blur");
1042
+ // console.log("[text-input-guard] blur");
875
1043
  this.evaluateCommit();
876
1044
  }
877
1045
 
@@ -1088,9 +1256,14 @@
1088
1256
  * @returns {string}
1089
1257
  */
1090
1258
  getRawValue() {
1091
- if (this.rawElement) {
1092
- return this.rawElement.value;
1093
- }
1259
+ return /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.hostElement).value;
1260
+ }
1261
+
1262
+ /**
1263
+ * 表示用の値を返す(displayの値)
1264
+ * @returns {string}
1265
+ */
1266
+ getDisplayValue() {
1094
1267
  return /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.displayElement).value;
1095
1268
  }
1096
1269
 
@@ -1105,6 +1278,8 @@
1105
1278
  isValid: () => this.isValid(),
1106
1279
  getErrors: () => this.getErrors(),
1107
1280
  getRawValue: () => this.getRawValue(),
1281
+ getDisplayValue: () => this.getDisplayValue(),
1282
+ getRawElement: () => /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.hostElement),
1108
1283
  getDisplayElement: () => /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.displayElement)
1109
1284
  };
1110
1285
  }
@@ -1120,6 +1295,63 @@
1120
1295
  * The MIT license https://opensource.org/licenses/MIT
1121
1296
  */
1122
1297
 
1298
+ /**
1299
+ * datasetのboolean値を解釈する
1300
+ * - 未指定なら undefined
1301
+ * - "" / "true" / "1" / "yes" / "on" は true
1302
+ * - "false" / "0" / "no" / "off" は false
1303
+ * @param {string|undefined} v
1304
+ * @returns {boolean|undefined}
1305
+ */
1306
+ function parseDatasetBool(v) {
1307
+ if (v == null) { return; }
1308
+ const s = String(v).trim().toLowerCase();
1309
+ if (s === "" || s === "true" || s === "1" || s === "yes" || s === "on") { return true; }
1310
+ if (s === "false" || s === "0" || s === "no" || s === "off") { return false; }
1311
+ return;
1312
+ }
1313
+
1314
+ /**
1315
+ * datasetのnumber値を解釈する(整数想定)
1316
+ * - 未指定/空なら undefined
1317
+ * - 数値でなければ undefined
1318
+ * @param {string|undefined} v
1319
+ * @returns {number|undefined}
1320
+ */
1321
+ function parseDatasetNumber(v) {
1322
+ if (v == null) { return; }
1323
+ const s = String(v).trim();
1324
+ if (s === "") { return; }
1325
+ const n = Number(s);
1326
+ return Number.isFinite(n) ? n : undefined;
1327
+ }
1328
+
1329
+ /**
1330
+ * enumを解釈する(未指定なら undefined)
1331
+ * @template {string} T
1332
+ * @param {string|undefined} v
1333
+ * @param {readonly T[]} allowed
1334
+ * @returns {T|undefined}
1335
+ */
1336
+ function parseDatasetEnum(v, allowed) {
1337
+ if (v == null) { return; }
1338
+ const s = String(v).trim();
1339
+ if (s === "") { return; }
1340
+ // 大文字小文字を区別したいならここを変える(今は厳密一致)
1341
+ return /** @type {T|undefined} */ (allowed.includes(/** @type {any} */ (s)) ? s : undefined);
1342
+ }
1343
+
1344
+ /**
1345
+ * The script is part of TextInputGuard.
1346
+ *
1347
+ * AUTHOR:
1348
+ * natade-jp (https://github.com/natade-jp)
1349
+ *
1350
+ * LICENSE:
1351
+ * The MIT license https://opensource.org/licenses/MIT
1352
+ */
1353
+
1354
+
1123
1355
  /**
1124
1356
  * @typedef {GuardGroup} GuardGroup
1125
1357
  * @typedef {Guard} Guard
@@ -1134,19 +1366,6 @@
1134
1366
  * @property {(dataset: DOMStringMap, el: HTMLInputElement|HTMLTextAreaElement) => Rule|null} fromDataset
1135
1367
  */
1136
1368
 
1137
- /**
1138
- * Boolean系のdata値を解釈する(未指定なら undefined を返す)
1139
- * @param {string|undefined} v
1140
- * @returns {boolean|undefined}
1141
- */
1142
- function parseBool(v) {
1143
- if (v == null) { return; }
1144
- const s = String(v).trim().toLowerCase();
1145
- if (s === "" || s === "true" || s === "1" || s === "yes" || s === "on") { return true; }
1146
- if (s === "false" || s === "0" || s === "no" || s === "off") { return false; }
1147
- return;
1148
- }
1149
-
1150
1369
  /**
1151
1370
  * separate mode を解釈する(未指定は "auto")
1152
1371
  * @param {string|undefined} v
@@ -1250,7 +1469,7 @@
1250
1469
  const options = {};
1251
1470
 
1252
1471
  // warn / invalidClass
1253
- const warn = parseBool(ds.tigWarn);
1472
+ const warn = parseDatasetBool(ds.tigWarn);
1254
1473
  if (warn != null) { options.warn = warn; }
1255
1474
 
1256
1475
  if (ds.tigInvalidClass != null && String(ds.tigInvalidClass).trim() !== "") {
@@ -1270,7 +1489,7 @@
1270
1489
  } catch (e) {
1271
1490
  const w = options.warn ?? true;
1272
1491
  if (w) {
1273
- console.warn(`[jp-input-guard] autoAttach: rule "${fac.name}" fromDataset() threw an error.`, e);
1492
+ console.warn(`[text-input-guard] autoAttach: rule "${fac.name}" fromDataset() threw an error.`, e);
1274
1493
  }
1275
1494
  }
1276
1495
  }
@@ -1296,62 +1515,6 @@
1296
1515
  }
1297
1516
  }
1298
1517
 
1299
- /**
1300
- * The script is part of TextInputGuard.
1301
- *
1302
- * AUTHOR:
1303
- * natade-jp (https://github.com/natade-jp)
1304
- *
1305
- * LICENSE:
1306
- * The MIT license https://opensource.org/licenses/MIT
1307
- */
1308
-
1309
- /**
1310
- * datasetのboolean値を解釈する
1311
- * - 未指定なら undefined
1312
- * - "" / "true" / "1" / "yes" / "on" は true
1313
- * - "false" / "0" / "no" / "off" は false
1314
- * @param {string|undefined} v
1315
- * @returns {boolean|undefined}
1316
- */
1317
- function parseDatasetBool(v) {
1318
- if (v == null) { return; }
1319
- const s = String(v).trim().toLowerCase();
1320
- if (s === "" || s === "true" || s === "1" || s === "yes" || s === "on") { return true; }
1321
- if (s === "false" || s === "0" || s === "no" || s === "off") { return false; }
1322
- return;
1323
- }
1324
-
1325
- /**
1326
- * datasetのnumber値を解釈する(整数想定)
1327
- * - 未指定/空なら undefined
1328
- * - 数値でなければ undefined
1329
- * @param {string|undefined} v
1330
- * @returns {number|undefined}
1331
- */
1332
- function parseDatasetNumber(v) {
1333
- if (v == null) { return; }
1334
- const s = String(v).trim();
1335
- if (s === "") { return; }
1336
- const n = Number(s);
1337
- return Number.isFinite(n) ? n : undefined;
1338
- }
1339
-
1340
- /**
1341
- * enumを解釈する(未指定なら undefined)
1342
- * @template {string} T
1343
- * @param {string|undefined} v
1344
- * @param {readonly T[]} allowed
1345
- * @returns {T|undefined}
1346
- */
1347
- function parseDatasetEnum(v, allowed) {
1348
- if (v == null) { return; }
1349
- const s = String(v).trim();
1350
- if (s === "") { return; }
1351
- // 大文字小文字を区別したいならここを変える(今は厳密一致)
1352
- return /** @type {T|undefined} */ (allowed.includes(/** @type {any} */ (s)) ? s : undefined);
1353
- }
1354
-
1355
1518
  /**
1356
1519
  * The script is part of TextInputGuard.
1357
1520
  *
@@ -1369,6 +1532,7 @@
1369
1532
  * @property {boolean} [allowFullWidth=true] - 全角数字/記号を許可して半角へ正規化する
1370
1533
  * @property {boolean} [allowMinus=false] - マイナス記号を許可する(先頭のみ)
1371
1534
  * @property {boolean} [allowDecimal=false] - 小数点を許可する(1つだけ)
1535
+ * @property {boolean} [allowEmpty=true] - 空文字を許可するか
1372
1536
  */
1373
1537
 
1374
1538
  /**
@@ -1384,7 +1548,8 @@
1384
1548
  const opt = {
1385
1549
  allowFullWidth: options.allowFullWidth ?? true,
1386
1550
  allowMinus: options.allowMinus ?? false,
1387
- allowDecimal: options.allowDecimal ?? false
1551
+ allowDecimal: options.allowDecimal ?? false,
1552
+ allowEmpty: options.allowEmpty ?? true
1388
1553
  };
1389
1554
 
1390
1555
  /** @type {Set<string>} */
@@ -1542,9 +1707,14 @@
1542
1707
  fix(value) {
1543
1708
  let v = String(value);
1544
1709
 
1710
+ // 空文字の扱い
1711
+ if (v === "") {
1712
+ return opt.allowEmpty ? "" : "0";
1713
+ }
1714
+
1545
1715
  // 未完成な数値は空にする
1546
1716
  if (v === "-" || v === "." || v === "-.") {
1547
- return "";
1717
+ return opt.allowEmpty ? "" : "0";
1548
1718
  }
1549
1719
 
1550
1720
  // "-.1" → "-0.1"
@@ -1616,6 +1786,7 @@
1616
1786
  * - data-tig-rules-numeric-allow-full-width -> dataset.tigRulesNumericAllowFullWidth
1617
1787
  * - data-tig-rules-numeric-allow-minus -> dataset.tigRulesNumericAllowMinus
1618
1788
  * - data-tig-rules-numeric-allow-decimal -> dataset.tigRulesNumericAllowDecimal
1789
+ * - data-tig-rules-numeric-allow-empty -> dataset.tigRulesNumericAllowEmpty
1619
1790
  *
1620
1791
  * @param {DOMStringMap} dataset
1621
1792
  * @param {HTMLInputElement|HTMLTextAreaElement} _el
@@ -1648,6 +1819,12 @@
1648
1819
  options.allowDecimal = allowDecimal;
1649
1820
  }
1650
1821
 
1822
+ // data-tig-rules-numeric-allow-empty(未指定なら numeric側デフォルト true)
1823
+ const allowEmpty = parseDatasetBool(dataset.tigRulesNumericAllowEmpty);
1824
+ if (allowEmpty != null) {
1825
+ options.allowEmpty = allowEmpty;
1826
+ }
1827
+
1651
1828
  return numeric(options);
1652
1829
  };
1653
1830
 
@@ -1672,6 +1849,7 @@
1672
1849
  * @property {"none"|"truncate"|"round"} [fixFracOnBlur="none"] - blur時の小数部補正
1673
1850
  * @property {"none"|"block"} [overflowInputInt="none"] - 入力中:整数部が最大桁を超える入力をブロックする
1674
1851
  * @property {"none"|"block"} [overflowInputFrac="none"] - 入力中:小数部が最大桁を超える入力をブロックする
1852
+ * @property {boolean} [forceFracOnBlur=false] - blur時に小数部を必ず表示(frac桁まで0埋め)
1675
1853
  */
1676
1854
 
1677
1855
  /**
@@ -1812,7 +1990,8 @@
1812
1990
  fixIntOnBlur: options.fixIntOnBlur ?? "none",
1813
1991
  fixFracOnBlur: options.fixFracOnBlur ?? "none",
1814
1992
  overflowInputInt: options.overflowInputInt ?? "none",
1815
- overflowInputFrac: options.overflowInputFrac ?? "none"
1993
+ overflowInputFrac: options.overflowInputFrac ?? "none",
1994
+ forceFracOnBlur: options.forceFracOnBlur ?? false
1816
1995
  };
1817
1996
 
1818
1997
  return {
@@ -1929,14 +2108,39 @@
1929
2108
  }
1930
2109
  }
1931
2110
 
1932
- // 組み立て(frac=0 のときは "." を残すか?は方針次第だが、ここでは消す)
1933
- if (!hasDot || typeof opt.frac !== "number") {
2111
+ if (opt.forceFracOnBlur && typeof opt.frac === "number" && opt.frac > 0) {
2112
+ const limit = opt.frac;
2113
+ // "." が無いなら作る(12 → 12.00)
2114
+ if (!hasDot) {
2115
+ fracPart = "";
2116
+ }
2117
+ // 足りない分を 0 埋め(12.3 → 12.30 / 12. → 12.00)
2118
+ const f = fracPart ?? "";
2119
+ if (f.length < limit) {
2120
+ fracPart = f + "0".repeat(limit - f.length);
2121
+ }
2122
+ }
2123
+
2124
+ // 組み立て
2125
+ if (typeof opt.frac !== "number") {
2126
+ // frac未指定なら、dot があっても digits は触らず intだけ返す方針(現状維持)
1934
2127
  return `${sign}${intPart}`;
1935
2128
  }
2129
+
1936
2130
  if (opt.frac === 0) {
2131
+ // 小数0桁なら常に整数表示
1937
2132
  return `${sign}${intPart}`;
1938
2133
  }
1939
- return `${sign}${intPart}.${fracPart}`;
2134
+
2135
+ // frac 指定あり(1以上)
2136
+ if (hasDot || (opt.forceFracOnBlur && opt.frac > 0)) {
2137
+ // "." が無いけど forceFracOnBlur の場合もここに来る
2138
+ const f = fracPart ?? "";
2139
+ return `${sign}${intPart}.${f}`;
2140
+ }
2141
+
2142
+ // "." が無くて force もしないなら整数表示
2143
+ return `${sign}${intPart}`;
1940
2144
  }
1941
2145
  };
1942
2146
  }
@@ -1955,6 +2159,7 @@
1955
2159
  * - data-tig-rules-digits-fix-frac-on-blur -> dataset.tigRulesDigitsFixFracOnBlur
1956
2160
  * - data-tig-rules-digits-overflow-input-int -> dataset.tigRulesDigitsOverflowInputInt
1957
2161
  * - data-tig-rules-digits-overflow-input-frac -> dataset.tigRulesDigitsOverflowInputFrac
2162
+ * - data-tig-rules-digits-force-frac-on-blur -> dataset.tigRulesDigitsForceFracOnBlur
1958
2163
  *
1959
2164
  * @param {DOMStringMap} dataset
1960
2165
  * @param {HTMLInputElement|HTMLTextAreaElement} _el
@@ -2017,6 +2222,12 @@
2017
2222
  options.overflowInputFrac = ovFrac;
2018
2223
  }
2019
2224
 
2225
+ // forceFracOnBlur
2226
+ const forceFrac = parseDatasetBool(dataset.tigRulesDigitsForceFracOnBlur);
2227
+ if (forceFrac != null) {
2228
+ options.forceFracOnBlur = forceFrac;
2229
+ }
2230
+
2020
2231
  return digits(options);
2021
2232
  };
2022
2233
 
@@ -2042,6 +2253,11 @@
2042
2253
 
2043
2254
  /**
2044
2255
  * 表示整形(確定時のみ)
2256
+ *
2257
+ * 前提:
2258
+ * - numeric / digits 等で正規化済みの数値文字列が渡される
2259
+ * - 整数部・小数部・符号のみを含む(カンマは含まない想定)
2260
+ *
2045
2261
  * @param {string} value
2046
2262
  * @returns {string}
2047
2263
  */
@@ -2129,11 +2345,11 @@
2129
2345
 
2130
2346
  /**
2131
2347
  * バージョン(ビルド時に置換したいならここを差し替える)
2132
- * 例: rollup replace で ""0.0.1"" を package.json の version に置換
2348
+ * 例: rollup replace で ""0.1.0"" を package.json の version に置換
2133
2349
  */
2134
2350
  // @ts-ignore
2135
2351
  // eslint-disable-next-line no-undef
2136
- const version = "0.0.1" ;
2352
+ const version = "0.1.0" ;
2137
2353
 
2138
2354
  exports.attach = attach;
2139
2355
  exports.attachAll = attachAll;