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

第24回JavaScriptによるUIの実装:カレンダー

こんにちは。今回も引き続き、JavaScriptでUIを実装する方法を紹介していきます。今回はやや趣向を変えて、JavaScriptでカレンダーを実装してみます。

Dateオブジェクト

カレンダーを実装する前に、JavaScriptのDateオブジェクトについておさらいしておきましょう。

まず、Dateオブジェクトはnew演算子でインスタンスを作ってから使います。そのとき渡す引数は4つのタイプがあります。

  • 引数なし→現在の日時
  • 文字列→日付を表す文字列としてパースして解釈
  • 数値(1つ)→1970年1月1日00:00:00時点からのミリ秒数として解釈
  • 数値(2つ以上)→第一引数から順番に、年、月、日、時、分、秒、ミリ秒として解釈
Dateオブジェクトの初期化
new Date(); // 現在日時
new Date('2011/2/28');
new Date(1298818800000);
new Date(2011, 1, 28);

このように、何か引数を渡すとそれを日時として解釈して、その日時を基点とするDateオブジェクトを作ります。

このDateオブジェクトはgetDate, setDateといった、set/getメソッドをセットで持っており、一度作ったDateオブジェクトの日時を操作することができます。

なお、多くのブラウザはdate.getYearとdate.getFullYearというメソッドを実装しており混同されがちです。実はgetYearのほうはECMAScriptで定義されていない非標準なメソッド(正確には、ECMA 262 5th editionの付録としてその仕様について言及があります)で、IE 6~8以外のブラウザは1900年から数えた年数(2011年なら111)を返すので、getYearは使うべきでないメソッドのひとつです。

また、getFullYear、getMonth、getDateのうち、getMonthだけは0オリジンであることにも注意が必要です。ちなみに、C/Java/Perlなども月は0オリジンです。英語圏では月をJan、Feb、Marなどのように文字で表記することが多いため、0オリジンのほうが配列との相性がよかったのだと思われます。MonthのほかにDay(曜日)も0オリジンです。曜日を表示するのには、0オリジンのほうが都合がよいことが実感できると思います。

ある月の最終日

