これでできる! クロスブラウザJavaScript入門

第1回ウェブブラウザとJavaScriptの未来

こんにちは、id:os0xこと太田昌吾です。今回から、クロスブラウザ対策を中心としたJavaScriptの初級から中級の方向けの連載を開始します。JavaScriptの基礎的な文法は理解されているという前提での解説となりますので、ご了承ください(間違いやすい、わかり難いと思われるところは適宜補足します⁠⁠。初回である今回はJavaScriptやウェブブラウザの背景など盛りだくさんの内容でお届けします。

JavaScriptのイマ

JavaScriptは2010年現在において、最も重要な言語となりつつあります。旧来はすべての処理をサーバーで行って、結果をウェブブラウザ上に表示するだけというのがウェブの一般的な姿でした。2005年に登場したGoogle Mapsを一つの契機として徐々にウェブブラウザ・クライアント側での処理が見直され始め、近年ではクラウドやSaas、そしてHTML5の流行によって、多くの処理をクライアント側で行うことが当たり前になってきています。特にクラウドにおいては、クライアント側での処理やキャッシュコントロールによってサーバーの負荷を抑えることで、コストの削減に直結できるというメリットもあります。

このクライアント側での処理について、その処理系はJavaScriptでほぼ決まりといってよいでしょう。対抗馬と言えるActionScript(Flash)は、開発環境にコストがかかる(フリーのSDKも存在はしますが⁠⁠、環境によってはクラッシュしやすく不安定という問題を抱えているといった理由からか、Flashが実際に採用されることは少なくなっています。多くのウェブアプリケーションで使われているのはJavaScriptです。もはや、JavaScriptが最も重要な言語であることは疑いようがないように思えます。

しかし、JavaScriptは多くのウェブ開発者にとって悩みの種となっているようです。JavaScriptのベースであるECMAScriptはコンパクトでわかりやすい仕様ですし、2009年に10年ぶりの改定となるECMAScript 5がリリースされましたが、それでも大きな仕様変更はありませんでした。それほど安定しているにも関わらず、JavaScript自体を苦手とされる方が多い背景には、クロスブラウザという課題があるためです。

主なJavaScriptエンジンには、IEのJScript、FirefoxのSpiderMonkey(TraceMonkey⁠⁠、SafariのJavaScriptCore(Nitro⁠⁠、ChromeのV8、OperaのFutharkやCharakanなどがあります。これらの実装の違いが多くの開発者の悩みの種となっています。さらに、各ブラウザはレンダリングエンジンも異なるため(SafariとChromeは同一のエンジン(WebKit)ですが、バージョン違いに悩まされることも少なくありません⁠⁠、HTMLやスタイルシートについてもエンジンごとの違いを把握しておく必要があります。

さらに、各ブラウザは頻繁にバージョンアップを行っており、古いバージョンも使われ続けることが多いため、各バージョンごとの違いにも注意する必要があります。特にIEについてはIE 6、IE 7、IE 8とバージョンが分散してしまっています。MicrosoftはIE 6のサポートを続けていますし、依然として一定のユーザーが存在するため、多くのケースでIE 6をサポートすることになります。IE 6をサポートしないというのも1つの選択ですが、どちらにしてもIE 7をサポートするのであれば、あと少し(少しでないケースもありますが…)の手間でIE 6もサポートできるので、あえてIE 6だけをサポートしない理由もあまりないでしょう。

と、問題ばかりのように書いてしまいましたが、実際にJavaScriptを扱う際は、ライブラリを使用することでクロスブラウザ対応のコストを大きく下げることができます。特に人気があるのはjQueryで、JavaScriptではない別言語といわれるほどに強力なライブラリです。そのほか、prototype.jsやdojo、YUI LibraryにGoogle Closure Library、日本製ではuupaa.jsなどの汎用的なライブラリは多数あります。これらを利用することで、クロスブラウザの問題をそれほど意識することなくJavaScriptを書くことは可能です。とはいえ、ライブラリに頼っていては限界があり、特に何か問題が起きた時の対処に苦労することが多くなります。

ウェブブラウザの種類とクセ

前置きが思いのほか長くなってしましたが、この連載のメインテーマはクロスブラウザです。⁠普段ライブラリ(特にjQuery)を使ってJavaScriptを書いている方向けに、ライブラリを使わないでクロスブラウザな実装をしてみる」というケーススタディを中心にお送りたいと思います。

さて、クロスブラウザというからには、その対応ブラウザを明確にしなければいけません。まず、一口にクロスブラウザ対応と言っても、その対応にも種類があります。例えば、YUIでは、サポートの段階をA-grade(フルサポートするブラウザ⁠⁠、C-grade(基本機能のみサポートするブラウザ⁠⁠、X-grade(不明もしくはレアなブラウザ)の3つに分けています(参考:YUI Graded Browser Support⁠。この連載でも、フルサポート(すべての動作が意図どおりに動く、積極的な対応)とベーシックサポート(最低限動けばよい、消極的な対応)を区別します。では、現在使用されているブラウザの種類とバージョンの特徴をまとめたいと思います。

まずは、標準仕様との関係を整理してみます。図1にECMAScriptとブラウザを中心とした仕様の関係をまとめてみました(HTML5関連の仕様は今回は関係性が弱いので、広義な意味でのHTML5としていますが、今後の連載ではそちらも扱っていきます⁠⁠。

図1 ブラウザの種類と標準仕様
図1 ブラウザの種類と標準仕様

どのブラウザもECMA-262 3rd editionとDOM Level 1までは概ね足並みが揃っています(DOM Level 0はまた特殊ですが、ここでは触れません⁠⁠。大きな問題は、IEがDOM Level 2に相当する部分を独自の仕様で実装している点です。この連載でも、この部分をたびたび取り上げることになります。また、IEのみECMAScript 5関連の実装が遅れている点もポイントです。正確にはIE8がECMAScript 5のnative JSONをサポートしているので(ただし仕様に準拠するには修正パッチを当てる必要がありますが⁠⁠、まったく手つかずというわけではないのですが、ほかのブラウザからは大きく乖離しています。

では、もう少し詳細に各ブラウザごとの特徴と、バージョンごとの対応についてざっくりとまとめます。ここではあえて大雑把に主観的なまとめをしており、詳細は連載の中で随時言及していきます。サポートするしないといった記述もありますが、この判断は2010年3月時点での筆者の主観にすぎない点をご了承ください。

IE(Internet Explorer)

シェアNo.1のIE。独自の仕様・実装に悩まされますが、バージョンごとの互換性は安定しています。

IE 5(もしくはそれ未満)

サポートの必要性はまずありません。

IE 5.5

特に理由がなければ、サポートする必要はありません。もしもサポートする場合、CSSが後方互換モードしかない点や、JavaScriptではArray.prototype.pushをサポートしていないなど、問題は多く、サポートには大きなコストがかかりますし、一方でそれに見合うメリットがあることは滅多にないでしょう。メジャーなJavaScriptライブラリも基本的にIE5.5をサポートしていません。

IE 6

最も悩ましいバージョンです。IE 6をサポートしないという選択をすれば、サイト構築・運営にかかるコストを抑えられるという面は確かにあるでしょう。ただし、IE 6に対応するためのノウハウ・バグは、これまで頻繁に話題になってきたので、少し調べれば情報が出てくるという特徴もあります(マイナーなブラウザでは自力で解決するしかないことが多いことに比べれば、利点と言えなくもないでしょう⁠⁠。例えば、position:fixedができない、透過PNGを表示できないといった問題はそのまま検索すればすぐに対応方法が見つかります。jQueryやPrototype.js、YUIなどメジャーなライブラリもIE 6をサポートしています。

ただ、最近ではGoogleが積極的にIE 6のサポートを終了するなど、IE 6に対応しないという風潮も出てきています。例えば、新しいモノ好きな人向けのサイトでIE 6を積極的にサポートする必要性は弱くなるでしょうから、場合によってもIE 6を基本機能しかサポートしないという選択も考えられます。

IE 7

IE 6をサポートした時点で、IE 7もサポートできていることが多いので、確認は後回しにしてしまっても問題ないバージョンです。逆に言えば、IE 7をサポートした時点で、IE 6のサポートはあと少しであることが多くなります。

IE 8

CSSの実装はかなり優秀です。Firefox、Webkitをターゲットに書いたCSS(もちろんCSS2.1の範囲ですが)をIE 8ではそのまま同様に表示できることも珍しくありません。JavaScriptについては主にDOMのイベント処理周りが依然として独自実装ですが、やはりIE 6で動けばIE 8でも動くので特別な対応をする必要はほとんどないでしょう。

Firefox

着実にシェアを伸ばしているFirefoxは、多くのユーザーを抱えていることもあり、後方互換はしっかりしています。Mozilla Developer Center(MDC)に素晴らしいドキュメントがあり、HTML/CSS/JavaScriptの標準仕様への準拠にも積極的なので、ウェブブラウザのスタンダードといえる存在です。ただし、現在のところJavaScriptについては独自実装が多いので注意が必要です。

Firefox 1.5(もしくはそれ未満)

サポートの必要性はまずありません。

Firefox 2

Firefoxを提供しているMozilla自体がFirefox 2のサポートを終了しているため、サポートしなければいけない理由はありません。しかし、現実にはFirefox 2を使い続けるユーザーは存在します。IE 5.5などに比べればFirefox 2のサポートは難しくないので、基本機能が動作しているか確認するレベルでも問題ないでしょう。

なお、CSSについては、display:inline-blockを使用する際に注意が必要だったりと、少々問題が出ることもあります。JavaScriptについてはECMA-262 3rd editionベースであれば、問題が出ることは少ないでしょう。

Firefox 3.0

HTML/CSS/JavaScriptを安定してサポートした模範的なウェブブラウザといえます。それゆえにFirefox 3.0での表示、動作は1つの指針となります(ただし、前述の通りJavaScriptについては独自実装が多いので注意が必要です⁠⁠。

Firefox 3.5、3.6

基本的にはFirefox 3.0と同様です。ただし、JavaScriptエンジンにJITが採用され大幅な高速化が行われており、一部の処理ではIE 6などの古いブラウザと大きな差があるため、Firefox 3.5基準の(IEから見たら)重いJavaScriptを書かないように注意する必要があります。

Safari

Firefoxに並んで、HTML/CSS/JavaScriptの標準化(特にHTML5関連)を牽引する存在です。しかし、ドキュメントが揃っていない、バージョンごとの細かな違いを調べることが難しい、異なるバージョンを同時にインストールする手段が(公式には)ないなど、やや問題もあるブラウザです。最近のバージョンではWeb Inspectorというデバッグツールが標準で搭載されており、デバッグが容易になっています。

Safari 1.3、2.0

特に理由がなければ、サポートしていなくても問題ないでしょう。JavaScriptを使用しないサイトであればそれなりに表示できるはずですが、Safari 1.3、2.0でのJavaScript周りをサポートするのは情報が少ないため容易ではありません。主要なJavaScriptライブラリでは、prototype.jsのみがSafari 2.0をサポートしているといった状況です。基本的に自力でサポートする必要があり、そのコストに見合うだけのメリットがあるかは疑問です。もしサポートする必要がある場合は、Safari の JavaScript の不備などが参考になります。

Safari 3.0.4

Safariのなかで、最もやっかいなバージョンです。CSS周りでの問題は少ないですが、JavaScript関連では、例えばaddEventListenerをサポートしているのに、DOMContentLoadedをサポートしていないなど、少々癖のあるバージョンです。とはいえ、主要なライブラリはSafari3.0からのサポートなので、ライブラリに頼れる範囲では難しくないでしょう。シェアとしては小さいので、サポートしないという選択も考えられます。

Safari 3.1、3.2

Safariの中では比較的安定したバージョンです。DOM/CSSのサポートはそこそこ、JavaScriptの処理速度などもそれなりに高速です。3.0.4に対応した後に、確認する程度でも十分かもしれません。

Safari 4.0.4

2010年3月時点での最新版です。DOM/CSSのサポートは十分なので、標準仕様に合わせて実装すれば自然とサポートできるはずです。

Google Chrome

レンダリングエンジンはSafariと同じWebKitを使用し、JavaScriptエンジンは独自開発した高速なV8を搭載しています。V8はSafariのJavaScriptエンジンとの互換性を重視しているため、ほぼSafariと同様に動作しますし、デバッガとしてWeb Inspectorも使用できます。デフォルトでブラウザの更新が自動で行われるため、ユーザーのほとんどが最新版を使用している点も大きな特徴です。そのため旧バージョンを積極的にサポートする必要はありません。サポートするとしても、どのバージョンもSafari 3.2やFirefox 3と同じかそれ以上にDOM/CSSのサポートが充実しているので、問題が出ることは少なくなっています。ただし、WebKitのバージョンがSafariと同じではないので、その微妙な差が問題になることもあります。

Opera

日本でのシェアは今一つですが、一昔前のハードウェアでも比較的快適に動作するという特徴のためか、一部の地域では大きなシェアを持っていたりと、なかなかクセのあるブラウザです。

Opera 9.2x(もしくはそれ未満)

すでにほとんど使われていないので、特にサポートする必要はありません。

Opera 9.5、9.6、10.x

DOM/CSSのサポートはそこそこで、IEやFirefoxとの互換性にも配慮されているため、一般的なサイトをなるべく標準的な仕様に従って作成すれば、Operaでも正常に動作することがほとんどです。ただし、ところどころでクセが強いため、右クリックのイベントが取れなかったり(ユーザーが明示的に許可した場合のみ可能)とイベント周りで特殊な動作をすることが多くなっています(仕様に対して忠実に実装した結果であることが多く、Operaのバグというわけでもないといったこともあります⁠⁠。また、何か問題があった際は参考となる情報を見つけることが難しく、自力で解決しなければいけないことが多くなっています。

Opera 10.50

2010年3月にリリースされたOpera 10.50はJavaScriptの実行速度が大幅に高速化されており、HTML5などのサポートも大幅に進んでいます。基本的にはそれ以前のバージョンと変わりありません。

以上、ざっくりと主要ブラウザの特徴を眺めてみました。これら以外にも、Sleipnir(IEのエンジンであるTridentを使用)やLunascape(Trident、Gecko、WebKitと3つのエンジンを搭載)などのブラウザが使われていますが、基本的にエンジンは同じなので、特別に対応しないといけないことはほとんどありません(あったとしたら、そのブラウザのバグか仕様かもしれません⁠⁠。

ケーススタディ:入力値チェック#1

ここまででもかなりのボリュームとなってしまいましたが、ここからは実際にクロスブラウザなコードを実践して書いていきます。

今回は、最も使われるであろうフォームの入力値チェックです。まず、以下のようなよくあるユーザー登録フォームのHTMLを用意しました。

フォームのHTML
<form id="userform" name="userform" action="" method="post">
  <fieldset>
    <legend>ユーザー登録</legend>
    <p>
      <label for="username">ユーザー名</label>
      <input type="text" name="name" value="" id="username" class="text" required="required">
    </p>
    <p>
      <label for="useremail">メールアドレス</label>
      <input type="text" name="email" value="" id="useremail" class="text" required="required">
    </p>
    <p>
      <label for="userpass">パスワード</label>
      <input type="password" name="pass" value="" id="userpass" class="text" required="required">
    </p>
    <p>
      <label for="userpass_sub">パスワード(確認)</label>
      <input type="password" name="pass2" value="" class="text" id="userpass_sub">
    </p>
    <p class="submit">
      <input type="submit" id="usersubmit" value="送信" class="button" id="usersubmit">
    </p>
    <p id="errors"></p>
  </fieldset>
</form>

まず、必須項目のチェック、パスワードの一致のチェックという基本的な処理をjQueryを使って実装してみます。

送信ボタンを押した際に、各テキストボックスが空だったらその要素の背景色を黄色にして、メッセージを表示(3秒後に自動的に隠れる)します。パスワードは一致していなかった場合も背景色を黄色に変え、メッセージを表示します。最初にエラーになった要素に自動的にフォーカスを戻し、フォーカスが当たった要素は背景色を白に戻すようにします。

jQueryを使った実装
jQuery(function($){
  //htmlの構築を待つ
  var query = '#userform input:text,#userform input:password';
  var $inputs = $(query).focus(function(evt){
    $(this).css('background','white');
    // inputにフォーカスしたとき背景色を白に戻す
  }); // input要素はキャッシュしておく
  var message = {name:'ユーザー名',email:'メールアドレス',pass:'パスワード'};
  var $password = $('#userform input:password');
  $('#userform').submit(function(evt){ // 送信時の処理
    var isError = false, errors = [];
    var noBlank = ($inputs.filter(function(i,input){
      // requiredになっていて、値が入力されてないinputをフィルタリング
      return input.required && !input.value && errors.push(message[input.name]);
      // エラーメッセージ用に空だったinputに対応するメッセージを取得
    }).css('background','#ffff66').eq(0).focus().size() === 0);
    // フィルタされた要素について背景色を黄色にして1つ目の要素にフォーカスをあてる、
    // その時の要素数が0でなければ空のinputがあったとみなす
    var pass = $password.map(function(i,v){return v.value});
     // passwordの値を取得して配列に入れる
    var matchedPass = pass[0] === pass[1];
     // パスワードの一致を確認
    if (noBlank && !matchedPass) {
      // 入力漏れがなくて、パスワードが一致しない場合
      $password.css('background','#ffff66').eq(0).focus();
      $('#errors').text('パスワードが一致していません');
      isError = true;
    }
    if (errors.length) { // エラーメッセージが存在する場合
      $('#errors').text(errors.join('、')+'は必須です');
      isError = true;
    }
    if (isError) {
      $('#errors').show().delay(3000).hide('fast');
       //エラーメッセージを表示して、3秒後に隠す
    }
    return noBlank && matchedPass;
  });
});

やや強引ですが、なるべくコンパクトな(コメントを除いて28行ほど)実装にしました。もちろんこれは実装の一例であり、改善の余地は多くありますが、jQueryについてはこれ以上触れません。

続いて、同等の機能をjQueryを使わずに実装してみます。

ライブラリを使わない入力値チェック実装
(function(){
  var addEvent;
  if(document.addEventListener) {
    addEvent = function(node,type,handler){
      node.addEventListener(type,handler,false);
    };
  } else if (document.attachEvent) {
    addEvent = function(node,type,handler){
      node.attachEvent('on' + type, function(evt){
        handler.call(node, evt);
      });
    };
  } else {
    addEvent = function(node,type,handler){
      var _handler = node['on' + type];
      node['on' + type] = function(evt){
        if (_handler) {
          _handler.call(node, evt||window.evt);
        }
        handler.call(node, evt);
      };
    };
  }
  var message = {name:'ユーザー名',email:'メールアドレス',pass:'パスワード'};
  addEvent(window,'load',function(){
    var form = document.getElementById('userform');
    var errorNode = document.getElementById('errors');
    var inputNodes = form.getElementsByTagName('input');
    var passwords = [];
    var timer;
    for (var i = 0, inputsLen = inputNodes.length;i < inputsLen; i++){
      var input = inputNodes[i];
      if (input.type === 'text' || input.type === 'password') {
        addEvent(input,'focus',function(){
          this.style.background = 'white';
        });
        if (input.type === 'password') {
          passwords.push(input);
        }
      }
    }
    addEvent(form,'submit',function(evt){
      var noBlank = true;
      var errors = [];
      for (var i = 0;i < inputsLen; i++){
        var input = inputNodes[i];
        if (input.getAttribute('required') && !input.value) {
          input.style.background = '#ffff66';
          if(noBlank) {
            input.focus();
            noBlank = false;
          }
          errors.push(message[input.name]);
        }
      }
      var matchedPass = passwords[0].value === passwords[1].value;
      if (noBlank && !matchedPass) {
        passwords[0].focus();
        passwords[1].style.background = '#ffff66';
        errorNode.innerHTML = 'パスワードが一致していません';
      }
      if (!noBlank) {
        errorNode.innerHTML = errors.join('、')+'は必須です';
      }
      if (!noBlank || !matchedPass) {
        if (evt.preventDefault) {
          evt.preventDefault();
        } else {
          evt.returnValue = false;
        }
        errorNode.style.display = 'block';
        if (timer) {
          clearTimeout(timer);
          timer = null;
        }
        timer = setTimeout(function(){
          errorNode.style.display = 'none';
          timer = null;
        }, 3000);
      }
    });
  })
})();

一気に80行ほどのコードになってしまいました。こちらを順に解説していきます。

まず最初はイベント登録用関数です。

イベント登録用関数
  var addEvent;//変数を宣言
  if(document.addEventListener) {
    //addEventListenerが使える場合(IE以外)
    addEvent = function(node,type,handler){
      node.addEventListener(type,handler,false);
    };
  } else if (document.attachEvent) {// IE用
    addEvent = function(node,type,handler){
      node.attachEvent('on' + type, function(evt){
        handler.call(node, evt);
      });
    };
  } else {
    //以下は古いブラウザ用のコードで、
    // 省略しても問題ありません
    addEvent = function(node,type,handler){
      // すでにイベントが定義されているかもしれないので変数に入れて置く
      var _handler = node['on' + type];
      node['on' + type] = function(evt){
        if (_handler) {// 定義済みであった場合に呼び出す
          _handler.call(node, evt||window.event);
        }
        handler.call(node, evt);
      };
    };
  }

このaddEventを文字列にしてみると、

IE以外のブラウザ
function(node,type,handler){
  node.addEventListener(type,handler,false);
}
IEの場合
function(node,type,handler){
  node.attachEvent('on' + type, function(evt){
    handler.call(node, evt);
  });
}

となっており、ブラウザの実装ごとに関数の定義を分けておくことができ、処理を最適化できます。よく使う関数でクロスブラウザ対応が必要な場合は、このように予めブラウザごとに定義する方法がよく使われます。

読み込み待ちと初期化
  addEvent(window,'load',function(){
    var form = document.getElementById('userform');
    var errorNode = document.getElementById('errors');
    var inputNodes = form.getElementsByTagName('input');
    var passwords = [];
    /* 続く */
  });

早速、上で定義したaddEventを使って、windowのloadイベントを待ちます。ロード後はその後使用する要素をdocument.getElementByIdで取得しておきます。使用する度にgetElementByIdを使うのではなく、予め取得してローカルの変数にしておけば、実際使用するときにローカル変数として高速にアクセスできます。フォーム内のinput要素は、フォーム要素からのgetElementsByTagName('input')で取得しています。フォーム要素を起点にすることで、そのフォーム内の要素だけを取得できます。また、フォーム要素は自分自身の持つ要素を保持しているので、getElementsByTagNameを使わなくてもinput要素を取得することは可能です。

フォームの走査#1
for (var i = 0, inputsLen = form.length;i < inputsLen; i++){
  var input = form[i];
}
フォームの走査#2
for (var i = 0, inputsLen = form.elements.length;i < inputsLen; i++){
  var input = form.elements[i];
}

ただし、この場合input要素以外の要素も含まれる点に注意が必要です。特に、Safari、Chromeではfieldset要素が含まれず、それ以外のブラウザではfieldsetも含まれるなどブラウザごとの細かな挙動の違いもあります。

続いて、テキストインプットにフォーカスが当たった際に背景色を白に戻す処理を実装しておきます。また、passwordは後で一致をチェックするのでここで配列に入れておきます。

テキストボックスのフォーカス処理
for (var i = 0, inputsLen = inputNodes.length;i < inputsLen; i++){
  var input = inputNodes[i];
  if (input.type === 'text' || input.type === 'password') {
    addEvent(input,'focus',function(){
      this.style.background = 'white';
    });
    if (input.type === 'password') {
      passwords.push(input);
    }
  }
}

ここでもaddEvent関数を使います。addEvent関数は、handler関数内のthisがイベントを設定したノード自身になるように実装してあります。ここで、

ローカル変数の誤った使いかた
for (var i = 0, inputsLen = inputNodes.length;i < inputsLen; i++){
  var input = inputNodes[i];
  if (input.type === 'text' || input.type === 'password') {
    addEvent(input,'focus',function(){
      input.style.background = 'white';
    });
  }
}

のようにしてはいけません。forループの中でinputは何度も書き換えられており、イベントが発生した時にinputが参照しているのはかならず最後のinputになってしまいます。

続いて、submit時の処理を実装します。submit時に空の要素や、パスワードが一致しない場合はsubmitを止めなければいけません。IE以外のブラウザはEventオブジェクトのpreventDefaultメソッドを呼び出すと、デフォルトの動作をキャンセルできます。submitイベントをpreventDefaultすればsubmitを止めることができますし、clickイベントをpreventDefaultすればクリックされなかったことにできます。IEの場合、EventオブジェクトのreturnValueプロパティの値をfalseに設定することで同様の動作が可能です。なお、ここでキャンセルができるのはキャンセルが可能と定義されたイベントのみで、loadイベントなどはキャンセルすることはできません。

submitイベントとキャンセル
addEvent(form,'submit',function(evt){
  var noBlank = true;
  var errors = [];
  /* 中略 */
  if (!noBlank || !matchedPass) {
    if (evt.preventDefault) {
      evt.preventDefault();
    } else {
      evt.returnValue = false;
    }
    errorNode.style.display = 'block';
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    timer = setTimeout(function(){
      errorNode.style.display = 'none';
      timer = null;
    }, 3000);
  }
});

エラーメッセージを表示し(errorNodeはCSSで非表示にしておきます⁠⁠、setTimeoutで3秒後に非表示に戻します。3秒経過する直前に再びエラーを出すと、2度目のメッセージのほうを非表示にしてしまうので、先のtimerが残っている場合は明示的にクリアするようにしています。

では、必須要素のチェックに入ります。

必須項目の値チェック
for (var i = 0;i < inputsLen; i++){
  var input = inputNodes[i];
  if (input.getAttribute('required') && !input.value) {
    input.style.background = '#ffff66';
    if(noBlank) {
      input.focus();
      noBlank = false;
    }
    errors.push(message[input.name]);
  }
}

input.getAttributeでrequiredの値が空でなく(requiredが設定されているかのチェック⁠⁠、input.valueが空の場合に、inputの背景色を黄色に変えています。さらに、その時点でnoBlankフラグがtrueであれば、それが最初に空だった要素なので、その要素にfocusしつつ、noBlankをfalseにしています。同時に空だった要素に対応するエラーメッセージを配列に入れておきます。

先ほど配列に入れておいたpasswordの値が一致しているかチェックします。

パスワードのチェック
var matchedPass = passwords[0].value === passwords[1].value;
if (noBlank && !matchedPass) {
  passwords[0].focus();
  passwords[1].style.background = '#ffff66';
  errorNode.innerHTML = 'パスワードが一致していません';
  isError = true;
}
if (!noBlank) {
  errorNode.innerHTML = errors.join('、')+'は必須です';
}

空の要素がある場合はそちらのメッセージを優先しています。errorNodeにinnerHTMLでメッセージを挿入しています。テキストの書き換えなので、なるべくならテキストのみを許容する処理を行いところですが、innerText(Firefoxで使えない)やtextContent(IEで使えない)はブラウザ依存があり、createTextNodeを使った場合は以下のように処理が煩雑になります(errorNodeに子要素が1つしかないことを前提としているので、より汎用的にするにはさらに実装が必要です⁠⁠。

テキストノードの挿入
  var textNode = document.createTextNode('パスワードが一致していません');
  if (errorNode.firstChild) {
    errorNode.replaceChild(textNode, errorNode.firstChild);
  } else {
    errorNode.appendChild(textNode);
  }

少々長くなりましたが、以上で今回の解説は終わりです。

まとめ

初回限定の増量キャンペーンでお送りしましたがいかがでしたでしょうか? 次回からはもう少しライトな内容にシフトする予定です。

なお、後半のjQuery版とjQuery不使用版は細かい動作が異なっています。次回以降、アニメーションなどの細かい動作も詰めてきますので、ご期待ください。

おすすめ記事

記事・ニュース一覧