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.
@@ -18,10 +18,10 @@
18
18
  * SwapState
19
19
  *
20
20
  * separateValue.mode="swap" のときに使用する
21
- * 元 input 要素の状態スナップショットおよび復元ロジックを管理するクラス
21
+ * 元 input/textarea 要素の状態スナップショットおよび復元ロジックを管理するクラス
22
22
  *
23
23
  * 役割
24
- * - swap前の input の属性状態を保持する
24
+ * - swap前の input/textarea の属性状態を保持する
25
25
  * - raw化および display生成時に必要な属性を適用する
26
26
  * - detach時に元の状態へ復元する
27
27
  *
@@ -97,16 +97,16 @@
97
97
  * swap時に生成された display 用 input 要素
98
98
  * detach時に削除するため保持する
99
99
  *
100
- * @type {HTMLInputElement|null}
100
+ * @type {HTMLInputElement|HTMLTextAreaElement|null}
101
101
  */
102
102
  createdDisplay;
103
103
 
104
104
  /**
105
- * @param {HTMLInputElement} input
105
+ * @param {HTMLInputElement|HTMLTextAreaElement} input
106
106
  * swap前の元 input 要素
107
107
  */
108
108
  constructor(input) {
109
- this.originalType = input.type;
109
+ this.originalType = input.type || "";
110
110
  this.originalId = input.getAttribute("id");
111
111
  this.originalName = input.getAttribute("name");
112
112
  this.originalClass = input.className;
@@ -117,22 +117,30 @@
117
117
  this.createdDisplay = null;
118
118
 
119
119
  const UI_ATTRS = [
120
- "placeholder",
120
+ // input 有
121
121
  "list",
122
+ "size",
123
+ "pattern",
124
+
125
+ // input / textarea 共有
126
+ "placeholder",
122
127
  "inputmode",
123
128
  "autocomplete",
124
129
  "autocapitalize",
125
130
  "autocorrect",
126
131
  "minlength",
127
132
  "maxlength",
128
- "size",
129
- "pattern",
130
133
  "dir",
131
134
  "title",
132
135
  "tabindex",
133
136
  "style",
134
137
  "enterkeyhint",
135
- "spellcheck"
138
+ "spellcheck",
139
+
140
+ // textarea 用
141
+ "rows",
142
+ "cols",
143
+ "wrap"
136
144
  ];
137
145
 
138
146
  const UI_BOOL_ATTRS = [
@@ -168,12 +176,17 @@
168
176
  * raw 元input を hidden 化する
169
177
  * 送信担当要素として扱う
170
178
  *
171
- * @param {HTMLInputElement} input
179
+ * @param {HTMLInputElement|HTMLTextAreaElement} input
172
180
  * @returns {void}
173
181
  */
174
182
  applyToRaw(input) {
175
183
  // raw化(送信担当)
176
- input.type = "hidden";
184
+ if (input instanceof HTMLInputElement) {
185
+ input.type = "hidden";
186
+ } else if (input instanceof HTMLTextAreaElement) {
187
+ input.setAttribute("hidden", "");
188
+ input.style.display = "none";
189
+ }
177
190
  input.removeAttribute("id");
178
191
  input.removeAttribute("class");
179
192
  input.className = "";
@@ -194,12 +207,22 @@
194
207
  /**
195
208
  * display用 input を生成し UI属性 aria属性 data属性を適用
196
209
  *
197
- * @param {HTMLInputElement} raw hidden化された元input
198
- * @returns {HTMLInputElement}
210
+ * @param {HTMLInputElement|HTMLTextAreaElement} raw hidden化された元input
211
+ * @returns {HTMLInputElement|HTMLTextAreaElement}
199
212
  */
200
213
  createDisplay(raw) {
201
- const display = document.createElement("input");
202
- display.type = "text";
214
+ /**
215
+ * @type {HTMLInputElement|HTMLTextAreaElement}
216
+ */
217
+ let display;
218
+ if (raw instanceof HTMLInputElement) {
219
+ display = document.createElement("input");
220
+ display.type = "text";
221
+ } else if (raw instanceof HTMLTextAreaElement) {
222
+ display = document.createElement("textarea");
223
+ } else {
224
+ throw new Error("Unsupported element type for display creation");
225
+ }
203
226
  display.dataset.tigRole = "display";
204
227
 
205
228
  if (this.originalId) {
@@ -248,11 +271,16 @@
248
271
  /**
249
272
  * raw hidden化された元input を元の状態へ復元する
250
273
  *
251
- * @param {HTMLInputElement} raw
274
+ * @param {HTMLInputElement|HTMLTextAreaElement} raw
252
275
  * @returns {void}
253
276
  */
254
277
  restoreRaw(raw) {
255
- raw.type = this.originalType;
278
+ if (raw instanceof HTMLInputElement) {
279
+ raw.type = this.originalType;
280
+ } else if (raw instanceof HTMLTextAreaElement) {
281
+ raw.removeAttribute("hidden");
282
+ raw.style.display = "";
283
+ }
256
284
 
257
285
  if (this.originalId) {
258
286
  raw.setAttribute("id", this.originalId);
@@ -276,7 +304,7 @@
276
304
  }
277
305
 
278
306
  /**
279
- * The script is part of JPInputGuard.
307
+ * The script is part of TextInputGuard.
280
308
  *
281
309
  * AUTHOR:
282
310
  * natade-jp (https://github.com/natade-jp)
@@ -365,7 +393,7 @@
365
393
  * @typedef {Object} GuardContext
366
394
  * @property {HTMLElement} hostElement - 元の要素(swap時はraw側)
367
395
  * @property {HTMLElement} displayElement - ユーザーが操作する表示要素
368
- * @property {HTMLInputElement|null} rawElement - 送信用hidden要素(swap時のみ)
396
+ * @property {HTMLInputElement|HTMLTextAreaElement|null} rawElement - 送信用hidden要素(swap時のみ)
369
397
  * @property {ElementKind} kind - 要素種別(input / textarea)
370
398
  * @property {boolean} warn - warnログを出すかどうか
371
399
  * @property {string} invalidClass - エラー時に付与するclass名
@@ -626,6 +654,8 @@
626
654
  }
627
655
  }
628
656
 
657
+ let globalGuardId = 0; // デバッグ用のガードID生成
658
+
629
659
  class InputGuard {
630
660
  /**
631
661
  * InputGuard の内部状態を初期化する(DOM/設定/イベント/パイプラインを持つ)
@@ -633,6 +663,12 @@
633
663
  * @param {AttachOptions} options
634
664
  */
635
665
  constructor(element, options) {
666
+ /**
667
+ * ガードID(デバッグ用、インスタンスごとにユニーク)
668
+ * @type {number}
669
+ */
670
+ this.id = ++globalGuardId;
671
+
636
672
  /**
637
673
  * attach対象の元の要素(swap前の原本)
638
674
  * detach時の復元や基準参照に使う
@@ -714,7 +750,7 @@
714
750
  /**
715
751
  * 実際に送信を担う要素(swap時は hidden(raw) 側)
716
752
  * swapしない場合は originalElement と同一
717
- * @type {HTMLElement}
753
+ * @type {HTMLInputElement|HTMLTextAreaElement}
718
754
  */
719
755
  this.hostElement = element;
720
756
 
@@ -728,7 +764,7 @@
728
764
  /**
729
765
  * swap時に生成される hidden(raw) input
730
766
  * swapしない場合は null
731
- * @type {HTMLInputElement|null}
767
+ * @type {HTMLInputElement|HTMLTextAreaElement|null}
732
768
  */
733
769
  this.rawElement = null;
734
770
 
@@ -825,6 +861,11 @@
825
861
  */
826
862
  this.onFocus = this.onFocus.bind(this);
827
863
 
864
+ /**
865
+ * keydownイベントハンドラ(this固定)
866
+ */
867
+ this.onKeyDown = this.onKeyDown.bind(this);
868
+
828
869
  /**
829
870
  * キャレット/選択範囲の変化イベントハンドラ(this固定)
830
871
  */
@@ -880,6 +921,14 @@
880
921
  this.revertRequest = null;
881
922
  }
882
923
 
924
+ /**
925
+ * デバッグ用の文字列化
926
+ * @returns {string}
927
+ */
928
+ toString() {
929
+ return `[TextInputGuard#${this.id} kind=${this.kind} host=${this.hostElement.tagName.toLowerCase()}#${this.hostElement.id}] value=${this.hostElement.value}]`;
930
+ }
931
+
883
932
  /**
884
933
  * 初期化処理(swap適用 → パイプライン構築 → イベント登録 → 初回評価)
885
934
  * @returns {void}
@@ -934,7 +983,7 @@
934
983
 
935
984
  /**
936
985
  * separateValue.mode="swap" のとき、input を hidden(raw) にして display(input[type=text]) を生成する
937
- * - textarea は非対応(warnして無視)
986
+ * - textarea も対応(hidden属性とdisplay:noneを使用)
938
987
  * @returns {void}
939
988
  */
940
989
  applySeparateValue() {
@@ -950,25 +999,20 @@
950
999
  return;
951
1000
  }
952
1001
 
953
- if (this.kind !== "input") {
954
- warnLog('[text-input-guard] separateValue.mode="swap" is not supported for <textarea>. ignored.', this.warn);
955
- return;
956
- }
1002
+ const element = this.originalElement;
957
1003
 
958
- const input = /** @type {HTMLInputElement} */ (this.originalElement);
1004
+ const state = new SwapState(element);
1005
+ state.applyToRaw(element);
959
1006
 
960
- const state = new SwapState(input);
961
- state.applyToRaw(input);
962
-
963
- const display = state.createDisplay(input);
964
- input.after(display);
1007
+ const display = state.createDisplay(element);
1008
+ element.after(display);
965
1009
 
966
1010
  this.swapState = state;
967
1011
 
968
1012
  // elements更新
969
- this.hostElement = input; // raw
1013
+ this.hostElement = element; // raw
970
1014
  this.displayElement = display; // display
971
- this.rawElement = input;
1015
+ this.rawElement = element;
972
1016
 
973
1017
  // revert 機構
974
1018
  this.lastAcceptedValue = display.value;
@@ -1084,6 +1128,7 @@
1084
1128
  this.displayElement.addEventListener("beforeinput", this.onBeforeInput);
1085
1129
  this.displayElement.addEventListener("blur", this.onBlur);
1086
1130
  this.displayElement.addEventListener("focus", this.onFocus);
1131
+ this.displayElement.addEventListener("keydown", this.onKeyDown);
1087
1132
  }
1088
1133
 
1089
1134
  /**
@@ -1097,6 +1142,7 @@
1097
1142
  this.displayElement.removeEventListener("beforeinput", this.onBeforeInput);
1098
1143
  this.displayElement.removeEventListener("blur", this.onBlur);
1099
1144
  this.displayElement.removeEventListener("focus", this.onFocus);
1145
+ this.displayElement.removeEventListener("keydown", this.onKeyDown);
1100
1146
  }
1101
1147
 
1102
1148
  /**
@@ -1514,6 +1560,19 @@
1514
1560
  }
1515
1561
  this.existBeforeInputEvent = true;
1516
1562
  this.beforeInputSnapshot = { selection, inputType, insertedText };
1563
+
1564
+ // アンドゥリドゥの beforeinput はフォーカスされている要素以外にも発生することがあるため、
1565
+ // 正しく判定するために onKeyDown で捕まえて、必要なときだけ onBeforeInput のスナップを作る
1566
+ if (inputType === "historyUndo" || inputType === "historyRedo") {
1567
+ e.preventDefault();
1568
+
1569
+ // フォーカス中ではない要素に飛んできたUndo/Redoは止めるだけ
1570
+ if (document.activeElement !== this.displayElement) {
1571
+ return;
1572
+ }
1573
+
1574
+ this.evaluateInput();
1575
+ }
1517
1576
  }
1518
1577
 
1519
1578
  /**
@@ -1570,6 +1629,51 @@
1570
1629
  this.history.push(raw);
1571
1630
  }
1572
1631
 
1632
+ /**
1633
+ * keydownイベント:特殊な用途向けに提供(例:Enterで確定させたいなど)
1634
+ * @param {Event} e
1635
+ * @returns {void}
1636
+ */
1637
+ onKeyDown(e) {
1638
+ if (!(e instanceof KeyboardEvent)) {
1639
+ return;
1640
+ }
1641
+
1642
+ // アンドゥ及びリドゥの onBeforeInput はフォーカスされている要素以外にも発生することがあるため
1643
+ // 正しく判定するために onKeyDown で捕まえて、必要なときだけ onBeforeInput のスナップを作る
1644
+
1645
+ const isUndo = (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key.toLowerCase() === "z";
1646
+ const isRedo =
1647
+ ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === "z") ||
1648
+ ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "y");
1649
+
1650
+ if (!isUndo && !isRedo) {
1651
+ return;
1652
+ }
1653
+
1654
+ // ここでチェックする
1655
+ if (document.activeElement !== this.displayElement) {
1656
+ return;
1657
+ }
1658
+
1659
+ // ブラウザのデフォルトの undo/redo をキャンセルして、自前でUndo/Redo を発生させる
1660
+ e.preventDefault();
1661
+
1662
+ // 擬似beforeinputのスナップを作る
1663
+ this.beforeInputSnapshot = {
1664
+ selection: this.readSelection(this.displayElement),
1665
+ inputType: isUndo ? "historyUndo" : "historyRedo",
1666
+ insertedText: ""
1667
+ };
1668
+
1669
+ this.existBeforeInputEvent = true;
1670
+ try {
1671
+ this.evaluateInput();
1672
+ } finally {
1673
+ this.existBeforeInputEvent = false;
1674
+ }
1675
+ }
1676
+
1573
1677
  /**
1574
1678
  * キャレット/選択範囲の変化を lastAcceptedSelection に反映する
1575
1679
  * - 値が変わっていない状態でもキャレットは動くため、block時に自然な位置へ戻すために使う
@@ -1913,7 +2017,7 @@
1913
2017
 
1914
2018
  /**
1915
2019
  * 外部に公開する Guard API を生成して返す
1916
- * - InputGuard 自体を公開せず、最小の操作だけを渡す
2020
+ * - TextInputGuard 自体を公開せず、最小の操作だけを渡す
1917
2021
  * @returns {Guard}
1918
2022
  */
1919
2023
  getGuard() {
@@ -2352,7 +2456,7 @@
2352
2456
 
2353
2457
  return {
2354
2458
  name: "numeric",
2355
- targets: ["input"],
2459
+ targets: ["input", "textarea"],
2356
2460
 
2357
2461
  /**
2358
2462
  * 文字単位の正規化(全角→半角、記号統一、不要文字の除去)
@@ -2701,7 +2805,6 @@
2701
2805
  * @returns {Rule}
2702
2806
  */
2703
2807
  function digits(options = {}) {
2704
- /** @type {DigitsRuleOptions} */
2705
2808
  const opt = {
2706
2809
  int: typeof options.int === "number" ? options.int : undefined,
2707
2810
  frac: typeof options.frac === "number" ? options.frac : undefined,
@@ -2715,7 +2818,7 @@
2715
2818
 
2716
2819
  return {
2717
2820
  name: "digits",
2718
- targets: ["input"],
2821
+ targets: ["input", "textarea"],
2719
2822
 
2720
2823
  /**
2721
2824
  * 桁数チェック(入力中:エラーを積むだけ)
@@ -2968,7 +3071,7 @@
2968
3071
  function comma() {
2969
3072
  return {
2970
3073
  name: "comma",
2971
- targets: ["input"],
3074
+ targets: ["input", "textarea"],
2972
3075
 
2973
3076
  /**
2974
3077
  * 表示整形(確定時のみ)
@@ -7447,7 +7550,7 @@
7447
7550
 
7448
7551
  return {
7449
7552
  name: "prefix",
7450
- targets: ["input"],
7553
+ targets: ["input", "textarea"],
7451
7554
 
7452
7555
  /**
7453
7556
  * 手動入力された prefix を除去
@@ -7541,7 +7644,7 @@
7541
7644
 
7542
7645
  return {
7543
7646
  name: "suffix",
7544
- targets: ["input"],
7647
+ targets: ["input", "textarea"],
7545
7648
 
7546
7649
  /**
7547
7650
  * 手動入力された suffix を除去
@@ -7707,11 +7810,11 @@
7707
7810
 
7708
7811
  /**
7709
7812
  * バージョン(ビルド時に置換したいならここを差し替える)
7710
- * 例: rollup replace で ""1.2.1"" を package.json の version に置換
7813
+ * 例: rollup replace で ""1.3.0"" を package.json の version に置換
7711
7814
  */
7712
7815
  // @ts-ignore
7713
7816
  // eslint-disable-next-line no-undef
7714
- const version = "1.2.1" ;
7817
+ const version = "1.3.0" ;
7715
7818
 
7716
7819
  /**
7717
7820
  * UMD公開時のグローバルオブジェクト