text-input-guard 0.2.0 → 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.
@@ -169,6 +169,7 @@
169
169
  // raw化(送信担当)
170
170
  input.type = "hidden";
171
171
  input.removeAttribute("id");
172
+ input.removeAttribute("class");
172
173
  input.className = "";
173
174
  input.dataset.tigRole = "raw";
174
175
 
@@ -176,6 +177,9 @@
176
177
  if (this.originalId) {
177
178
  input.dataset.tigOriginalId = this.originalId;
178
179
  }
180
+ if (this.originalClass) {
181
+ input.dataset.tigOriginalClass = this.originalClass;
182
+ }
179
183
  if (this.originalName) {
180
184
  input.dataset.tigOriginalName = this.originalName;
181
185
  }
@@ -196,7 +200,10 @@
196
200
  display.id = this.originalId;
197
201
  }
198
202
 
199
- display.className = this.originalClass ?? "";
203
+ if (this.originalClass) {
204
+ display.className = this.originalClass;
205
+ }
206
+
200
207
  display.value = raw.value;
201
208
 
202
209
  for (const [name, v] of Object.entries(this.originalUiAttrs)) {
@@ -257,6 +264,7 @@
257
264
 
258
265
  delete raw.dataset.tigRole;
259
266
  delete raw.dataset.tigOriginalId;
267
+ delete raw.dataset.tigOriginalClass;
260
268
  delete raw.dataset.tigOriginalName;
261
269
  }
262
270
  }
@@ -402,7 +410,7 @@
402
410
  * @typedef {Object} AttachOptions
403
411
  * @property {Rule[]} [rules] - 適用するルール配列(順番がフェーズ内実行順になる)
404
412
  * @property {boolean} [warn] - 非対応ルールなどを console.warn するか
405
- * @property {string} [invalidClass] - エラー時に付けるclass名
413
+ * @property {string} [invalidClass="is-invalid"] - エラー時に付けるclass名
406
414
  * @property {SeparateValueOptions} [separateValue] - 表示値と内部値の分離設定
407
415
  * @property {(result: ValidateResult) => void} [onValidate] - 評価完了時の通知(input/commitごと)
408
416
  */
@@ -450,6 +458,28 @@
450
458
  }
451
459
  }
452
460
 
461
+ /**
462
+ * input / textarea 要素と内部 Guard インスタンスの対応表
463
+ *
464
+ * - key: displayElement
465
+ * - value: InputGuard(内部実装)
466
+ *
467
+ * @type {WeakMap<HTMLInputElement|HTMLTextAreaElement, InputGuard>}
468
+ */
469
+ const guardMap = new WeakMap();
470
+
471
+ document.addEventListener("selectionchange", () => {
472
+ const el = document.activeElement;
473
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
474
+ return;
475
+ }
476
+ const inputGuard = guardMap.get(el);
477
+ if (!inputGuard) {
478
+ return;
479
+ }
480
+ inputGuard.onSelectionChange();
481
+ });
482
+
453
483
  /**
454
484
  * 指定した1要素に対してガードを適用し、Guard API を返す
455
485
  * @param {HTMLInputElement|HTMLTextAreaElement} element
@@ -457,9 +487,12 @@
457
487
  * @returns {Guard}
458
488
  */
459
489
  function attach(element, options = {}) {
460
- const guard = new InputGuard(element, options);
461
- guard.init();
462
- return guard.getGuard();
490
+ const inputGuard = new InputGuard(element, options);
491
+ inputGuard.init();
492
+ const guard = inputGuard.getGuard();
493
+ const display = guard.getDisplayElement();
494
+ guardMap.set(display, inputGuard);
495
+ return guard;
463
496
  }
464
497
 
465
498
  /**
@@ -555,7 +588,7 @@
555
588
  /**
556
589
  * ユーザーが直接入力する表示側要素
557
590
  * swapしない場合は originalElement と同一
558
- * @type {HTMLElement}
591
+ * @type {HTMLInputElement|HTMLTextAreaElement}
559
592
  */
