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