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