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.
@@ -163,6 +163,7 @@ class SwapState {
163
163
  // raw化(送信担当)
164
164
  input.type = "hidden";
165
165
  input.removeAttribute("id");
166
+ input.removeAttribute("class");
166
167
  input.className = "";
167
168
  input.dataset.tigRole = "raw";
168
169
 
@@ -170,6 +171,9 @@ class SwapState {
170
171
  if (this.originalId) {
171
172
  input.dataset.tigOriginalId = this.originalId;
172
173
  }
174
+ if (this.originalClass) {
175
+ input.dataset.tigOriginalClass = this.originalClass;
176
+ }
173
177
  if (this.originalName) {
174
178
  input.dataset.tigOriginalName = this.originalName;
175
179
  }
@@ -190,7 +194,10 @@ class SwapState {
190
194
  display.id = this.originalId;
191
195
  }
192
196
 
193
- display.className = this.originalClass ?? "";
197
+ if (this.originalClass) {
198
+ display.className = this.originalClass;
199
+ }
200
+
194
201
  display.value = raw.value;
195
202
 
196
203
  for (const [name, v] of Object.entries(this.originalUiAttrs)) {
@@ -251,6 +258,7 @@ class SwapState {
251
258
 
252
259
  delete raw.dataset.tigRole;
253
260
  delete raw.dataset.tigOriginalId;
261
+ delete raw.dataset.tigOriginalClass;
254
262
  delete raw.dataset.tigOriginalName;
255
263
  }
256
264
  }
@@ -396,7 +404,7 @@ class SwapState {
396
404
  * @typedef {Object} AttachOptions
397
405
  * @property {Rule[]} [rules] - 適用するルール配列(順番がフェーズ内実行順になる)
398
406
  * @property {boolean} [warn] - 非対応ルールなどを console.warn するか
399
- * @property {string} [invalidClass] - エラー時に付けるclass名
407
+ * @property {string} [invalidClass="is-invalid"] - エラー時に付けるclass名
400
408
  * @property {SeparateValueOptions} [separateValue] - 表示値と内部値の分離設定
401
409
  * @property {(result: ValidateResult) => void} [onValidate] - 評価完了時の通知(input/commitごと)
402
410
  */
@@ -444,6 +452,28 @@ function warnLog(msg, warn) {
444
452
  }
445
453
  }
446
454
 
455
+ /**
456
+ * input / textarea 要素と内部 Guard インスタンスの対応表
457
+ *
458
+ * - key: displayElement
459
+ * - value: InputGuard(内部実装)
460
+ *
461
+ * @type {WeakMap<HTMLInputElement|HTMLTextAreaElement, InputGuard>}
462
+ */
463
+ const guardMap = new WeakMap();
464
+
465
+ document.addEventListener("selectionchange", () => {
466
+ const el = document.activeElement;
467
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
468
+ return;
469
+ }
470
+ const inputGuard = guardMap.get(el);
471
+ if (!inputGuard) {
472
+ return;
473
+ }
474
+ inputGuard.onSelectionChange();
475
+ });
476
+
447
477
  /**
448
478
  * 指定した1要素に対してガードを適用し、Guard API を返す
449
479
  * @param {HTMLInputElement|HTMLTextAreaElement} element
@@ -451,9 +481,12 @@ function warnLog(msg, warn) {
451
481
  * @returns {Guard}
452
482
  */
453
483
  function attach(element, options = {}) {
454
- const guard = new InputGuard(element, options);
455
- guard.init();
456
- return guard.getGuard();
484
+ const inputGuard = new InputGuard(element, options);
485
+ inputGuard.init();
486
+ const guard = inputGuard.getGuard();
487
+ const display = guard.getDisplayElement();
488
+ guardMap.set(display, inputGuard);
489
+ return guard;
457
490
  }
458
491
 
459
492
  /**
@@ -549,7 +582,7 @@ class InputGuard {
549
582
  /**
550
583
  * ユーザーが直接入力する表示側要素
551
584
  * swapしない場合は originalElement と同一
552
- * @type {HTMLElement}
585
+ * @type {HTMLInputElement|HTMLTextAreaElement}
553
586
  */
554
587
  this.displayElement = element;
555
588
 
@@ -671,6 +704,12 @@ class InputGuard {
671
704
  */
672
705
  this.pendingCompositionCommit = false;
673
706
 
707
+ /**
708
+ * selection 更新のフレーム予約ID
709
+ * @type {number|null}
710
+ */
711
+ this.selectionFrameId = null;
712
+
674
713
  /**
675
714
  * 直前に受理した表示値、正しい情報のスナップショットのような情報(block時の戻し先)
676
715
  * @type {string}
@@ -716,9 +755,11 @@ class InputGuard {
716
755
  * @returns {SelectionState}
717
756
  */
718
757
  readSelection(el) {
758
+ const start = el.selectionStart ?? 0;
759
+ const end = el.selectionEnd ?? start;
719
760
  return {
720
- start: el.selectionStart,
721
- end: el.selectionEnd,
761
+ start,
762
+ end,
722
763
  direction: el.selectionDirection
723
764
  };
724
765
  }
@@ -832,6 +873,8 @@ class InputGuard {
832
873
  * @returns {void}
833
874
  */
834
875
  detach() {
876
+ // 管理マップから削除
877
+ guardMap.delete(this.displayElement);
835
878
  // イベント解除(displayElementがswap後の可能性があるので先に外す)
836
879
  this.unbindEvents();
837
880
  // swap復元
@@ -892,15 +935,7 @@ class InputGuard {
892
935
  this.displayElement.addEventListener("input", this.onInput);
893
936
  this.displayElement.addEventListener("beforeinput", this.onBeforeInput);
894
937
  this.displayElement.addEventListener("blur", this.onBlur);
895
-
896
- // フォーカスで編集用に戻す
897
938
  this.displayElement.addEventListener("focus", this.onFocus);
898
-
899
- // キャレット/選択範囲の変化を拾う(block時の不自然ジャンプ対策)
900
- this.displayElement.addEventListener("keyup", this.onSelectionChange);
901
- this.displayElement.addEventListener("mouseup", this.onSelectionChange);
902
- this.displayElement.addEventListener("select", this.onSelectionChange);
903
- this.displayElement.addEventListener("focus", this.onSelectionChange);
904
939
  }
905
940
 
906
941
  /**
@@ -914,10 +949,6 @@ class InputGuard {
914
949
  this.displayElement.removeEventListener("beforeinput", this.onBeforeInput);
915
950
  this.displayElement.removeEventListener("blur", this.onBlur);
916
951
  this.displayElement.removeEventListener("focus", this.onFocus);
917
- this.displayElement.removeEventListener("keyup", this.onSelectionChange);
918
- this.displayElement.removeEventListener("mouseup", this.onSelectionChange);
919
- this.displayElement.removeEventListener("select", this.onSelectionChange);
920
- this.displayElement.removeEventListener("focus", this.onSelectionChange);
921
952
  }
922
953
 
923
954
  /**
@@ -1285,12 +1316,41 @@ class InputGuard {
1285
1316
  * @returns {void}
1286
1317
  */
1287
1318
  onSelectionChange() {
1288
- // IME変換中は無視(この間はキャレット位置が不安定になることがあるため)
1289
- if (this.composing) {
1290
- return;
1319
+ const requestFrame =
1320
+ typeof requestAnimationFrame === "function"
1321
+ ? requestAnimationFrame
1322
+ : (
1323
+ /** @param {FrameRequestCallback} cb */
1324
+ (cb) => setTimeout(cb, 0)
1325
+ );
1326
+
1327
+ const cancelFrame =
1328
+ typeof cancelAnimationFrame === "function"
1329
+ ? cancelAnimationFrame
1330
+ : clearTimeout;
1331
+
1332
+ // すでに予約済みならキャンセル(selectionchange は連続発火するため)
1333
+ if (this.selectionFrameId != null) {
1334
+ cancelFrame(this.selectionFrameId);
1291
1335
  }
1292
- const el = /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.displayElement);
1293
- this.lastAcceptedSelection = this.readSelection(el);
1336
+
1337
+ this.selectionFrameId = requestFrame(() => {
1338
+ this.selectionFrameId = null;
1339
+
1340
+ // IME変換中は無視(キャレット位置が不安定になるため)
1341
+ if (this.composing) {
1342
+ return;
1343
+ }
1344
+
1345
+ const el = /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.displayElement);
1346
+
1347
+ // 要素がフォーカスされていない場合は無視
1348
+ if (document.activeElement !== el) {
1349
+ return;
1350
+ }
1351
+
1352
+ this.lastAcceptedSelection = this.readSelection(el);
1353
+ });
1294
1354
  }
1295
1355
 
1296
1356
  /**
@@ -2699,6 +2759,104 @@ comma.fromDataset = function fromDataset(dataset, _el) {
2699
2759
  return comma();
2700
2760
  };
2701
2761
 
2762
+ /**
2763
+ * The script is part of TextInputGuard.
2764
+ *
2765
+ * AUTHOR:
2766
+ * natade-jp (https://github.com/natade-jp)
2767
+ *
2768
+ * LICENSE:
2769
+ * The MIT license https://opensource.org/licenses/MIT
2770
+ */
2771
+
2772
+ /**
2773
+ * IMEオフ入力相当の文字変換テーブル
2774
+ * @type {Record<string, string>}
2775
+ */
2776
+ /* eslint-disable quote-props */
2777
+ const IME_OFF_MAP = {
2778
+ "\u3000": "\u0020", // 全角スペース → space
2779
+ "\u3001": "\u002C", // 、 → ,
2780
+ "\u3002": "\u002E", // 。 → .
2781
+ "\u300C": "\u005B", // 「 → [
2782
+ "\u300D": "\u005D", // 」 → ]
2783
+ "\u301C": "\u007E", // 〜 → ~
2784
+ "\u30FC": "\u002D", // ー → -
2785
+ "\uFFE5": "\u005C" // ¥ → \
2786
+ };
2787
+ /* eslint-enable quote-props */
2788
+
2789
+ /**
2790
+ * ASCII入力欄に日本語IMEで入った文字をASCIIへ矯正する
2791
+ * @param {string} text - 変換したいテキスト
2792
+ * @returns {string} 変換後のテキスト
2793
+ */
2794
+ const toImeOff = function (text) {
2795
+ return Array.from(String(text), (ch) => {
2796
+ // 個別マップ
2797
+ if (ch in IME_OFF_MAP) {
2798
+ return IME_OFF_MAP[ch];
2799
+ }
2800
+
2801
+ const code = ch.charCodeAt(0);
2802
+
2803
+ // 全角ASCII
2804
+ if (code >= 0xFF01 && code <= 0xFF5E) {
2805
+ return String.fromCharCode(code - 0xFEE0);
2806
+ }
2807
+
2808
+ // シングルクォート系
2809
+ if (code >= 0x2018 && code <= 0x201B) {
2810
+ return "'";
2811
+ }
2812
+
2813
+ // ダブルクォート系
2814
+ if (code >= 0x201C && code <= 0x201F) {
2815
+ return '"';
2816
+ }
2817
+
2818
+ return ch;
2819
+ }).join("");
2820
+ };
2821
+
2822
+ /**
2823
+ * ASCII入力欄に日本語IMEで入った文字をASCIIへ矯正する
2824
+ *
2825
+ * 注意:
2826
+ * - これは「半角化」ではなく「IMEオフ入力相当への寄せ」
2827
+ * - ascii() とは責務が異なる
2828
+ *
2829
+ * @returns {Rule}
2830
+ */
2831
+ function imeOff() {
2832
+ return {
2833
+ name: "imeOff",
2834
+ targets: ["input", "textarea"],
2835
+
2836
+ normalizeChar(value, ctx) {
2837
+ return toImeOff(value);
2838
+ }
2839
+ };
2840
+ }
2841
+
2842
+ /**
2843
+ * dataset から imeOff ルールを生成する
2844
+ *
2845
+ * 対応する data 属性
2846
+ * - data-tig-rules-ime-off
2847
+ *
2848
+ * @param {DOMStringMap} dataset
2849
+ * @param {HTMLInputElement|HTMLTextAreaElement} _el
2850
+ * @returns {Rule|null}
2851
+ */
2852
+ imeOff.fromDataset = function fromDataset(dataset, _el) {
2853
+ if (dataset.tigRulesImeOff == null) {
2854
+ return null;
2855
+ }
2856
+
2857
+ return imeOff();
2858
+ };
2859
+
2702
2860
  /**
2703
2861
  * The script is part of Mojix for TextInputGuard.
2704
2862
  *
@@ -5920,7 +6078,7 @@ kana.fromDataset = function fromDataset(dataset, _el) {
5920
6078
  function ascii(options = {}) {
5921
6079
  /** @type {AsciiRuleOptions} */
5922
6080
  const opt = {
5923
- case: options.case ?? null
6081
+ case: options.case ?? "none"
5924
6082
  };
5925
6083
 
5926
6084
  return {
@@ -5933,6 +6091,10 @@ function ascii(options = {}) {
5933
6091
  // まず半角へ正規化
5934
6092
  s = Mojix.toHalfWidthAsciiCode(s);
5935
6093
 
6094
+ // toHalfWidthAsciiCode で対応できていない文字も実施
6095
+ s = s.replace(/\uFFE5/g, "\u005C"); //¥
6096
+ s = s.replace(/[\u2010-\u2015\u2212\u30FC\uFF0D\uFF70]/g, "\u002D"); //ハイフンに似ている記号
6097
+
5936
6098
  // 英字の大文字/小文字統一
5937
6099
  if (opt.case === "upper") {
5938
6100
  s = s.toUpperCase();
@@ -6927,7 +7089,7 @@ function bytes(options = {}) {
6927
7089
  code: "bytes.max_overflow",
6928
7090
  rule: "bytes",
6929
7091
  phase: "validate",
6930
- detail: { max: opt.max, actual: len }
7092
+ detail: { limit: opt.max, actual: len }
6931
7093
  });
6932
7094
  }
6933
7095
  }
@@ -7234,6 +7396,7 @@ const auto = new InputGuardAutoAttach(attach, [
7234
7396
  { name: "numeric", fromDataset: numeric.fromDataset },
7235
7397
  { name: "digits", fromDataset: digits.fromDataset },
7236
7398
  { name: "comma", fromDataset: comma.fromDataset },
7399
+ { name: "imeOff", fromDataset: imeOff.fromDataset },
7237
7400
  { name: "kana", fromDataset: kana.fromDataset },
7238
7401
  { name: "ascii", fromDataset: ascii.fromDataset },
7239
7402
  { name: "filter", fromDataset: filter.fromDataset },
@@ -7258,6 +7421,7 @@ const rules = {
7258
7421
  numeric,
7259
7422
  digits,
7260
7423
  comma,
7424
+ imeOff,
7261
7425
  kana,
7262
7426
  ascii,
7263
7427
  filter,
@@ -7271,10 +7435,10 @@ const rules = {
7271
7435
 
7272
7436
  /**
7273
7437
  * バージョン(ビルド時に置換したいならここを差し替える)
7274
- * 例: rollup replace で ""0.2.0"" を package.json の version に置換
7438
+ * 例: rollup replace で ""0.2.2"" を package.json の version に置換
7275
7439
  */
7276
7440
  // @ts-ignore
7277
7441
  // eslint-disable-next-line no-undef
7278
- const version = "0.2.0" ;
7442
+ const version = "0.2.2" ;
7279
7443
 
7280
- export { ascii, attach, attachAll, autoAttach, bytes, comma, digits, filter, kana, length, numeric, prefix, rules, suffix, trim, version, width };
7444
+ export { ascii, attach, attachAll, autoAttach, bytes, comma, digits, filter, imeOff, kana, length, numeric, prefix, rules, suffix, trim, version, width };