遭遇
今まで、タイトルの状況に遭遇した事が無かったのです。年に1回くらいはフォーム関連のスクリプトは書いていたのに、なぜ今更と思ったんですが、良く考えるとjQuery使用していたり、IEを対象にしていないサイトだったりだったので、遭遇しなかったんだと思います。しかし、昨年辺りから「普通のサイトでも脱jQueryしたい」と思ってVanilla JSでコードを書くようになったわけです。
Vanilla JSってなんやねんって方はこちら
なぜ、脱jQueryをしたかったかというと、フルリニューアルを一度もしていない10年以上運営しているサイトがとんでもなく肥大化してしまっていて、JSファイル、CSSファイル多すぎて、GoogleのPageSpeed Insightを通すと、その当たりを、指摘されまくる訳です。地味に、jQueryって容量大きいので、できる限りjQueryを読み込まないようにしていこう思い立って、それからVanilla JSで書くようになりました。IE 10/11対応のサイトをプラグインを使用せずにフォームのスクリプトをフルスクラッチで書いていたら、タイトルの状況に遭遇したって訳です。
IE 10/11とplaceholderとinputイベントの問題
この現象は、IE 10/11のバグのようです。placeholderが設定されている<input>や<textarea>で、inputイベント(oninput)を設定していると、placeholderの表示・非表示の切り替わり時に、inputイベントが発火するというものです。リアルタイムバリエーションとかしないのであれば無視できますが、1文字毎の入力に対してバリエーション表示をしてあげた方が親切なので、なんとかしたいバグですね。
解決方法を探していたら以下のような解決策が出てきます。
こちらのように、placeholderを使用しないとか、inputイベントではなく、keyupを使うとかっていう解決方法が大抵ヒットします。
placeholderを使わなければ良い?
placeholderを使わないというのはUI/UXの面からも一理あります。
ただし、記述例自体は載せておいた方がいいサイトなどもあり、その際にはをフォーム外に記載することになります。記述例のベストな位置は入力欄の近く(よほど特殊なレイアウト出ない限り上か下)になるので、フォームの縦の長さ(高さ)が増えます。
私の場合は、チーム内で、縦に伸びることによるCVR低下を危惧する人がいて、「placeholder不要派」と「高さ増えるの嫌派」の戦いが勃発しました。結論、うちのサイトではCVRは、高さ増える方が悪くなってしまったので、placeholderを使うことに落ち着きました。そんな事情もあり、placeholder自体を無くすのは難しくなっています。
keyupにすれば良い?
一方、keyupを使うというのは、一見なるほどと思ったものの、そもそも挙動違うのに大丈夫か?と思って置き換えてみましたが、結論だめでした。tabキーで次の入力欄に移動した際に、oninputは移動前の入力欄で発火してほしいところですが、keyupだと、tabキーを押した瞬間に次の入力欄にフォーカスが移動してしまい、tabキーを離した時には、フォーカス移動後の入力欄でkeyupイベントが発火します。簡潔にいうと、
- oninputだと、tabキーを押して次の入力欄に移動した時に、移動前の入力欄でイベントが発火する。
- keyupだと、tabキーを押して次の入力欄に移動した時に、移動後の入力欄でイベントが発火する。
という違いがあります。同じような理由で、keydownも駄目です。こうなると、初回入力フラグを用意して、keyupとkeydown併用でなんとかするしかないのか?色々面倒な処理になりそうだなと思っていて、もっとスマートな解決策はないのかと思っていたら、まぁまぁ良い解決策が見つかりました。
良さそうな解決策
以下のStackOverflowの記事にありました。
IE 10, 11. How to prevent the triggering of input events on focus from text input with placeholder?
こちらはで、一番支持されている回答に以下のコードが選ばれておりました。
function onInputWraper(cb) {
if (!window.navigator.userAgent.match(/MSIE|Trident/)) return cb;
return function (e) {
var t = e.target,
active = (t == document.activeElement);
if (!active || (t.placeholder && t.composition_started !== true)) {
t.composition_started = active;
if ((!active && t.tagName == 'TEXTAREA') || t.tagName == 'INPUT') {
e.stopPropagation();
e.preventDefault();
return false;
}
}
cb(e);
};
}
var el = document.getElementById('myEmail');
el.addEventListener("input", onInputWraper(myFunction), true);
function myFunction() {
alert("changed");
}
上記の例では、inputイベントの関数のラッパーを作り、IE以外は、何もせず、IEの時に色々処理をし、inputイベントが発火してほしく無い時にイベントをキャンセルしています。
<input>でのイベント発火の時を考えると、
- イベントが起こった時に、アクティブな要素かどうかを判定して、アクティブでは無い時には、そもそもinputイベントをキャンセル。
- 初回フォーカス時には、placeholderが非表示になりますが、この時は、アクティブになり、ターゲット要素のcomposition_startedがまだfalseなので、compsition_startedをtrueにして、イベントをキャンセル。
- 入力すると、compsition_startedがtrueなので、キャンセルせず、inputイベントが発火します。入力中は、composition_startedがtrueのままなので、そのままinputイベントは発火し続けます。
- フォーカスが外れた時に、アクティブじゃなくなるので、composition_startedをfalseにして、イベントをキャンセル。
という風になっています。
composition_startedって、compositionstartイベント関係のもともとJavaScriptが持っているプロパティかなと思ったら、ただ入力中であることを意味するプロパティを作成し設定しているだけのようでした。
ひとまず、この対応で様子見しようと思います。
しっかし、まだIEに悩まされるとは・・・うちのサイトはIEユーザーがまだほどほどに居るから仕方ないですね。