text-input-guard 0.2.1 → 0.2.2

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 CHANGED
@@ -1,11 +1,19 @@
1
- # TextInputGuard
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://natade-jp.github.io/text-input-guard/ogp-dark.svg">
4
+ <source media="(prefers-color-scheme: light)" srcset="https://natade-jp.github.io/text-input-guard/ogp-light.svg">
5
+ <img src="https://natade-jp.github.io/text-input-guard/ogp-light.svg" width="700" alt="TextInputGuard">
6
+ </picture>
7
+ </p>
2
8
 
3
- TextInputGuard は、**開発中**の日本向けの入力欄ガードライブラリです。
9
+ TextInputGuard は、**開発中**の日本語入力環境を前提に設計された入力補助ライブラリです。
4
10
 
5
- `<input>` / `<textarea>` に対して、数値入力や日本語特有の制約(全角混在、桁数、表示整形など)を扱いやすい形で提供します。
11
+ `<input>` / `<textarea>` に対して、全角混在・桁数制限・小数処理・表示整形など、日本語環境特有の入力制御を扱いやすい形で提供します。業務系フォームや金額入力など、IME の影響を受けやすい入力欄でも、表示用の値と送信用の値を分離しながら、安定した入力制御を実現できます。
6
12
 
7
- シンプルな設計のため、機能追加も簡単に行えます。
13
+ ---
8
14
 
9
- 詳細は以下をご確認ください
15
+ ## Documentation
10
16
 
11
- ### **[TextInputGuard 紹介](https://natade-jp.github.io/text-input-guard/)**
17
+ 詳しいドキュメントはこちら
18
+
19
+ 👉 https://natade-jp.github.io/text-input-guard/
@@ -165,6 +165,7 @@ class SwapState {
165
165
  // raw化(送信担当)
166
166
  input.type = "hidden";
167
167
  input.removeAttribute("id");
168
+ input.removeAttribute("class");
168
169
  input.className = "";
169
170
  input.dataset.tigRole = "raw";
170
171
 
@@ -172,6 +173,9 @@ class SwapState {
172
173
  if (this.originalId) {
173
174
  input.dataset.tigOriginalId = this.originalId;
174
175
  }
176
+ if (this.originalClass) {
177
+ input.dataset.tigOriginalClass = this.originalClass;
178
+ }
175
179
  if (this.originalName) {
176
180
  input.dataset.tigOriginalName = this.originalName;
177
181
  }
@@ -192,7 +196,10 @@ class SwapState {
192
196
  display.id = this.originalId;
193
197
  }
194
198
 
195
- display.className = this.originalClass ?? "";
199
+ if (this.originalClass) {
200
+ display.className = this.originalClass;
201
+ }
202
+
196
203
  display.value = raw.value;
197
204
 
198
205
  for (const [name, v] of Object.entries(this.originalUiAttrs)) {
@@ -253,6 +260,7 @@ class SwapState {
253
260
 
254
261
  delete raw.dataset.tigRole;
255
262
  delete raw.dataset.tigOriginalId;
263
+ delete raw.dataset.tigOriginalClass;
256
264
  delete raw.dataset.tigOriginalName;
257
265
  }
258
266
  }
@@ -446,6 +454,28 @@ function warnLog(msg, warn) {
446
454
  }
447
455
  }
448
456
 
457
+ /**
458
+ * input / textarea 要素と内部 Guard インスタンスの対応表
459
+ *
460
+ * - key: displayElement
461
+ * - value: InputGuard(内部実装)
462
+ *
463
+ * @type {WeakMap<HTMLInputElement|HTMLTextAreaElement, InputGuard>}
464
+ */
465
+ const guardMap = new WeakMap();
466
+
467
+ document.addEventListener("selectionchange", () => {
468
+ const el = document.activeElement;
469
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
470
+ return;
471
+ }
472
+ const inputGuard = guardMap.get(el);
473
+ if (!inputGuard) {
474
+ return;
475
+ }
476
+ inputGuard.onSelectionChange();
477
+ });
478
+
449
479
  /**
450
480
  * 指定した1要素に対してガードを適用し、Guard API を返す
451
481
  * @param {HTMLInputElement|HTMLTextAreaElement} element
@@ -453,9 +483,12 @@ function warnLog(msg, warn) {
453
483
  * @returns {Guard}
454
484
  */
455
485
  function attach(element, options = {}) {
456
- const guard = new InputGuard(element, options);
457
- guard.init();
458
- return guard.getGuard();
486
+ const inputGuard = new InputGuard(element, options);
487
+ inputGuard.init();
488
+ const guard = inputGuard.getGuard();
489
+ const display = guard.getDisplayElement();
490
+ guardMap.set(display, inputGuard);
491
+ return guard;
459
492
  }
460
493
 
461
494
  /**
@@ -551,7 +584,7 @@ class InputGuard {
551
584
  /**
552
585
  * ユーザーが直接入力する表示側要素
553
586
  * swapしない場合は originalElement と同一
554
- * @type {HTMLElement}
587
+ * @type {HTMLInputElement|HTMLTextAreaElement}
555
588
  */
556
589
  this.displayElement = element;
557
590
 
