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,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 {() =>
|
|
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("[
|
|
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('[
|
|
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
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
531
|
-
display.parentNode.removeChild(display);
|
|
532
|
-
}
|
|
723
|
+
state.removeDisplay();
|
|
533
724
|
|
|
534
725
|
// rawを元に戻す(type)
|
|
535
|
-
|
|
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
|
-
`[
|
|
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("[
|
|
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("[
|
|
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("[
|
|
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("[
|
|
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
|
-
|
|
1088
|
-
|
|
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 =
|
|
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(`[
|
|
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
|
-
|
|
1929
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2348
|
+
const version = "0.1.0" ;
|
|
2133
2349
|
|
|
2134
2350
|
exports.attach = attach;
|
|
2135
2351
|
exports.attachAll = attachAll;
|