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.
- package/README.md +3 -129
- package/dist/cjs/text-input-guard.cjs +398 -182
- package/dist/cjs/text-input-guard.min.cjs +6 -0
- package/dist/esm/text-input-guard.js +398 -182
- package/dist/esm/text-input-guard.min.js +6 -0
- package/dist/types/text-input-guard.d.ts +10 -28
- package/dist/umd/text-input-guard.js +398 -182
- package/dist/umd/text-input-guard.min.js +1 -1
- package/package.json +17 -7
|
@@ -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 {() =>
|
|
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("[
|
|
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('[
|
|
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
|
-
|
|
442
|
-
|
|
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
|
-
|
|
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
|
-
|
|
529
|
-
display.parentNode.removeChild(display);
|
|
530
|
-
}
|
|
721
|
+
state.removeDisplay();
|
|
531
722
|
|
|
532
723
|
// rawを元に戻す(type)
|
|
533
|
-
|
|
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
|
-
`[
|
|
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("[
|
|
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("[
|
|
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("[
|
|
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("[
|
|
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
|
-
|
|
1086
|
-
|
|
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 =
|
|
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(`[
|
|
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
|
-
|
|
1927
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2346
|
+
const version = "0.1.0" ;
|
|
2131
2347
|
|
|
2132
2348
|
export { attach, attachAll, autoAttach, comma, digits, numeric, rules, version };
|