text-input-guard 1.2.1 → 1.3.0
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/dist/cjs/text-input-guard.cjs +146 -43
- package/dist/cjs/text-input-guard.min.cjs +1 -1
- package/dist/esm/text-input-guard.js +146 -43
- package/dist/esm/text-input-guard.min.js +1 -1
- package/dist/types/text-input-guard.d.ts +1 -1
- package/dist/umd/text-input-guard.js +146 -43
- package/dist/umd/text-input-guard.min.js +1 -1
- package/package.json +1 -1
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
* SwapState
|
|
13
13
|
*
|
|
14
14
|
* separateValue.mode="swap" のときに使用する
|
|
15
|
-
* 元 input 要素の状態スナップショットおよび復元ロジックを管理するクラス
|
|
15
|
+
* 元 input/textarea 要素の状態スナップショットおよび復元ロジックを管理するクラス
|
|
16
16
|
*
|
|
17
17
|
* 役割
|
|
18
|
-
* - swap前の input の属性状態を保持する
|
|
18
|
+
* - swap前の input/textarea の属性状態を保持する
|
|
19
19
|
* - raw化および display生成時に必要な属性を適用する
|
|
20
20
|
* - detach時に元の状態へ復元する
|
|
21
21
|
*
|
|
@@ -91,16 +91,16 @@ class SwapState {
|
|
|
91
91
|
* swap時に生成された display 用 input 要素
|
|
92
92
|
* detach時に削除するため保持する
|
|
93
93
|
*
|
|
94
|
-
* @type {HTMLInputElement|null}
|
|
94
|
+
* @type {HTMLInputElement|HTMLTextAreaElement|null}
|
|
95
95
|
*/
|
|
96
96
|
createdDisplay;
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* @param {HTMLInputElement} input
|
|
99
|
+
* @param {HTMLInputElement|HTMLTextAreaElement} input
|
|
100
100
|
* swap前の元 input 要素
|
|
101
101
|
*/
|
|
102
102
|
constructor(input) {
|
|
103
|
-
this.originalType = input.type;
|
|
103
|
+
this.originalType = input.type || "";
|
|
104
104
|
this.originalId = input.getAttribute("id");
|
|
105
105
|
this.originalName = input.getAttribute("name");
|
|
106
106
|
this.originalClass = input.className;
|
|
@@ -111,22 +111,30 @@ class SwapState {
|
|
|
111
111
|
this.createdDisplay = null;
|
|
112
112
|
|
|
113
113
|
const UI_ATTRS = [
|
|
114
|
-
|
|
114
|
+
// input 有
|
|
115
115
|
"list",
|
|
116
|
+
"size",
|
|
117
|
+
"pattern",
|
|
118
|
+
|
|
119
|
+
// input / textarea 共有
|
|
120
|
+
"placeholder",
|
|
116
121
|
"inputmode",
|
|
117
122
|
"autocomplete",
|
|
118
123
|
"autocapitalize",
|
|
119
124
|
"autocorrect",
|
|
120
125
|
"minlength",
|
|
121
126
|
"maxlength",
|
|
122
|
-
"size",
|
|
123
|
-
"pattern",
|
|
124
127
|
"dir",
|
|
125
128
|
"title",
|
|
126
129
|
"tabindex",
|
|
127
130
|
"style",
|
|
128
131
|
"enterkeyhint",
|
|
129
|
-
"spellcheck"
|
|
132
|
+
"spellcheck",
|
|
133
|
+
|
|
134
|
+
// textarea 用
|
|
135
|
+
"rows",
|
|
136
|
+
"cols",
|
|
137
|
+
"wrap"
|
|
130
138
|
];
|
|
131
139
|
|
|
132
140
|
const UI_BOOL_ATTRS = [
|
|
@@ -162,12 +170,17 @@ class SwapState {
|
|
|
162
170
|
* raw 元input を hidden 化する
|
|
163
171
|
* 送信担当要素として扱う
|
|
164
172
|
*
|
|
165
|
-
* @param {HTMLInputElement} input
|
|
173
|
+
* @param {HTMLInputElement|HTMLTextAreaElement} input
|
|
166
174
|
* @returns {void}
|
|
167
175
|
*/
|
|
168
176
|
applyToRaw(input) {
|
|
169
177
|
// raw化(送信担当)
|
|
170
|
-
input
|
|
178
|
+
if (input instanceof HTMLInputElement) {
|
|
179
|
+
input.type = "hidden";
|
|
180
|
+
} else if (input instanceof HTMLTextAreaElement) {
|
|
181
|
+
input.setAttribute("hidden", "");
|
|
182
|
+
input.style.display = "none";
|
|
183
|
+
}
|
|
171
184
|
input.removeAttribute("id");
|
|
172
185
|
input.removeAttribute("class");
|
|
173
186
|
input.className = "";
|
|
@@ -188,12 +201,22 @@ class SwapState {
|
|
|
188
201
|
/**
|
|
189
202
|
* display用 input を生成し UI属性 aria属性 data属性を適用
|
|
190
203
|
*
|
|
191
|
-
* @param {HTMLInputElement} raw hidden化された元input
|
|
192
|
-
* @returns {HTMLInputElement}
|
|
204
|
+
* @param {HTMLInputElement|HTMLTextAreaElement} raw hidden化された元input
|
|
205
|
+
* @returns {HTMLInputElement|HTMLTextAreaElement}
|
|
193
206
|
*/
|
|
194
207
|
createDisplay(raw) {
|
|
195
|
-
|
|
196
|
-
|
|
208
|
+
/**
|
|
209
|
+
* @type {HTMLInputElement|HTMLTextAreaElement}
|
|
210
|
+
*/
|
|
211
|
+
let display;
|
|
212
|
+
if (raw instanceof HTMLInputElement) {
|
|
213
|
+
display = document.createElement("input");
|
|
214
|
+
display.type = "text";
|
|
215
|
+
} else if (raw instanceof HTMLTextAreaElement) {
|
|
216
|
+
display = document.createElement("textarea");
|
|
217
|
+
} else {
|
|
218
|
+
throw new Error("Unsupported element type for display creation");
|
|
219
|
+
}
|
|
197
220
|
display.dataset.tigRole = "display";
|
|
198
221
|
|
|
199
222
|
if (this.originalId) {
|
|
@@ -242,11 +265,16 @@ class SwapState {
|
|
|
242
265
|
/**
|
|
243
266
|
* raw hidden化された元input を元の状態へ復元する
|
|
244
267
|
*
|
|
245
|
-
* @param {HTMLInputElement} raw
|
|
268
|
+
* @param {HTMLInputElement|HTMLTextAreaElement} raw
|
|
246
269
|
* @returns {void}
|
|
247
270
|
*/
|
|
248
271
|
restoreRaw(raw) {
|
|
249
|
-
raw
|
|
272
|
+
if (raw instanceof HTMLInputElement) {
|
|
273
|
+
raw.type = this.originalType;
|
|
274
|
+
} else if (raw instanceof HTMLTextAreaElement) {
|
|
275
|
+
raw.removeAttribute("hidden");
|
|
276
|
+
raw.style.display = "";
|
|
277
|
+
}
|
|
250
278
|
|
|
251
279
|
if (this.originalId) {
|
|
252
280
|
raw.setAttribute("id", this.originalId);
|
|
@@ -270,7 +298,7 @@ class SwapState {
|
|
|
270
298
|
}
|
|
271
299
|
|
|
272
300
|
/**
|
|
273
|
-
* The script is part of
|
|
301
|
+
* The script is part of TextInputGuard.
|
|
274
302
|
*
|
|
275
303
|
* AUTHOR:
|
|
276
304
|
* natade-jp (https://github.com/natade-jp)
|
|
@@ -359,7 +387,7 @@ class SwapState {
|
|
|
359
387
|
* @typedef {Object} GuardContext
|
|
360
388
|
* @property {HTMLElement} hostElement - 元の要素(swap時はraw側)
|
|
361
389
|
* @property {HTMLElement} displayElement - ユーザーが操作する表示要素
|
|
362
|
-
* @property {HTMLInputElement|null} rawElement - 送信用hidden要素(swap時のみ)
|
|
390
|
+
* @property {HTMLInputElement|HTMLTextAreaElement|null} rawElement - 送信用hidden要素(swap時のみ)
|
|
363
391
|
* @property {ElementKind} kind - 要素種別(input / textarea)
|
|
364
392
|
* @property {boolean} warn - warnログを出すかどうか
|
|
365
393
|
* @property {string} invalidClass - エラー時に付与するclass名
|
|
@@ -620,6 +648,8 @@ class HistoryQueue {
|
|
|
620
648
|
}
|
|
621
649
|
}
|
|
622
650
|
|
|
651
|
+
let globalGuardId = 0; // デバッグ用のガードID生成
|
|
652
|
+
|
|
623
653
|
class InputGuard {
|
|
624
654
|
/**
|
|
625
655
|
* InputGuard の内部状態を初期化する(DOM/設定/イベント/パイプラインを持つ)
|
|
@@ -627,6 +657,12 @@ class InputGuard {
|
|
|
627
657
|
* @param {AttachOptions} options
|
|
628
658
|
*/
|
|
629
659
|
constructor(element, options) {
|
|
660
|
+
/**
|
|
661
|
+
* ガードID(デバッグ用、インスタンスごとにユニーク)
|
|
662
|
+
* @type {number}
|
|
663
|
+
*/
|
|
664
|
+
this.id = ++globalGuardId;
|
|
665
|
+
|
|
630
666
|
/**
|
|
631
667
|
* attach対象の元の要素(swap前の原本)
|
|
632
668
|
* detach時の復元や基準参照に使う
|
|
@@ -708,7 +744,7 @@ class InputGuard {
|
|
|
708
744
|
/**
|
|
709
745
|
* 実際に送信を担う要素(swap時は hidden(raw) 側)
|
|
710
746
|
* swapしない場合は originalElement と同一
|
|
711
|
-
* @type {
|
|
747
|
+
* @type {HTMLInputElement|HTMLTextAreaElement}
|
|
712
748
|
*/
|
|
713
749
|
this.hostElement = element;
|
|
714
750
|
|
|
@@ -722,7 +758,7 @@ class InputGuard {
|
|
|
722
758
|
/**
|
|
723
759
|
* swap時に生成される hidden(raw) input
|
|
724
760
|
* swapしない場合は null
|
|
725
|
-
* @type {HTMLInputElement|null}
|
|
761
|
+
* @type {HTMLInputElement|HTMLTextAreaElement|null}
|
|
726
762
|
*/
|
|
727
763
|
this.rawElement = null;
|
|
728
764
|
|
|
@@ -819,6 +855,11 @@ class InputGuard {
|
|
|
819
855
|
*/
|
|
820
856
|
this.onFocus = this.onFocus.bind(this);
|
|
821
857
|
|
|
858
|
+
/**
|
|
859
|
+
* keydownイベントハンドラ(this固定)
|
|
860
|
+
*/
|
|
861
|
+
this.onKeyDown = this.onKeyDown.bind(this);
|
|
862
|
+
|
|
822
863
|
/**
|
|
823
864
|
* キャレット/選択範囲の変化イベントハンドラ(this固定)
|
|
824
865
|
*/
|
|
@@ -874,6 +915,14 @@ class InputGuard {
|
|
|
874
915
|
this.revertRequest = null;
|
|
875
916
|
}
|
|
876
917
|
|
|
918
|
+
/**
|
|
919
|
+
* デバッグ用の文字列化
|
|
920
|
+
* @returns {string}
|
|
921
|
+
*/
|
|
922
|
+
toString() {
|
|
923
|
+
return `[TextInputGuard#${this.id} kind=${this.kind} host=${this.hostElement.tagName.toLowerCase()}#${this.hostElement.id}] value=${this.hostElement.value}]`;
|
|
924
|
+
}
|
|
925
|
+
|
|
877
926
|
/**
|
|
878
927
|
* 初期化処理(swap適用 → パイプライン構築 → イベント登録 → 初回評価)
|
|
879
928
|
* @returns {void}
|
|
@@ -928,7 +977,7 @@ class InputGuard {
|
|
|
928
977
|
|
|
929
978
|
/**
|
|
930
979
|
* separateValue.mode="swap" のとき、input を hidden(raw) にして display(input[type=text]) を生成する
|
|
931
|
-
* - textarea
|
|
980
|
+
* - textarea も対応(hidden属性とdisplay:noneを使用)
|
|
932
981
|
* @returns {void}
|
|
933
982
|
*/
|
|
934
983
|
applySeparateValue() {
|
|
@@ -944,25 +993,20 @@ class InputGuard {
|
|
|
944
993
|
return;
|
|
945
994
|
}
|
|
946
995
|
|
|
947
|
-
|
|
948
|
-
warnLog('[text-input-guard] separateValue.mode="swap" is not supported for <textarea>. ignored.', this.warn);
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
996
|
+
const element = this.originalElement;
|
|
951
997
|
|
|
952
|
-
const
|
|
998
|
+
const state = new SwapState(element);
|
|
999
|
+
state.applyToRaw(element);
|
|
953
1000
|
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
const display = state.createDisplay(input);
|
|
958
|
-
input.after(display);
|
|
1001
|
+
const display = state.createDisplay(element);
|
|
1002
|
+
element.after(display);
|
|
959
1003
|
|
|
960
1004
|
this.swapState = state;
|
|
961
1005
|
|
|
962
1006
|
// elements更新
|
|
963
|
-
this.hostElement =
|
|
1007
|
+
this.hostElement = element; // raw
|
|
964
1008
|
this.displayElement = display; // display
|
|
965
|
-
this.rawElement =
|
|
1009
|
+
this.rawElement = element;
|
|
966
1010
|
|
|
967
1011
|
// revert 機構
|
|
968
1012
|
this.lastAcceptedValue = display.value;
|
|
@@ -1078,6 +1122,7 @@ class InputGuard {
|
|
|
1078
1122
|
this.displayElement.addEventListener("beforeinput", this.onBeforeInput);
|
|
1079
1123
|
this.displayElement.addEventListener("blur", this.onBlur);
|
|
1080
1124
|
this.displayElement.addEventListener("focus", this.onFocus);
|
|
1125
|
+
this.displayElement.addEventListener("keydown", this.onKeyDown);
|
|
1081
1126
|
}
|
|
1082
1127
|
|
|
1083
1128
|
/**
|
|
@@ -1091,6 +1136,7 @@ class InputGuard {
|
|
|
1091
1136
|
this.displayElement.removeEventListener("beforeinput", this.onBeforeInput);
|
|
1092
1137
|
this.displayElement.removeEventListener("blur", this.onBlur);
|
|
1093
1138
|
this.displayElement.removeEventListener("focus", this.onFocus);
|
|
1139
|
+
this.displayElement.removeEventListener("keydown", this.onKeyDown);
|
|
1094
1140
|
}
|
|
1095
1141
|
|
|
1096
1142
|
/**
|
|
@@ -1508,6 +1554,19 @@ class InputGuard {
|
|
|
1508
1554
|
}
|
|
1509
1555
|
this.existBeforeInputEvent = true;
|
|
1510
1556
|
this.beforeInputSnapshot = { selection, inputType, insertedText };
|
|
1557
|
+
|
|
1558
|
+
// アンドゥリドゥの beforeinput はフォーカスされている要素以外にも発生することがあるため、
|
|
1559
|
+
// 正しく判定するために onKeyDown で捕まえて、必要なときだけ onBeforeInput のスナップを作る
|
|
1560
|
+
if (inputType === "historyUndo" || inputType === "historyRedo") {
|
|
1561
|
+
e.preventDefault();
|
|
1562
|
+
|
|
1563
|
+
// フォーカス中ではない要素に飛んできたUndo/Redoは止めるだけ
|
|
1564
|
+
if (document.activeElement !== this.displayElement) {
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
this.evaluateInput();
|
|
1569
|
+
}
|
|
1511
1570
|
}
|
|
1512
1571
|
|
|
1513
1572
|
/**
|
|
@@ -1564,6 +1623,51 @@ class InputGuard {
|
|
|
1564
1623
|
this.history.push(raw);
|
|
1565
1624
|
}
|
|
1566
1625
|
|
|
1626
|
+
/**
|
|
1627
|
+
* keydownイベント:特殊な用途向けに提供(例:Enterで確定させたいなど)
|
|
1628
|
+
* @param {Event} e
|
|
1629
|
+
* @returns {void}
|
|
1630
|
+
*/
|
|
1631
|
+
onKeyDown(e) {
|
|
1632
|
+
if (!(e instanceof KeyboardEvent)) {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// アンドゥ及びリドゥの onBeforeInput はフォーカスされている要素以外にも発生することがあるため
|
|
1637
|
+
// 正しく判定するために onKeyDown で捕まえて、必要なときだけ onBeforeInput のスナップを作る
|
|
1638
|
+
|
|
1639
|
+
const isUndo = (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key.toLowerCase() === "z";
|
|
1640
|
+
const isRedo =
|
|
1641
|
+
((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === "z") ||
|
|
1642
|
+
((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "y");
|
|
1643
|
+
|
|
1644
|
+
if (!isUndo && !isRedo) {
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// ここでチェックする
|
|
1649
|
+
if (document.activeElement !== this.displayElement) {
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// ブラウザのデフォルトの undo/redo をキャンセルして、自前でUndo/Redo を発生させる
|
|
1654
|
+
e.preventDefault();
|
|
1655
|
+
|
|
1656
|
+
// 擬似beforeinputのスナップを作る
|
|
1657
|
+
this.beforeInputSnapshot = {
|
|
1658
|
+
selection: this.readSelection(this.displayElement),
|
|
1659
|
+
inputType: isUndo ? "historyUndo" : "historyRedo",
|
|
1660
|
+
insertedText: ""
|
|
1661
|
+
};
|
|
1662
|
+
|
|
1663
|
+
this.existBeforeInputEvent = true;
|
|
1664
|
+
try {
|
|
1665
|
+
this.evaluateInput();
|
|
1666
|
+
} finally {
|
|
1667
|
+
this.existBeforeInputEvent = false;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1567
1671
|
/**
|
|
1568
1672
|
* キャレット/選択範囲の変化を lastAcceptedSelection に反映する
|
|
1569
1673
|
* - 値が変わっていない状態でもキャレットは動くため、block時に自然な位置へ戻すために使う
|
|
@@ -1907,7 +2011,7 @@ class InputGuard {
|
|
|
1907
2011
|
|
|
1908
2012
|
/**
|
|
1909
2013
|
* 外部に公開する Guard API を生成して返す
|
|
1910
|
-
* -
|
|
2014
|
+
* - TextInputGuard 自体を公開せず、最小の操作だけを渡す
|
|
1911
2015
|
* @returns {Guard}
|
|
1912
2016
|
*/
|
|
1913
2017
|
getGuard() {
|
|
@@ -2346,7 +2450,7 @@ function numeric(options = {}) {
|
|
|
2346
2450
|
|
|
2347
2451
|
return {
|
|
2348
2452
|
name: "numeric",
|
|
2349
|
-
targets: ["input"],
|
|
2453
|
+
targets: ["input", "textarea"],
|
|
2350
2454
|
|
|
2351
2455
|
/**
|
|
2352
2456
|
* 文字単位の正規化(全角→半角、記号統一、不要文字の除去)
|
|
@@ -2695,7 +2799,6 @@ function roundFraction(intPart, fracPart, fracLimit) {
|
|
|
2695
2799
|
* @returns {Rule}
|
|
2696
2800
|
*/
|
|
2697
2801
|
function digits(options = {}) {
|
|
2698
|
-
/** @type {DigitsRuleOptions} */
|
|
2699
2802
|
const opt = {
|
|
2700
2803
|
int: typeof options.int === "number" ? options.int : undefined,
|
|
2701
2804
|
frac: typeof options.frac === "number" ? options.frac : undefined,
|
|
@@ -2709,7 +2812,7 @@ function digits(options = {}) {
|
|
|
2709
2812
|
|
|
2710
2813
|
return {
|
|
2711
2814
|
name: "digits",
|
|
2712
|
-
targets: ["input"],
|
|
2815
|
+
targets: ["input", "textarea"],
|
|
2713
2816
|
|
|
2714
2817
|
/**
|
|
2715
2818
|
* 桁数チェック(入力中:エラーを積むだけ)
|
|
@@ -2962,7 +3065,7 @@ digits.fromDataset = function fromDataset(dataset, _el) {
|
|
|
2962
3065
|
function comma() {
|
|
2963
3066
|
return {
|
|
2964
3067
|
name: "comma",
|
|
2965
|
-
targets: ["input"],
|
|
3068
|
+
targets: ["input", "textarea"],
|
|
2966
3069
|
|
|
2967
3070
|
/**
|
|
2968
3071
|
* 表示整形(確定時のみ)
|
|
@@ -7441,7 +7544,7 @@ function prefix(options) {
|
|
|
7441
7544
|
|
|
7442
7545
|
return {
|
|
7443
7546
|
name: "prefix",
|
|
7444
|
-
targets: ["input"],
|
|
7547
|
+
targets: ["input", "textarea"],
|
|
7445
7548
|
|
|
7446
7549
|
/**
|
|
7447
7550
|
* 手動入力された prefix を除去
|
|
@@ -7535,7 +7638,7 @@ function suffix(options) {
|
|
|
7535
7638
|
|
|
7536
7639
|
return {
|
|
7537
7640
|
name: "suffix",
|
|
7538
|
-
targets: ["input"],
|
|
7641
|
+
targets: ["input", "textarea"],
|
|
7539
7642
|
|
|
7540
7643
|
/**
|
|
7541
7644
|
* 手動入力された suffix を除去
|
|
@@ -7701,11 +7804,11 @@ const rules = {
|
|
|
7701
7804
|
|
|
7702
7805
|
/**
|
|
7703
7806
|
* バージョン(ビルド時に置換したいならここを差し替える)
|
|
7704
|
-
* 例: rollup replace で ""1.
|
|
7807
|
+
* 例: rollup replace で ""1.3.0"" を package.json の version に置換
|
|
7705
7808
|
*/
|
|
7706
7809
|
// @ts-ignore
|
|
7707
7810
|
// eslint-disable-next-line no-undef
|
|
7708
|
-
const version = "1.
|
|
7811
|
+
const version = "1.3.0" ;
|
|
7709
7812
|
|
|
7710
7813
|
/**
|
|
7711
7814
|
* UMD公開時のグローバルオブジェクト
|