560
593
  this.displayElement = element;
561
594
 
@@ -677,6 +710,12 @@
677
710
  */
678
711
  this.pendingCompositionCommit = false;
679
712
 
713
+ /**
714
+ * selection 更新のフレーム予約ID
715
+ * @type {number|null}
716
+ */
717
+ this.selectionFrameId = null;
718
+
680
719
  /**
681
720
  * 直前に受理した表示値、正しい情報のスナップショットのような情報(block時の戻し先)
682
721
  * @type {string}
@@ -722,9 +761,11 @@
722
761
  * @returns {SelectionState}
723
762
  */
724
763
  readSelection(el) {
764
+ const start = el.selectionStart ?? 0;
765
+ const end = el.selectionEnd ?? start;
725
766
  return {
726
- start: el.selectionStart,
727
- end: el.selectionEnd,
767
+ start,
768
+ end,
728
769
  direction: el.selectionDirection
729
770
  };
730
771
  }
@@ -838,6 +879,8 @@
838
879
  * @returns {void}
839
880
  */
840
881
  detach() {
882
+ // 管理マップから削除
883
+ guardMap.delete(this.displayElement);
841
884
  // イベント解除(displayElementがswap後の可能性があるので先に外す)
842
885
  this.unbindEvents();
843
886
  // swap復元
@@ -898,15 +941,7 @@
898
941
  this.displayElement.addEventListener("input", this.onInput);
899
942
  this.displayElement.addEventListener("beforeinput", this.onBeforeInput);
900
943
  this.displayElement.addEventListener("blur", this.onBlur);
901
-
902
- // フォーカスで編集用に戻す
903
944
  this.displayElement.addEventListener("focus", this.onFocus);
904
-
905
- // キャレット/選択範囲の変化を拾う(block時の不自然ジャンプ対策)
906
- this.displayElement.addEventListener("keyup", this.onSelectionChange);
907
- this.displayElement.addEventListener("mouseup", this.onSelectionChange);
908
- this.displayElement.addEventListener("select", this.onSelectionChange);
909
- this.displayElement.addEventListener("focus", this.onSelectionChange);
910
945
  }
911
946
 
912
947
  /**
@@ -920,10 +955,6 @@
920
955
  this.displayElement.removeEventListener("beforeinput", this.onBeforeInput);
921
956
  this.displayElement.removeEventListener("blur", this.onBlur);
922
957
  this.displayElement.removeEventListener("focus", this.onFocus);
923
- this.displayElement.removeEventListener("keyup", this.onSelectionChange);
924
- this.displayElement.removeEventListener("mouseup", this.onSelectionChange);
925
- this.displayElement.removeEventListener("select", this.onSelectionChange);
926
- this.displayElement.removeEventListener("focus", this.onSelectionChange);
927
958
  }
928
959
 
929
960
  /**
@@ -1291,12 +1322,41 @@
1291
1322
  * @returns {void}
1292
1323
  */
1293
1324
  onSelectionChange() {
1294
- // IME変換中は無視(この間はキャレット位置が不安定になることがあるため)
1295
- if (this.composing) {
1296
- return;
1325
+ const requestFrame =
1326
+ typeof requestAnimationFrame === "function"
1327
+ ? requestAnimationFrame
1328
+ : (
1329
+ /** @param {FrameRequestCallback} cb */
1330
+ (cb) => setTimeout(cb, 0)
1331
+ );
1332
+
1333
+ const cancelFrame =
1334
+ typeof cancelAnimationFrame === "function"
1335
+ ? cancelAnimationFrame
1336
+ : clearTimeout;
1337
+
1338
+ // すでに予約済みならキャンセル(selectionchange は連続発火するため)
1339
+ if (this.selectionFrameId != null) {
1340
+ cancelFrame(this.selectionFrameId);
1297
1341
  }
1298
- const el = /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.displayElement);
1299
- this.lastAcceptedSelection = this.readSelection(el);
1342
+
1343
+ this.selectionFrameId = requestFrame(() => {
1344
+ this.selectionFrameId = null;
1345
+
1346
+ // IME変換中は無視(キャレット位置が不安定になるため)
1347
+ if (this.composing) {
1348
+ return;
1349
+ }
1350
+
1351
+ const el = /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.displayElement);
1352
+
1353
+ // 要素がフォーカスされていない場合は無視
1354
+ if (document.activeElement !== el) {
1355
+ return;
1356
+ }
1357
+
1358
+ this.lastAcceptedSelection = this.readSelection(el);
1359
+ });
1300
1360
  }
