JavaScriptで数字のみの入力ボックスを作る(全角対応)
今回説明するのは電話番号や郵便番号といった、半角数字だけが入力できる数字入力ボックスの作り方になります。
応用すればハイフンを含む場合や半角英数字だけの入力ボックスも簡単に作れます。
本記事の途中までは半角数字だけの話で、途中からもし全角数字も許容するならの話になります。
完成品はこちらに置いています。
type="number"
を使わない
まず、数字を入力するときにやりがちな間違いは、次のようにtype="number"
を使ってしまうことです。
<input type="number" name="zip"> // Bad
type="number"
はあくまで『数値』を入力するためのものだからです。
数字を入力するケースとして電話番号の場合はtype="tel"
を使って、それ以外の場合はtype="text"
とinputmode
属性を一緒に使います。
<input type="tel" name="tel"> // Good
<input type="text" name="id" inputmode="numeric"> // Good
<input type="text" name="zip" inputmode="tel"> // Good
inputmode
属性を使えばブラウザに対して数値と誤解されることなく、数字のみのインターフェースで入力ができます。
数字以外の入力を制限する
スマホでは数字のみが入力できるようになりましたが、物理キーボードがあるとどんな文字でも入力できてしまいます。また、スマホのスクリーンキーボードでもカンマやハイフンが入力できる場合もあります。
これらの文字は入力値を送信するときや、入力直後にバリデーションが動くことで、ユーザーに修正をもとめることになります。それでもOKですが、より良いUIにしたいなら、そもそも入力できないようにするほうが良さそうです。
そこで、JavaScriptを使ってinput
イベントを受け取って数字以外の文字を削除します。
input
イベントを扱う方法は、利用しているフレームワークに依存します。本記事ではJitoを使って説明しますが、VueやReact等でも書き方が異なるだけでやることは同じです。
input
イベントを受信して半角の0~9以外の文字を削除して再設定するコンポーネントはこちらです。
import { compact, mount } from 'https://cdn.jsdelivr.net/gh/ittedev/jito@1.4.0/jito.js'
let main = () => {
function input(event) {
event.target.value = event.target.value
.replace(/[^0-9]/g, '')
}
return { input }
}
let html = /* html */`
<input
type="text"
inputmode="numeric"
oninput="input(event)"
>
<style>
/* 略 */
</style>
`
export default compact(html, main)
input
イベントを発火させない
IMEが有効のときはさて、先の方法では、replace()
メソッドで0~9以外の文字を削除していました。
英語圏ではこれで問題ないと思いますが、ここ日本だと、日本語入力モードのときに何も入力できないという問題が生じます。
日本語入力モードでは数字のキーを押すとまず変換前の全角数字が入力されますので、半角数字ではないため入力できないのは当たり前です。
inputmode
属性を適切に設定していれば、フォーカスが当たったときに自動的にIMEがOFFになりますので大きな問題とは言えませんが、もしユーザーがついついIMEを切り替えてしまったら、数字のキーを押しているのに画面に何も表示されないという状態になり、ユーザーからすると「?」です。離脱に繋がる恐れもあります。
まー、実は私が潜在的にIMEは基本ONになっているものと認識しているのか、半角で入力する場面で無意識に変換キーを押してしまうときがあるんですよね。
・・・私だけ?であればこの記事はここでおしまいです。読んでいただきありがとうございました。
さて私のような人が居ると仮定すると、日本語入力モード(IMEが有効)のときに、先のinput
イベントが反応しないようにして、変換が確定したときに初めてinput
イベントを発火させるようにしたいです。
input
イベントにはisComposing
フラグというのがあって、IMEが有効のときはこのフラグが立つのでIMEが有効かどうかを判定できます。このフラグはinput
イベントがcompositionstart
イベントより後でcompositionend
イベントより前の場合にisComposing
フラグがtrueになります。
しかし、変換確定時のinput
イベントは、Firefoxはcompositionend
イベントより前に発生し、
Chromeはcompositionend
イベントより後に発生します。そのためブラウザごとにisComposing
フラグの値にばらつきがあります。しかも、Safariは2023年3月と最近対応したばかりです。
この問題を解消し、少し古いSafariにも対応する方法は、長いですが次のようなイベントセットを作ります(詳細は後で説明します)。
function createThroughEvents() {
let isComposing = false
let isFirefox = window.navigator.userAgent.toLowerCase().includes('firefox')
return {
oncompositionstart() {
isComposing = true
},
oncompositionend(event) {
if (isComposing) {
isComposing = false
if (!isFirefox) {
event.target.dispatchEvent(new InputEvent('input', {
inputType: 'insertText',
data: event.data,
isComposing: false
}))
}
}
},
onkeydown(event) {
if (isComposing && event.code === 'Enter') {
isComposing = false
}
},
oninput(event) {
if (event.inputType === 'insertFromComposition') {
isComposing = false
}
if (isComposing) {
event.stopImmediatePropagation()
}
},
}
}
実装手順
このイベントセットは次の手順で作りました。VueやReact、JQueryなどでやる場合もこの手順で作ってみてください。
isComposing
フラグ
手順1 オリジナルのcompositionstart
イベントでONになり、compositionend
イベントでOFFになる、オリジナルのisComposing
フラグを作ります。
この時点でisComposing
フラグはinput
イベントのものと全く同じ挙動になります。
let isComposing = false
oncompositionstart() {
isComposing = true
},
oncompositionend(event) {
isComposing = false
},
isComposing
フラグでinput
イベントを無効化
手順2 input
イベントでisComposing
フラグがONのときに、stopImmediatePropagation()
メソッドで後続のinput
イベントが実行されないようにします。
この時点でFirefoxは対応完了になります。
oninput(event) {
if (isComposing) {
event.stopImmediatePropagation()
}
},
手順3 Safariへの対応
Safariでは変換確定時にinputType
プロパティが'insertFromComposition'
になっているinput
イベントが発行されます。
手順2の直前にinputType
プロパティが'insertFromComposition'
だった場合はisComposing
フラグをOFFにします。Safariへの対応完了です。
oninput(event) {
+ if (event.inputType === 'insertFromComposition') {
+ isComposing = false
+ }
if (isComposing) {
event.stopImmediatePropagation()
}
}
手順4 Chromeへの対応
ChromeやEdgeのようなChromiumベースのブラウザは、compositionend
イベントの前に変換確定のinput
イベントが動く上に、Safariのような他に変換確定を判定するためのプロパティがありません。そのため、compositionend
イベントのときにあえてもう一回input
イベントを発火させます。これでChromeも対応完了です。
oncompositionend(event) {
isComposing = false;
+ event.target.dispatchEvent(new InputEvent('input', {
+ inputType: 'insertText',
+ data: event.data,
+ isComposing: false
+ }))
},
手順5 無駄なイベント発生を減らす①
以上で完了なのですが、手順4のcompositionend
イベントでinput
イベントを発火させるときに、Safariは変換完了のinput
イベントが既に発火しており、Firefoxはわざわざ発火しなくてもこのあとで自然発火します。そのためSafariとFirefoxでは無駄なinput
イベントが発火することになってしまいます。これを防ぐためにChromiumベースのブラウザだけで発火させたいです。
そこでisComposing
フラグがOFFでFirefox以外のときに発火させることにします。
let isFirefox = window.navigator.userAgent.toLowerCase().includes('firefox')
oncompositionend(event) {
+ if (isComposing) {
isComposing = false;
+ if (!isFirefox) {
event.target.dispatchEvent(new InputEvent('input', {
inputType: 'insertText',
data: event.data,
isComposing: false
}))
+ }
+ }
},
手順6 無駄なイベント発生を減らす②
更に無駄なinput
イベントを抑えたければ、ChromiumベースのブラウザでEnterキーを押して変換確定する場合は、Keydown
イベントを受け取ってisComposing
フラグをOFFにします。
onkeydown(event) {
if (isComposing && event.code === 'Enter') {
isComposing = false;
}
},
カスタム要素に設定
今回はJitoで作りましたので1~6の各手順で作ったイベントセットを、先の<input>要素にチャンクとして付与します。input
イベントの実行順序が重要ですので、oninput
よりも前に書く必要があります。
let main = () => {
function input(event) {
event.target.value = event.target.value
.replace(/[0-9]/g, c => String.fromCharCode(c.charCodeAt(0) - 0xFEE0))
.replace(/[^0-9]/g, '')
}
+ let throughEvents = createThroughEvents()
return { input, throughEvents }
}
let html = /* html */`
<input
type="text"
inputmode="numeric"
+ @chunk="throughEvents"
oninput="input(event)"
>
<style>
/* 略 */
</style>
`
export default compact(html, main)
これで日本語入力中はinput
イベントが実行されず、入力確定時にinput
イベントが発火するinputボックスができました。
全角数字も入力できるようにする
IMEの問題が解消されましたので、全角数字が入力されたら自動的に半角数字に変換されるようにしてみます。
これは単純にinput
イベントで変換すればできます。
function input(event) {
event.target.value = event.target.value
+ .replace(/[0-9]/g, c => String.fromCharCode(c.charCodeAt(0) - 0xFEE0))
.replace(/[^0-9]/g, '')
}
応用
郵便番号
応用で、数字の途中でハイフンやカンマを入れたい場合は、input
イベントで変換を工夫すれば可能です。
function input(event) {
event.target.value = event.target.value
.replace(/[0-9]/g, c => String.fromCharCode(c.charCodeAt(0) - 0xFEE0))
.replace(/[^0-9]/g, '')
+ .slice(0, 7)
+ .split('')
+ .reduce((r, c, i, s) => r + c + (i === 2 && (s.length > 3 || event.target.value[3] === '-') ? '-' : ''), '')
}
半角英数字だけの入力ボックス
他にも、半角英数字だけを許可する場合はこんな感じです。
function input(event) {
event.target.value = event.target.value
.replace(/[0-9a-zA-Z]/g, c => String.fromCharCode(c.charCodeAt(0) - 0xFEE0))
.replace(/[^0-9a-zA-Z]/g, '')
}
いい感じの入力ボックスができました。
IMEを含めてUIを考えるとなかなか骨が折れますね。
Discussion