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.
- package/README.md +14 -6
- package/dist/cjs/text-input-guard.cjs +194 -29
- package/dist/cjs/text-input-guard.min.cjs +1 -1
- package/dist/esm/text-input-guard.js +194 -30
- package/dist/esm/text-input-guard.min.js +1 -1
- package/dist/types/text-input-guard.d.ts +26 -1
- package/dist/umd/text-input-guard.js +194 -29
- package/dist/umd/text-input-guard.min.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -398,7 +406,7 @@ class SwapState {
|
|
|
398
406
|
* @typedef {Object} AttachOptions
|
|
399
407
|
* @property {Rule[]} [rules] - 適用するルール配列(順番がフェーズ内実行順になる)
|
|
400
408
|
* @property {boolean} [warn] - 非対応ルールなどを console.warn するか
|
|
401
|
-
* @property {string} [invalidClass] - エラー時に付けるclass名
|
|
409
|
+
* @property {string} [invalidClass="is-invalid"] - エラー時に付けるclass名
|
|
402
410
|
* @property {SeparateValueOptions} [separateValue] - 表示値と内部値の分離設定
|
|
403
411
|
* @property {(result: ValidateResult) => void} [onValidate] - 評価完了時の通知(input/commitごと)
|
|
404
412
|
*/
|
|
@@ -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
|
|
457
|
-
|
|
458
|
-
|
|
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 {
|
|
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
|
|
723
|
-
end
|
|
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
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
1295
|
-
this.
|
|
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
|
/**
|
|
@@ -2701,6 +2761,104 @@ comma.fromDataset = function fromDataset(dataset, _el) {
|
|
|
2701
2761
|
return comma();
|
|
2702
2762
|
};
|
|
2703
2763
|
|
|
2764
|
+
/**
|
|
2765
|
+
* The script is part of TextInputGuard.
|
|
2766
|
+
*
|
|
2767
|
+
* AUTHOR:
|
|
2768
|
+
* natade-jp (https://github.com/natade-jp)
|
|
2769
|
+
*
|
|
2770
|
+
* LICENSE:
|
|
2771
|
+
* The MIT license https://opensource.org/licenses/MIT
|
|
2772
|
+
*/
|
|
2773
|
+
|
|
2774
|
+
/**
|
|
2775
|
+
* IMEオフ入力相当の文字変換テーブル
|
|
2776
|
+
* @type {Record<string, string>}
|
|
2777
|
+
*/
|
|
2778
|
+
/* eslint-disable quote-props */
|
|
2779
|
+
const IME_OFF_MAP = {
|
|
2780
|
+
"\u3000": "\u0020", // 全角スペース → space
|
|
2781
|
+
"\u3001": "\u002C", // 、 → ,
|
|
2782
|
+
"\u3002": "\u002E", // 。 → .
|
|
2783
|
+
"\u300C": "\u005B", // 「 → [
|
|
2784
|
+
"\u300D": "\u005D", // 」 → ]
|
|
2785
|
+
"\u301C": "\u007E", // 〜 → ~
|
|
2786
|
+
"\u30FC": "\u002D", // ー → -
|
|
2787
|
+
"\uFFE5": "\u005C" // ¥ → \
|
|
2788
|
+
};
|
|
2789
|
+
/* eslint-enable quote-props */
|
|
2790
|
+
|
|
2791
|
+
/**
|
|
2792
|
+
* ASCII入力欄に日本語IMEで入った文字をASCIIへ矯正する
|
|
2793
|
+
* @param {string} text - 変換したいテキスト
|
|
2794
|
+
* @returns {string} 変換後のテキスト
|
|
2795
|
+
*/
|
|
2796
|
+
const toImeOff = function (text) {
|
|
2797
|
+
return Array.from(String(text), (ch) => {
|
|
2798
|
+
// 個別マップ
|
|
2799
|
+
if (ch in IME_OFF_MAP) {
|
|
2800
|
+
return IME_OFF_MAP[ch];
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
const code = ch.charCodeAt(0);
|
|
2804
|
+
|
|
2805
|
+
// 全角ASCII
|
|
2806
|
+
if (code >= 0xFF01 && code <= 0xFF5E) {
|
|
2807
|
+
return String.fromCharCode(code - 0xFEE0);
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
// シングルクォート系
|
|
2811
|
+
if (code >= 0x2018 && code <= 0x201B) {
|
|
2812
|
+
return "'";
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// ダブルクォート系
|
|
2816
|
+
if (code >= 0x201C && code <= 0x201F) {
|
|
2817
|
+
return '"';
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
return ch;
|
|
2821
|
+
}).join("");
|
|
2822
|
+
};
|
|
2823
|
+
|
|
2824
|
+
/**
|
|
2825
|
+
* ASCII入力欄に日本語IMEで入った文字をASCIIへ矯正する
|
|
2826
|
+
*
|
|
2827
|
+
* 注意:
|
|
2828
|
+
* - これは「半角化」ではなく「IMEオフ入力相当への寄せ」
|
|
2829
|
+
* - ascii() とは責務が異なる
|
|
2830
|
+
*
|
|
2831
|
+
* @returns {Rule}
|
|
2832
|
+
*/
|
|
2833
|
+
function imeOff() {
|
|
2834
|
+
return {
|
|
2835
|
+
name: "imeOff",
|
|
2836
|
+
targets: ["input", "textarea"],
|
|
2837
|
+
|
|
2838
|
+
normalizeChar(value, ctx) {
|
|
2839
|
+
return toImeOff(value);
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
/**
|
|
2845
|
+
* dataset から imeOff ルールを生成する
|
|
2846
|
+
*
|
|
2847
|
+
* 対応する data 属性
|
|
2848
|
+
* - data-tig-rules-ime-off
|
|
2849
|
+
*
|
|
2850
|
+
* @param {DOMStringMap} dataset
|
|
2851
|
+
* @param {HTMLInputElement|HTMLTextAreaElement} _el
|
|
2852
|
+
* @returns {Rule|null}
|
|
2853
|
+
*/
|
|
2854
|
+
imeOff.fromDataset = function fromDataset(dataset, _el) {
|
|
2855
|
+
if (dataset.tigRulesImeOff == null) {
|
|
2856
|
+
return null;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
return imeOff();
|
|
2860
|
+
};
|
|
2861
|
+
|
|
2704
2862
|
/**
|
|
2705
2863
|
* The script is part of Mojix for TextInputGuard.
|
|
2706
2864
|
*
|
|
@@ -5922,7 +6080,7 @@ kana.fromDataset = function fromDataset(dataset, _el) {
|
|
|
5922
6080
|
function ascii(options = {}) {
|
|
5923
6081
|
/** @type {AsciiRuleOptions} */
|
|
5924
6082
|
const opt = {
|
|
5925
|
-
case: options.case ??
|
|
6083
|
+
case: options.case ?? "none"
|
|
5926
6084
|
};
|
|
5927
6085
|
|
|
5928
6086
|
return {
|
|
@@ -5935,6 +6093,10 @@ function ascii(options = {}) {
|
|
|
5935
6093
|
// まず半角へ正規化
|
|
5936
6094
|
s = Mojix.toHalfWidthAsciiCode(s);
|
|
5937
6095
|
|
|
6096
|
+
// toHalfWidthAsciiCode で対応できていない文字も実施
|
|
6097
|
+
s = s.replace(/\uFFE5/g, "\u005C"); //¥
|
|
6098
|
+
s = s.replace(/[\u2010-\u2015\u2212\u30FC\uFF0D\uFF70]/g, "\u002D"); //ハイフンに似ている記号
|
|
6099
|
+
|
|
5938
6100
|
// 英字の大文字/小文字統一
|
|
5939
6101
|
if (opt.case === "upper") {
|
|
5940
6102
|
s = s.toUpperCase();
|
|
@@ -6929,7 +7091,7 @@ function bytes(options = {}) {
|
|
|
6929
7091
|
code: "bytes.max_overflow",
|
|
6930
7092
|
rule: "bytes",
|
|
6931
7093
|
phase: "validate",
|
|
6932
|
-
detail: {
|
|
7094
|
+
detail: { limit: opt.max, actual: len }
|
|
6933
7095
|
});
|
|
6934
7096
|
}
|
|
6935
7097
|
}
|
|
@@ -7236,6 +7398,7 @@ const auto = new InputGuardAutoAttach(attach, [
|
|
|
7236
7398
|
{ name: "numeric", fromDataset: numeric.fromDataset },
|
|
7237
7399
|
{ name: "digits", fromDataset: digits.fromDataset },
|
|
7238
7400
|
{ name: "comma", fromDataset: comma.fromDataset },
|
|
7401
|
+
{ name: "imeOff", fromDataset: imeOff.fromDataset },
|
|
7239
7402
|
{ name: "kana", fromDataset: kana.fromDataset },
|
|
7240
7403
|
{ name: "ascii", fromDataset: ascii.fromDataset },
|
|
7241
7404
|
{ name: "filter", fromDataset: filter.fromDataset },
|
|
@@ -7260,6 +7423,7 @@ const rules = {
|
|
|
7260
7423
|
numeric,
|
|
7261
7424
|
digits,
|
|
7262
7425
|
comma,
|
|
7426
|
+
imeOff,
|
|
7263
7427
|
kana,
|
|
7264
7428
|
ascii,
|
|
7265
7429
|
filter,
|
|
@@ -7273,11 +7437,11 @@ const rules = {
|
|
|
7273
7437
|
|
|
7274
7438
|
/**
|
|
7275
7439
|
* バージョン(ビルド時に置換したいならここを差し替える)
|
|
7276
|
-
* 例: rollup replace で ""0.2.
|
|
7440
|
+
* 例: rollup replace で ""0.2.2"" を package.json の version に置換
|
|
7277
7441
|
*/
|
|
7278
7442
|
// @ts-ignore
|
|
7279
7443
|
// eslint-disable-next-line no-undef
|
|
7280
|
-
const version = "0.2.
|
|
7444
|
+
const version = "0.2.2" ;
|
|
7281
7445
|
|
|
7282
7446
|
exports.ascii = ascii;
|
|
7283
7447
|
exports.attach = attach;
|
|
@@ -7287,6 +7451,7 @@ exports.bytes = bytes;
|
|
|
7287
7451
|
exports.comma = comma;
|
|
7288
7452
|
exports.digits = digits;
|
|
7289
7453
|
exports.filter = filter;
|
|
7454
|
+
exports.imeOff = imeOff;
|
|
7290
7455
|
exports.kana = kana;
|
|
7291
7456
|
exports.length = length;
|
|
7292
7457
|
exports.numeric = numeric;
|