1301
1361
 
1302
1362
  /**
@@ -2705,6 +2765,104 @@
2705
2765
  return comma();
2706
2766
  };
2707
2767
 
2768
+ /**
2769
+ * The script is part of TextInputGuard.
2770
+ *
2771
+ * AUTHOR:
2772
+ * natade-jp (https://github.com/natade-jp)
2773
+ *
2774
+ * LICENSE:
2775
+ * The MIT license https://opensource.org/licenses/MIT
2776
+ */
2777
+
2778
+ /**
2779
+ * IMEオフ入力相当の文字変換テーブル
2780
+ * @type {Record<string, string>}
2781
+ */
2782
+ /* eslint-disable quote-props */
2783
+ const IME_OFF_MAP = {
2784
+ "\u3000": "\u0020", // 全角スペース → space
2785
+ "\u3001": "\u002C", // 、 → ,
2786
+ "\u3002": "\u002E", // 。 → .
2787
+ "\u300C": "\u005B", // 「 → [
2788
+ "\u300D": "\u005D", // 」 → ]
2789
+ "\u301C": "\u007E", // 〜 → ~
2790
+ "\u30FC": "\u002D", // ー → -
2791
+ "\uFFE5": "\u005C" // ¥ → \
2792
+ };
2793
+ /* eslint-enable quote-props */
2794
+
2795
+ /**
2796
+ * ASCII入力欄に日本語IMEで入った文字をASCIIへ矯正する
2797
+ * @param {string} text - 変換したいテキスト
2798
+ * @returns {string} 変換後のテキスト
2799
+ */
2800
+ const toImeOff = function (text) {
2801
+ return Array.from(String(text), (ch) => {
2802
+ // 個別マップ
2803
+ if (ch in IME_OFF_MAP) {
2804
+ return IME_OFF_MAP[ch];
2805
+ }
2806
+
2807
+ const code = ch.charCodeAt(0);
2808
+
2809
+ // 全角ASCII
2810
+ if (code >= 0xFF01 && code <= 0xFF5E) {
2811
+ return String.fromCharCode(code - 0xFEE0);
2812
+ }
2813
+
2814
+ // シングルクォート系
2815
+ if (code >= 0x2018 && code <= 0x201B) {
2816
+ return "'";
2817
+ }
2818
+
2819
+ // ダブルクォート系
2820
+ if (code >= 0x201C && code <= 0x201F) {
2821
+ return '"';
2822
+ }
2823
+
2824
+ return ch;
2825
+ }).join("");
2826
+ };
2827
+
2828
+ /**
2829
+ * ASCII入力欄に日本語IMEで入った文字をASCIIへ矯正する
2830
+ *
2831
+ * 注意:
2832
+ * - これは「半角化」ではなく「IMEオフ入力相当への寄せ」
2833
+ * - ascii() とは責務が異なる
2834
+ *
2835
+ * @returns {Rule}
2836
+ */
2837
+ function imeOff() {
2838
+ return {
2839
+ name: "imeOff",
2840
+ targets: ["input", "textarea"],
2841
+
2842
+ normalizeChar(value, ctx) {
2843
+ return toImeOff(value);
2844
+ }
2845
+ };
2846
+ }
2847
+
2848
+ /**
2849
+ * dataset から imeOff ルールを生成する
2850
+ *
2851
+ * 対応する data 属性
2852
+ * - data-tig-rules-ime-off
2853
+ *
2854
+ * @param {DOMStringMap} dataset
2855
+ * @param {HTMLInputElement|HTMLTextAreaElement} _el
2856
+ * @returns {Rule|null}
2857
+ */
2858
+ imeOff.fromDataset = function fromDataset(dataset, _el) {
2859
+ if (dataset.tigRulesImeOff == null) {
2860
+ return null;
2861
+ }
2862
+
2863
+ return imeOff();
2864
+ };
2865
+
2708
2866
  /**
2709
2867
  * The script is part of Mojix for TextInputGuard.
2710
2868
  *
@@ -5926,7 +6084,7 @@
5926
6084
  function ascii(options = {}) {
5927
6085
  /** @type {AsciiRuleOptions} */
