プルダウンメニューの日付選択で、JavaScriptを使って不正な日付(2月30日等)を選択不能にする

HTMLフォームにて、プルダウンメニュー(select要素)を使って日付選択を実現する場合、2月30日などの不正な日付は選択させたくありません。これをJavaScriptを使って実現させてみます。

フォーム自体は、「年」「月」「日」それぞれのプルダウンと、送信ボタンがあるだけの単純なもの。

今回例にするフォーム

たとえば、2月を選択した場合、29〜31日(閏年の場合は30〜31日)が選択不能になります。

2010年2月を選択した例。29から31の部分はグレー色になり選択できない。
  1. 主な仕様
  2. HTML
  3. JavaScript(date.js)

主な仕様

§

  • 当然ながら、スクリプト無効環境では動作しません。不正な値が送信されて困る場合は、サーバー側でのチェックが必要です。
  • IE 7 以下は、option要素のdisabled属性を認識しないようで、選択不能になりません。
  • 「日」のoption要素は、value値が昇順にソートされていることが必要です。
  • option要素のvalue値が数値形式以外など、HTML自体が想定外の場合の対策は行っていません。
  • 「日」以外を変更したときに日付として不正になる場合は、日付を妥当な最大値にします。たとえば、3月31日を選択した後に「月」を4月に変えると、「日」は30日に変わります。(これは IE 7 以下でも動きます。)
  • サンプルをjsdo.it(jsdo.it) に上げています。

2013年1月13日追記JavaScriptコードを大幅に変更しました。仕様は変わりません。

HTML

§

年月日それぞれのコントロールにIDを指定します。これはlabelと紐づけるためでもありますが、JavaScriptから getElementById() で特定するのに必要なものです。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<title>プルダウンメニューの日付選択で、JavaScriptを使って不正な日付を選択不能にする</title>
<script src="date.js" type="text/javascript"></script>
</head>
<body>
<form action="">
<p>
<select name="year" id="year">
  <option value="2010" selected="selected">2010</option>
  <option value="2011">2011</option>
  <option value="2012">2012</option>
</select><label for="year"></label>
<select name="month" id="month">
  <option value="01" selected="selected">1</option>
  <option value="02">2</option>
  <option value="03">3</option>
  <option value="04">4</option>
  <option value="05">5</option>
  <option value="06">6</option>
  <option value="07">7</option>
  <option value="08">8</option>
  <option value="09">9</option>
  <option value="10">10</option>
  <option value="11">11</option>
  <option value="12">12</option>
</select><label for="month"></label>
<select name="day" id="day">
  <option value="01" selected="selected">1</option>
  <option value="02">2</option>
  <option value="03">3</option>
  <option value="04">4</option>
  <option value="05">5</option>
  <option value="06">6</option>
  <option value="07">7</option>
  <option value="08">8</option>
  <option value="09">9</option>
  <option value="10">10</option>
  <option value="11">11</option>
  <option value="12">12</option>
  <option value="13">13</option>
  <option value="14">14</option>
  <option value="15">15</option>
  <option value="16">16</option>
  <option value="17">17</option>
  <option value="18">18</option>
  <option value="19">19</option>
  <option value="20">20</option>
  <option value="21">21</option>
  <option value="22">22</option>
  <option value="23">23</option>
  <option value="24">24</option>
  <option value="25">25</option>
  <option value="26">26</option>
  <option value="27">27</option>
  <option value="28">28</option>
  <option value="29">29</option>
  <option value="30">30</option>
  <option value="31">31</option>
</select><label for="day"></label>
</p>
<p><input type="submit" /></p>
</form>
</body>
</html>

JavaScript(date.js)

§

年月日それぞれのコントロールをIDで特定しているため、ページ中に日付選択のプルダウンが複数ある場合には対応していません。その場合はselect要素や親要素にクラス名等を設定してforループで走査するといった方式に変更する必要があります。

