全角数字を半角数字へ変換する JavaScript:IME入力で文字が重複しない版
Copyright Notice: This article is an original work licensed under the CC 4.0 BY-NC-ND license.
If you wish to repost this article, please include the original source link and this copyright notice.
Source link: https://v2know.com/article/1317
ASP.NET WebForms や普通の HTML の TextBox で、ユーザーに数字を入力してもらう画面はよくあります。
ただ、日本語入力環境では 123 や ー123.2 のように、全角数字・全角小数点・長音符のマイナスが入力されることがあります。
この記事では、全角数字を半角数字へ変換しつつ、IME変換中に input イベントで value を書き換えて文字が重複する問題を避ける JavaScript を紹介します。
結論
ポイントは、compositionstart から compositionend までの間は、入力欄の value を変更しないことです。
IME合成中に el.value = ... のような処理をしてしまうと、環境によっては次の入力時に文字が重複することがあります。たとえば、入力したつもりは 123 なのに、途中で 112 や 112123 のような不自然な値になるケースです。
実際に試せるサンプル
下の入力欄はそのまま試せます。全角数字や全角小数点を入力してみてください。
ー123.2 → -123.2123 → 123、1000 → 999123.2 → 123.2、123.23 → 123.2ー123.2 → -123.2、ー1000 → -999.9使い方
入力欄に用途別の class を付けます。
<input type="text" class="ig-normalize-only">
<input type="text" class="ig-int0to999">
<input type="text" class="ig-dec0to999-1">
<input type="text" class="ig-dec-neg999_9">
ASP.NET WebForms の TextBox なら、次のように CssClass に指定します。
<asp:TextBox ID="txtNumber1" runat="server" CssClass="ig-int0to999" />
<asp:TextBox ID="txtRate1" runat="server" CssClass="ig-dec0to999-1" />
<asp:TextBox ID="txtAmount1" runat="server" CssClass="ig-dec-neg999_9" />
class の意味
ig-normalize-only:全角数字・全角小数点・マイナス系文字を半角へ変換するだけig-int0to999:整数のみ。範囲は 0~999ig-dec0to999-1:小数1位まで。範囲は 0~999.9ig-dec-neg999_9:小数1位まで。範囲は -999.9~999.9
なぜ keydown だけでは足りないのか
半角数字だけを許可したい場合、keydown で数字以外を止める実装をよく見ます。
しかし、日本語IMEで全角数字を入力する場合、キーを押した時点ではまだ変換中です。ブラウザ上では keyCode が 229 になったり、key が Process になったりします。
そのため、keydown だけで完全に制御しようとすると、IME入力と相性が悪くなります。
このサンプルでは、通常の半角入力は keydown である程度制限し、IME経由の入力は compositionend と input で安全に整形します。
変換対象
このサンプルでは、次の文字を変換対象にしています。
0~9→0~9.→.ー、-、−→-
JavaScript
以下の JavaScript をページに1回だけ読み込めば、同じ class を付けた複数の TextBox にまとめて適用できます。
(function () {
"use strict";
function isNavKey(key) {
return (
key === "Backspace" ||
key === "Delete" ||
key === "Tab" ||
key === "Enter" ||
key === "Escape" ||
key === "ArrowLeft" ||
key === "ArrowRight" ||
key === "ArrowUp" ||
key === "ArrowDown" ||
key === "Home" ||
key === "End"
);
}
function isCtrlCombo(e) {
return e.ctrlKey || e.metaKey;
}
// 全角数字・全角小数点・マイナス系文字を半角へ変換する
function normalizeNumberText(value) {
return String(value || "").replace(/[0-9.ー-−]/g, function (ch) {
if (ch >= "0" && ch <= "9") {
return String.fromCharCode(ch.charCodeAt(0) - 0xFEE0);
}
if (ch === ".") {
return ".";
}
if (ch === "ー" || ch === "-" || ch === "−") {
return "-";
}
return ch;
});
}
// IME合成中は value を書き換えない
function bindImeSafeInput(el, handler) {
el._igIsComposing = false;
el.addEventListener("compositionstart", function () {
el._igIsComposing = true;
});
el.addEventListener("compositionend", function () {
el._igIsComposing = false;
handler();
});
el.addEventListener("input", function (e) {
if (e.isComposing || el._igIsComposing) {
return;
}
handler();
});
}
// 1. 変換だけ
function bindNormalizeOnly(el) {
if (el._igBoundNormalizeOnly) return;
el._igBoundNormalizeOnly = true;
bindImeSafeInput(el, function () {
el.value = normalizeNumberText(el.value);
});
}
// 2. 整数 0~999
function bindInt0to999(el) {
if (el._igBoundInt0to999) return;
el._igBoundInt0to999 = true;
el.addEventListener("keydown", function (e) {
if (isNavKey(e.key) || isCtrlCombo(e)) return;
// IME合成中は止めない
if (e.isComposing || e.key === "Process" || e.keyCode === 229) return;
if (!/^[0-9]$/.test(e.key)) {
e.preventDefault();
}
});
bindImeSafeInput(el, function () {
var v = normalizeNumberText(el.value);
v = v.replace(/[^\d]/g, "");
v = v.replace(/^0+(?=\d)/, "");
if (v === "") {
el.value = "";
return;
}
var n = parseInt(v, 10);
if (isNaN(n)) {
el.value = "";
} else if (n > 999) {
el.value = "999";
} else {
el.value = String(n);
}
});
}
// 3. 小数 0~999.9(小数1位)
function bindDecimal0to999_1(el) {
if (el._igBoundDecimal0to999_1) return;
el._igBoundDecimal0to999_1 = true;
el.addEventListener("keydown", function (e) {
if (isNavKey(e.key) || isCtrlCombo(e)) return;
if (e.isComposing || e.key === "Process" || e.keyCode === 229) return;
if (!/^[0-9.]$/.test(e.key)) {
e.preventDefault();
return;
}
if (e.key === "." && el.value.indexOf(".") >= 0) {
e.preventDefault();
}
});
bindImeSafeInput(el, function () {
var v = normalizeNumberText(el.value);
v = v.replace(/[^0-9.]/g, "");
var parts = v.split(".");
if (parts.length > 2) {
v = parts[0] + "." + parts.slice(1).join("");
}
var idx = v.indexOf(".");
if (idx >= 0) {
var a = v.substring(0, idx);
var b = v.substring(idx + 1).substring(0, 1);
a = a.replace(/^0+(?=\d)/, "");
if (a === "") a = "0";
if (parseInt(a, 10) > 999) {
a = "999";
}
v = a + "." + b;
} else {
v = v.replace(/^0+(?=\d)/, "");
if (v !== "") {
var n = parseInt(v, 10);
if (!isNaN(n) && n > 999) {
v = "999";
}
}
}
el.value = v;
});
}
// 4. 小数 -999.9~999.9(小数1位)
function bindDecimalNeg999_9to999_9_1(el) {
if (el._igBoundDecimalNeg999_9) return;
el._igBoundDecimalNeg999_9 = true;
el.addEventListener("keydown", function (e) {
if (isNavKey(e.key) || isCtrlCombo(e)) return;
if (e.isComposing || e.key === "Process" || e.keyCode === 229) return;
if (!/^[0-9.-]$/.test(e.key)) {
e.preventDefault();
return;
}
if (e.key === "." && el.value.indexOf(".") >= 0) {
e.preventDefault();
return;
}
if (e.key === "-") {
if (el.value.indexOf("-") >= 0 || el.selectionStart !== 0) {
e.preventDefault();
}
}
});
bindImeSafeInput(el, function () {
var v = normalizeNumberText(el.value);
v = v.replace(/[^0-9.-]/g, "");
var neg = v.indexOf("-") >= 0;
v = v.replace(/-/g, "");
if (neg) {
v = "-" + v;
}
var sign = "";
if (v.indexOf("-") === 0) {
sign = "-";
v = v.substring(1);
}
var parts = v.split(".");
if (parts.length > 2) {
v = parts[0] + "." + parts.slice(1).join("");
}
var idx = v.indexOf(".");
if (idx >= 0) {
var a = v.substring(0, idx);
var b = v.substring(idx + 1).substring(0, 1);
v = a + "." + b;
}
v = sign + v;
// 入力途中の "-" は残す
if (v === "" || v === "-") {
el.value = v;
return;
}
if (v === "." || v === "-.") {
el.value = "";
return;
}
var num = parseFloat(v);
if (isNaN(num)) {
el.value = "";
return;
}
if (num > 999.9) num = 999.9;
if (num < -999.9) num = -999.9;
var s = String(num);
if (s.indexOf(".") >= 0) {
var p = s.split(".");
s = p[0] + "." + (p[1] || "").substring(0, 1);
}
el.value = s;
});
}
function bindAllNumberInputs() {
var i;
var list;
list = document.querySelectorAll(".ig-normalize-only");
for (i = 0; i < list.length; i++) bindNormalizeOnly(list[i]);
list = document.querySelectorAll(".ig-int0to999");
for (i = 0; i < list.length; i++) bindInt0to999(list[i]);
list = document.querySelectorAll(".ig-dec0to999-1");
for (i = 0; i < list.length; i++) bindDecimal0to999_1(list[i]);
list = document.querySelectorAll(".ig-dec-neg999_9");
for (i = 0; i < list.length; i++) bindDecimalNeg999_9to999_9_1(list[i]);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bindAllNumberInputs);
} else {
bindAllNumberInputs();
}
})();
注意点
- この処理はクライアント側の入力補助です。DB登録前やサーバー側のチェックは別途必要です。
- 入力途中の見た目を優先したい場合は、
inputではなくblur時だけ整形する形に変えることもできます。
まとめ
全角数字を半角数字へ変換する処理自体は簡単ですが、日本語IMEを考慮する場合は compositionstart、compositionend、input の扱いが重要です。
特に、IME合成中に入力欄の値を書き換えないこと。これを守るだけで、文字の重複や変換中の違和感をかなり避けられます。
This article was last edited at