5928
6086
  const opt = {
5929
- case: options.case ?? null
6087
+ case: options.case ?? "none"
5930
6088
  };
5931
6089
 
5932
6090
  return {
@@ -5939,6 +6097,10 @@
5939
6097
  // まず半角へ正規化
5940
6098
  s = Mojix.toHalfWidthAsciiCode(s);
5941
6099
 
6100
+ // toHalfWidthAsciiCode で対応できていない文字も実施
6101
+ s = s.replace(/\uFFE5/g, "\u005C"); //¥
6102
+ s = s.replace(/[\u2010-\u2015\u2212\u30FC\uFF0D\uFF70]/g, "\u002D"); //ハイフンに似ている記号
6103
+
5942
6104
  // 英字の大文字/小文字統一
5943
6105
  if (opt.case === "upper") {
5944
6106
  s = s.toUpperCase();
@@ -6933,7 +7095,7 @@
6933
7095
  code: "bytes.max_overflow",
6934
7096
  rule: "bytes",
6935
7097
  phase: "validate",
6936
- detail: { max: opt.max, actual: len }
7098
+ detail: { limit: opt.max, actual: len }
6937
7099
  });
6938
7100
  }
6939
7101
  }
@@ -7240,6 +7402,7 @@
7240
7402
  { name: "numeric", fromDataset: numeric.fromDataset },
7241
7403
  { name: "digits", fromDataset: digits.fromDataset },
7242
7404
  { name: "comma", fromDataset: comma.fromDataset },
7405
+ { name: "imeOff", fromDataset: imeOff.fromDataset },
7243
7406
  { name: "kana", fromDataset: kana.fromDataset },
7244
7407
  { name: "ascii", fromDataset: ascii.fromDataset },
7245
7408
  { name: "filter", fromDataset: filter.fromDataset },
@@ -7264,6 +7427,7 @@
7264
7427
  numeric,
7265
7428
  digits,
7266
7429
  comma,
7430
+ imeOff,
7267
7431
  kana,
7268
7432
  ascii,
7269
7433
  filter,
@@ -7277,11 +7441,11 @@
7277
7441
 
7278
7442
  /**
7279
7443
  * バージョン(ビルド時に置換したいならここを差し替える)
7280
- * 例: rollup replace で ""0.2.0"" を package.json の version に置換
7444
+ * 例: rollup replace で ""0.2.2"" を package.json の version に置換
7281
7445
  */
7282
7446
  // @ts-ignore
7283
7447
  // eslint-disable-next-line no-undef
7284
- const version = "0.2.0" ;
7448
+ const version = "0.2.2" ;
7285
7449
 
7286
7450
  exports.ascii = ascii;
7287
7451
  exports.attach = attach;
@@ -7291,6 +7455,7 @@
7291
7455
  exports.comma = comma;
7292
7456
  exports.digits = digits;
7293
7457
  exports.filter = filter;
7458
+ exports.imeOff = imeOff;
7294
7459
  exports.kana = kana;
7295
7460
  exports.length = length;
7296
7461
  exports.numeric = numeric;