カレンダーを書く上で、一日と晦日を取得する必要があります。一日は new Date(year, month-1, 1) でよいので簡単ですね(monthにさえ注意すれば!⁠⁠。問題は晦日です。

といっても、実はちゃんとその方法が用意されていて、日にちに0(もしくはマイナスの値)を渡すとDateオブジェクトは前月に遡ってくれます。

つまり、 new Date(year, month, 0) とすればその月の晦日が取得できます。なお、月についても考慮してくれるので、 2010年の大晦日を取得したい時は、 new Date(2010, 12, 0) で取得できます(わざわざ、new Date(2011, 1, 0) のようにしなくても大丈夫です)。

カレンダーを書く

さてカレンダーを書く際にもう一つ考えなければいけないのが、何列書くか、という問題です。といっても、カレンダーは最短で4週(2月1日が日曜で、うるう年でない場合のみ⁠⁠、最長で6週の3つのパターンしかないので、6列で決め打ちにしてしまってもよいでしょう。

行数は当然7行ですから、6回ループする中に7回のループを入れ子にするという処理は見えてきました。あとは、どこから一日を始めて、晦日で終了する条件だけですね。

まず、一日は曜日によってどの位置になるかが決定します(曜日によって曜日が決まると言っているようなもので、当たり前のことなのですが⁠⁠。晦日は加算していった日にちが晦日と一致するところで判断できます。これらの間ではそれぞれの日にちを、それ以外では空白のセルで埋めればシンプルなカレンダーの出来上がりです。

カレンダーの骨組み
var first = new Date(year, month-1, 1);
var last  = new Date(year, month  , 0);
var first_day = first.getDay();// 一日の曜日
var last_date = last.getDate();// 晦日の日にち
var skip = true; // 日にちを埋めるかどうかのフラグ
var date = 1;
for (var row = 0; row < 6; row++){
  var tr = document.createElement('tr');
  for (var col = 0; col < 7; col++) {
    if (row === 0 && first_day === col){
      skip = false;
    }
    if (date > last_date) {
      skip = true;
    }
    var td = document.createElement('td');
    td.innerHTML = skip ? '&nbsp;' : date++;
    tr.appendChild(td);
  }
  tbody.appendChild(tr);
}

空白のセルには&nbsp;をいれています。これは空白のセルが潰れてしまうことを防ぐための応急処置です。

カレンダーの実装

ここまででカレンダーはほぼ出来上がりなので、見た目や実装について少し工夫してみましょう。

見た目についてはもちろんCSSで制御します。captionやth要素などを使って、なるべく無駄なく装飾します。

カレンダーの装飾
.js_calendar{
  font-size:80%;
  font-family:"Verdana", sans-serif;
}
.js_calendar td{
  border:1px solid #ddd;
  text-align:right;
  -moz-border-radius:3px;
       border-radius:3px;
}
.js_calendar caption{
  text-align:left;
}
.js_calendar caption div{
  position:relative;
}
.js_calendar caption span{
  display:block;
  padding:3px;
  text-align:center;
}
.js_calendar caption a{
  display:inline-block;
  position:absolute;
  background:#eee;
  text-decoration:none;
  font-weight:bold;
  padding:3px;
  top:0px;
  -moz-border-radius:4px;
       border-radius:4px;
}
.js_calendar caption a:link,
.js_calendar caption a:visited{
  color:#666;
}
.js_calendar caption a:hover{
  color:#f0f;
}
.js_calendar caption a.next{
  right:4px;
}
.js_calendar caption a.prev{
  left:4px;
}
.js_calendar td.calendar a{
  padding:2px;
  display:block;
  background:#eee;
  text-decoration: none;
  color: #333;
}
.js_calendar td.calendar span.blank{
  padding:2px;
  display:block;
}
.js_calendar th.day0{
  color:red;
}
.js_calendar th.day6{
  color:blue;
}
.js_calendar td.day0 a{
  background:#fdd;
}
.js_calendar td.day6 a{
  background:#def;
}
.js_calendar a:hover{
  color:#f0f;
}
.js_calendar th{
  font-weight:normal;
  font-size:90%;
}

さて汎用的な実装の部分ですが、まず外部に公開するメソッドを定義します。この時に、メソッドの中身とメソッドの名前の定義を分けておきます。これにより、公開されているメソッドの見通しがよくなるというメリットがあります。ただ、同じメソッド名を2回書くことになるというデメリット(というほど大した問題ではありませんが)もあります。

カレンダーの汎用的な実装
(function(){
var day_ja = ['日', '月', '火', '水', '木', '金', '土'];
// 外から見えるメソッドを定義
function JCalendar(parent){
  if (typeof parent === 'string') {
    parent = document.getElementById(parent);
  }
  this.parent = parent;
}
window.JCalendar = JCalendar;

JCalendar.prototype = {
  create: create,
  update: update,
  remove: remove,
  set_caption: set_caption,
  set_body: set_body,
  set_date: set_date,
  onclick_date: onclick_date,
  onclick_month: onclick_month
};
// 上記のメソッドの中身
function onclick_date(id, year, month, date){
  return false;
}
function onclick_month(id, year, month){
  this.update(+year, +month);
  return false;
}
function remove(){
  this.parent.removeChild(this.table);
}
function update(year, month){
  this.remove();
  this.create(year, month);
}
function set_date(year, month){
  var today = new Date();
  this.month =parseInt(month, 10)|| (today.getMonth()+1);
  this.year = parseInt(year, 10) || today.getFullYear();
}
function set_caption(year, month){
  var caption = document.createElement('caption');
  var div = document.createElement('div');
  var next = document.createElement('a');
  next.href = '#month-' + ((month === 11) ? year+1 : year)
              + '-' + (month===11?1:month+1);
  next.className = 'next';
  next.innerHTML = '→';
  var prev = document.createElement('a');
  prev.href = '#month-' + ((month === 1) ? year-1 : year)
              + '-' + (month===1?12:month-1);
  prev.className = 'prev';
  prev.innerHTML = '←';
  var current = document.createElement('span');
  var text = document.createTextNode(year + '/' + month);
  current.appendChild(text);
  div.appendChild(prev);
  div.appendChild(current);
  div.appendChild(next);
  caption.appendChild(div);
  this.table.appendChild(caption);
}
function set_body(year, month){
  var tbody = document.createElement('tbody');
  var first = new Date(year, month - 1, 1);
  var last = new Date(year, month, 0);
  var first_day = first.getDay();
  var last_date = last.getDate();
  var date = 1;
  var skip = true;
  for (var row = 0; row < 7; row++) {
    var tr = document.createElement('tr');
    for (var col = 0; col < 7; col++){
      if (row === 0){
        var th = document.createElement('th');
        var day = day_ja[col];
        th.appendChild(document.createTextNode(day));
        th.className = 'calendar day-head day' + col;
        tr.appendChild(th);
      } else {
        if (row === 1 && first_day === col){
          skip = false;
        }
        if (date > last_date) {
          skip = true;
        }
        var td = document.createElement('td');
        td.className = 'calendar day' + col;
        if (!skip) {
          var a = document.createElement('a');
          a.href = '#day-' +year+ '-' +month+ '-' +date;
          a.appendChild(document.createTextNode(date));
          td.appendChild(a);
          date++;
        } else {
          td.innerHTML='<span class="blank">&nbsp;</span>';
        }
        tr.appendChild(td);
      }
    }
    tbody.appendChild(tr);
  }
  this.table.appendChild(tbody);
}
function create(year, month){
  var that = this;
  var table = document.createElement('table');
  table.className = 'js_calendar';
  this.table = table;
  table.onclick = function(e){
    var evt = e || window.event;
    var target = evt.target || evt.srcElement;
    if (target.tagName === 'A' &&
        target.hash.indexOf('#day-') === 0) {
      return that.onclick_date.apply(that,
             target.hash.match(/day-(\d+)-(\d+)-(\d+)/));
    } else if (target.tagName === 'A' &&
               target.hash.indexOf('#month-') === 0) {
      return that.onclick_month.apply(that,
             target.hash.match(/month-(\d+)-(\d+)/));
    }
  };
  this.set_date(year, month);
  this.set_caption(this.year, this.month);
  this.set_body(this.year, this.month);
  this.parent.appendChild(table);
}
})();
new JCalendar('j-calendar2').create();

このJCalendarはnewした後のオブジェクトについてメソッドを上書きすることで挙動を一部だけ変えることができます。例えば、onclick_dateを上書きすれば、日付をクリックしたイベントを自由に定義することができます。

日付をフォームに反映
(function(){
  var mycal = new JCalendar('j-calendar3');
  var input = document.getElementById('j-calendar3-i');
  mycal.create();
  mycal.onclick_date = function(id,year,month,date){
    input.value = [year,month,date].join('/');
    return false;
  };
})();

まとめ

今回はJavaScriptを使ったUIとしてカレンダーの実装を紹介しました。こちらもまだまだ改良の余地があるので、是非自分なりに改良してみてください。

次回はこれまで作ったパーツの幾つかを組み合わせたアプリを作ってみたいと思います。

おすすめ記事

記事・ニュース一覧