@@ -673,6 +706,12 @@ class InputGuard {
673
706
  */
674
707
  this.pendingCompositionCommit = false;
675
708
 
709
+ /**
710
+ * selection 更新のフレーム予約ID
711
+ * @type {number|null}
712
+ */
713
+ this.selectionFrameId = null;
714
+
676
715
  /**
677
716
  * 直前に受理した表示値、正しい情報のスナップショットのような情報(block時の戻し先)
678
717
  * @type {string}
@@ -718,9 +757,11 @@ class InputGuard {
718
757
  * @returns {SelectionState}
719
758
  */
720
759
  readSelection(el) {
760
+ const start = el.selectionStart ?? 0;
761
+ const end = el.selectionEnd ?? start;
721
762
  return {
722
- start: el.selectionStart,
723
- end: el.selectionEnd,
763
+ start,
764
+ end,
724
765
  direction: el.selectionDirection
725
766
  };
726
767
  }
@@ -834,6 +875,8 @@ class InputGuard {
834
875
  * @returns {void}
835
876
  */
836
877
  detach() {
878
+ // 管理マップから削除
879
+ guardMap.delete(this.displayElement);
837
880
  // イベント解除(displayElementがswap後の可能性があるので先に外す)
838
881
  this.unbindEvents();
839
882
  // swap復元
@@ -894,15 +937,7 @@ class InputGuard {
894
937
  this.displayElement.addEventListener("input", this.onInput);
895
938
  this.displayElement.addEventListener("beforeinput", this.onBeforeInput);
896
939
  this.displayElement.addEventListener("blur", this.onBlur);
897
-
898
- // フォーカスで編集用に戻す
899
940
  this.displayElement.addEventListener("focus", this.onFocus);
900
-
901
- // キャレット/選択範囲の変化を拾う(block時の不自然ジャンプ対策)
902
- this.displayElement.addEventListener("keyup", this.onSelectionChange);
903
- this.displayElement.addEventListener("mouseup", this.onSelectionChange);
904
- this.displayElement.addEventListener("select", this.onSelectionChange);
905
- this.displayElement.addEventListener("focus", this.onSelectionChange);
906
941
  }
907
942
 
908
943
  /**
@@ -916,10 +951,6 @@ class InputGuard {
916
951
  this.displayElement.removeEventListener("beforeinput", this.onBeforeInput);
917
952
  this.displayElement.removeEventListener("blur", this.onBlur);
918
953
  this.displayElement.removeEventListener("focus", this.onFocus);
919
- this.displayElement.removeEventListener("keyup", this.onSelectionChange);
920
- this.displayElement.removeEventListener("mouseup", this.onSelectionChange);
921
- this.displayElement.removeEventListener("select", this.onSelectionChange);
922
- this.displayElement.removeEventListener("focus", this.onSelectionChange);
923
954
  }
924
955
 
925
956
  /**
@@ -1287,12 +1318,41 @@ class InputGuard {
1287
1318
  * @returns {void}
1288
1319
  */
1289
1320
  onSelectionChange() {
1290
- // IME変換中は無視(この間はキャレット位置が不安定になることがあるため)
1291
- if (this.composing) {
1292
- return;
1321
+ const requestFrame =
1322
+ typeof requestAnimationFrame === "function"
1323
+ ? requestAnimationFrame
1324
+ : (
1325
+ /** @param {FrameRequestCallback} cb */
1326
+ (cb) => setTimeout(cb, 0)
1327
+ );
1328
+
1329
+ const cancelFrame =
1330
+ typeof cancelAnimationFrame === "function"
1331
+ ? cancelAnimationFrame
1332
+ : clearTimeout;
1333
+
1334
+ // すでに予約済みならキャンセル(selectionchange は連続発火するため)
1335
+ if (this.selectionFrameId != null) {
1336
+ cancelFrame(this.selectionFrameId);
1293
1337
  }
1294
- const el = /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.displayElement);
1295
- this.lastAcceptedSelection = this.readSelection(el);
1338
+
1339
+ this.selectionFrameId = requestFrame(() => {
1340
+ this.selectionFrameId = null;
1341
+
1342
+ // IME変換中は無視(キャレット位置が不安定になるため)
1343
+ if (this.composing) {
1344
+ return;
1345
+ }
1346
+
1347
+ const el = /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.displayElement);
1348
+
1349
+ // 要素がフォーカスされていない場合は無視
1350
+ if (document.activeElement !== el) {
1351
+ return;
1352
+ }
1353
+
1354
+ this.lastAcceptedSelection = this.readSelection(el);
1355
+ });
1296
1356
  }
1297
1357
 
1298
1358
  /**
@@ -7031,7 +7091,7 @@ function bytes(options = {}) {
7031
7091
  code: "bytes.max_overflow",
7032
7092
  rule: "bytes",
7033
7093
  phase: "validate",
7034
- detail: { max: opt.max, actual: len }
7094
+ detail: { limit: opt.max, actual: len }
7035
7095
  });
7036
7096
  }
7037
7097
  }
@@ -7377,11 +7437,11 @@ const rules = {
7377
7437
 
7378
7438
  /**
7379
7439
  * バージョン(ビルド時に置換したいならここを差し替える)
7380
- * 例: rollup replace で ""0.2.1"" を package.json の version に置換
7440
+ * 例: rollup replace で ""0.2.2"" を package.json の version に置換
7381
7441
  */
7382
7442
  // @ts-ignore
7383
7443
  // eslint-disable-next-line no-undef
7384
- const version = "0.2.1" ;
7444
+ const version = "0.2.2" ;
7385
7445
 
7386
7446
  exports.ascii = ascii;
7387
7447
  exports.attach = attach;