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.
@@ -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
- "placeholder",
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.type = "hidden";
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
- const display = document.createElement("input");
196
- display.type = "text";
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.type = this.originalType;
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 JPInputGuard.
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 {HTMLElement}
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 は非対応(warnして無視)
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
- if (this.kind !== "input") {
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 input = /** @type {HTMLInputElement} */ (this.originalElement);
998
+ const state = new SwapState(element);
999
+ state.applyToRaw(element);
953
1000
 
954
- const state = new SwapState(input);
955
- state.applyToRaw(input);
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 = input; // raw
1007
+ this.hostElement = element; // raw
964
1008
  this.displayElement = display; // display
965
- this.rawElement = input;
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
- * - InputGuard 自体を公開せず、最小の操作だけを渡す
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.2.1"" を package.json の version に置換
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.2.1" ;
7811
+ const version = "1.3.0" ;
7709
7812
 
7710
7813
  /**
7711
7814
  * UMD公開時のグローバルオブジェクト