/**
 * イベントを登録する(IE8以下が addEventListener() に対応していないためのラッパー関数)
 *
 * @param t 対象ノード
 * @param p イベントタイプ
 * @param l 実行される関数
 */
var _addEvent=function(t,p,l){try{t.addEventListener(p,l,false);}catch(e){t.attachEvent("on"+p,function(e){l.call(t,e);});}};

(function(){
  _addEvent(window, "load", function(e) {
    var yearId = "year"; // 年コントロールのID
    var monthId = "month"; // 月コントロールのID
    var dayId = "day"; // 日コントロールのID

    var targetYear = document.getElementById(yearId);
    var targetMonth = document.getElementById(monthId);
    var targetDay = document.getElementById(dayId);

    _addEvent(targetYear, "change", function(e) {
      // 年コントロールを変更したとき
      nonExistDayIsNonDisplayed(this, targetMonth, targetDay);
    });
    _addEvent(targetMonth, "change", function(e) {
      // 月コントロールを変更したとき
      nonExistDayIsNonDisplayed(targetYear, this, targetDay);
    });
  });

  /**
   * 存在しない日(2月30日など)の選択肢を非表示にする
   *
   * @param targetYear 年コントロール
   * @param targetMonth 月コントロール
   * @param targetDay 日コントロール
   */
  var nonExistDayIsNonDisplayed = function(targetYear, targetMonth, targetDay) {
    var selectedMonthValue = parseInt(targetMonth.getElementsByTagName("option")[targetMonth.selectedIndex].value, 10);
    var targetDayOptions = targetDay.getElementsByTagName("option");

    if (selectedMonthValue === 2) {
      // 2月の場合
      var selectedYearValue = parseInt(targetYear.getElementsByTagName("option")[targetYear.selectedIndex].value, 10)
      var leapYear = isLeapYear(selectedYearValue); // 閏年か

      for (var i = targetDayOptions.length - 1; i >= 0; i--) {
        var targetDayOption = targetDayOptions[i];
        var dayValue = parseInt(targetDayOption.value, 10);
        if (dayValue >= 30 || (dayValue === 29 && !leapYear)) {
          targetDayOption.disabled = true; // 選択不能指定
          if (targetDayOption.selected) {
            // 29日(閏年でない場合のみ)、30日、31日のいずれかが選択されていた場合は、2月の最終日に変更
            if (leapYear) {
              targetDay.value = "29";
            } else {
              targetDay.value = "28";
            }
          }
        } else if (targetDayOption.disabled) {
          // 選択不能指定が成されていたら解除
          targetDayOption.disabled = false;
        } else {
          break;
        }
      }
    } else if (selectedMonthValue === 4 || selectedMonthValue === 6 || selectedMonthValue === 9 || selectedMonthValue === 11) {
      // 月の日数が30日の場合
      for (var i = targetDayOptions.length - 1; i >= 0; i--) {
        var targetDayOption = targetDayOptions[i];
        var dayValue = parseInt(targetDayOption.value, 10);
        if (dayValue >= 31) {
          targetDayOption.disabled = true; // 選択不能指定
          if (targetDayOption.selected) {
            // 31日が選択されていた場合は、各月の最終日に変更
            targetDay.value = "30";
          }
        } else if (targetDayOption.disabled) {
          // 選択不能指定が成されていたら解除
          targetDayOption.disabled = false;
        } else {
          break;
        }
      }
    } else {
      // 月の日数が31日の場合
      for (var i = targetDayOptions.length - 1; i >= 0; i--) {
        var targetDayOption = targetDayOptions[i];
        if (targetDayOption.disabled) {
          // 選択不能指定が成されていたら解除
          targetDayOption.disabled = false;
        } else {
          break;
        }
      }
    }
  };

  /**
   * 閏年か
   *
   * @param year 年
   *
   * @return 閏年ならtrue、それ以外の場合はfalse
   */
  var isLeapYear = function(year) {
    return new Date(year, 1, 29).getMonth() === 1;
  };
})();