jQuery で簡単なデートピッカー(日付選択UI)を作ってみる

去年の8月頃から作ってるシステムが来月リリース予定ってことで、後回しにしてた画面周りの微調整をしてるのですが、デートピッカー(カレンダーから日付を選択するUI)もその1つで後で jQueryプラグインの中から適当にチョイスすればいいやと思っていました。
で、いざいろいろ調べだすと機能過多だったり動きがもっさりしてたりとで、なかなか欲しいのが見つかりません。
これならシンプルなやつを自前で作ったほうが早いかなってことでサクっと作ってみました。

サンプルページ

デザイン構造

基本、デザインは CSS のみでカスタマイズできるようにしたいので、JavaScript では サイズ等の設定は行わずクラス名等の割り当てまでとします。
カレンダーといえばテーブルを使うのがセオリーっぽいですが、実用性を重視して「 float:leftな日付 BOX の流し込み 」方式にしてみました。
イメージ的には以下のような感じです。

<div class="container">
    <div class="adjuster"/>
    <a style="float:left;display:block">01</div>
    <a style="float:left;display:block">02</div>
     …
    <a style="float:left;display:block">31</div>
</div>

月初の曜日に応じて、div.adjuster の width を変化させ、月変更の都度のレイアウトの再描写は行わない方式です。

まず、JavaScript は使用せず下記ソースでデザインを確認。

CSS
/*container*/
div.calendar{
	font-family: Arial, Helvetica, "ヒラギノ角ゴPro W3", "MS Pゴシック", sans-serif;
	font-size:12px;
	width:140px;
	height:160px;
	background:#bbb;
	color:#555;
	border:solid 1px #aaa;
}
div.calendar-head,
div.calendar-body{
	overflow:hidden;
	zoom:1;
}
div.calendar-body{
	background:#ccc;	
}
div.calendar a:visited,
div.calendar a{
	text-decoration:none;
	color:#3355aa;
}
div.calendar a:hover{
	color:#01b0f0;
}

/*ym*/
span.ym{
	display:block;
	width:100px;
	height:18px;
	padding:1px 0;
	float:left;
	text-align:center;
}
/*day box*/
a.next,
a.prev,
span.d,
a.d{
	display:block;
	width:18px;
	height:18px;
	float:left;
	padding:1px;
	text-align:center;
}
a.d:hover {   
	background:#ddd;
}
a.next:hover,
a.prev:hover{
	background:#ddd;
}
/* d0 は日曜日 */
div.day-head span.d0{
	color:#ff7700;
}
/* d6 は土曜日 */
div.day-head span.d6{
	color:#0077ff;
}

/*adjuster*/
div.adjuster{
	display:block;
	width:20px;
	height:20px;
	float:left;
}
/* m0 〜 m6 は月初の曜日を表す。Dateオブジェクトの getDay()に対応 */
div.m0 div.adjuster{
	width:0px;
}
div.m1 div.adjuster{
	width:20px;
}
div.m2 div.adjuster{
	width:40px;
}
div.m3 div.adjuster{
	width:60px;
}
div.m4 div.adjuster{
	width:80px;
}
div.m5 div.adjuster{
	width:100px;
}
div.m6 div.adjuster{
	width:120px;
}
/*last day*/
/*月末の日を表す。例えば月末が28日だったら 29〜31 は表示にする*/
div.l28 a.d29,
div.l28 a.d30,
div.l28 a.d31,
div.l29 a.d30,
div.l29 a.d31,
div.l30 a.d31{
	display:none;
}
HTML
<!-- m1(月初が月曜日) により adjuster サイズが 20px となり
     l30(月末が30日) により 31日が非表示になる-->
<div class="calendar m1 l30">
    <div class="calendar-head">
        <a href="javascript:void(0)" class="prev">&laquo;</a>
        <span class="ym">2009.4</span>
        <a href="javascript:void(0)" class="next">&raquo;</a>
    </div>
    <div class="calendar-body">
        <div class="day-head">
            <span class="d d0">S</span>
            <span class="d d1">M</span>
            <span class="d d2">T</span>
            <span class="d d3">W</span>
            <span class="d d4">T</span>
            <span class="d d5">F</span>
            <span class="d d6">S</span>
        </div>
        <div href="javascript:void(0)" class="day-body">
            <!-- div.adjuster 初日(1日)の配置位置を調整する -->
            <div class="adjuster"></div>
            <a href="javascript:void(0)" class="d">1</a>
            <a href="javascript:void(0)" class="d">2</a>
            <a href="javascript:void(0)" class="d">3</a>
            <a href="javascript:void(0)" class="d">4</a>
            <a href="javascript:void(0)" class="d">5</a>
            <a href="javascript:void(0)" class="d">6</a>
            <a href="javascript:void(0)" class="d">7</a>
            <a href="javascript:void(0)" class="d">8</a>
            <a href="javascript:void(0)" class="d">9</a>
            <a href="javascript:void(0)" class="d">10</a>
            <a href="javascript:void(0)" class="d">11</a>
            <a href="javascript:void(0)" class="d">12</a>
            <a href="javascript:void(0)" class="d">13</a>
            <a href="javascript:void(0)" class="d">14</a>
            <a href="javascript:void(0)" class="d">15</a>
            <a href="javascript:void(0)" class="d">16</a>
            <a href="javascript:void(0)" class="d">17</a>
            <a href="javascript:void(0)" class="d">18</a>
            <a href="javascript:void(0)" class="d">19</a>
            <a href="javascript:void(0)" class="d">20</a>
            <a href="javascript:void(0)" class="d">21</a>
            <a href="javascript:void(0)" class="d">22</a>
            <a href="javascript:void(0)" class="d">23</a>
            <a href="javascript:void(0)" class="d">24</a>
            <a href="javascript:void(0)" class="d">25</a>
            <a href="javascript:void(0)" class="d">26</a>
            <a href="javascript:void(0)" class="d">27</a>
            <a href="javascript:void(0)" class="d">28</a>
            <a href="javascript:void(0)" class="d d29">29</a>
            <a href="javascript:void(0)" class="d d30">30</a>
            <a href="javascript:void(0)" class="d d31">31</a>
        </div>
    </div>
</div>

.l28 .d29 .d30 は、月末日を表していて、月末日の表示制御用に定義してます。
月末日の取得は前々回のエントリの exDate.js を活用します。

サンプルページ(JavaScriptなし)

jQuery で実装してみる

//プラグイン定義
(function($j){
    var calendar = function(target,cfg){
        var o=this,c=o.cfg=$j.extend({
            target:target,
            date:null,
            format:'yyyy/mm/dd'
        },cfg);
        o.buildLayout().setDate();
        c.calendar.hide();
    }
    $j.extend(calendar.prototype,{
        //カレンダー表示処理
        show:function(){
            var o=this,c=o.cfg;

//カレンダーは DOM ツリー上 input 要素の弟として配置されるため、コンテナ要素が
//input 要素と同じものになる。
//したがって input 要素の position().top に対し、input 要素の高さを足した位置に
//カレンダーを配置すれば、input 要素の直下に表示される。
            var pos=c.target.position();
            c.calendar.css({
                left:pos.left,
                top:pos.top+c.target.outerHeight()
            }).show('fast');
        },
        //年月変更処理
        setDate : function(date,format){
            var o=this,c=o.cfg;
            date=date||c.date;
            format=format||c.format;

            //エフェクト用に幅変更前の値を保持
            var w1=c.adjuster.width();

            //exDate オブジェクトの生成
            if(!date || typeof date=='string')
                c.date=date=$j.exDate(date,format);

            //年月の表示
            c.calendar.find('span.ym').html(c.date.toChar('yyyy.mm'));

            //月初の曜日の切り替えと保持
            if(c.firstDay)c.calendar.removeClass(c.firstDay);
            c.firstDay=('m'+$j.exDate(c.date.toChar('yyyy/mm/01')).getDay());
            c.calendar.addClass(c.firstDay)

            //月末日の切り替えと保持
            if(c.lastDay)c.calendar.removeClass(c.lastDay);
            c.lastDay='l'+c.date.lastDay().toChar('dd');
            c.calendar.addClass(c.lastDay)

//月変更時のエフェクト処理
//変更前と変更後の adjust の幅を w1,w2 に保持し、幅が w1 から w2 になるように
//animate させる。
//w2 の幅をそのまま持っていると style 定義より強くなり幅が固定化されてしまう
//ので、animate 完了後 width('') で設定をクリアする    
            var w2=c.adjuster.width();
            c.adjuster.width(w1).animate({width:w2},function(){
                c.adjuster.width('');
            });
            return o;
        },
        //レイアウト生成(初回のみ)
        buildLayout : function(){
            var o=this,c=o.cfg;

            //innerHTML ベースな単純なレイアウト生成
            var s='<div class="calendar">\
                <div class="calendar-head">\
                    <a href="javascript:void(0)" class="prev">&laquo;</a>\
                    <span class="ym"></span>\
                    <a href="javascript:void(0)" class="next">&raquo;</a>\
                </div>\
                <div class="calendar-body">\
                    <div class="day-head">';
            (function(){
                for(var i=0;i<this.length;i++)
                    s+=('<span class="d d'+i+'">'+this[i]+'</span>');
            }).apply(['S','M','T','W','T','F','S']);
            s+='</div>\
                <div href="javascript:void(0)" class="day-body">\
                    <div class="adjuster"></div>';
            for(var i=1;i<=31;i++)
                s+='<a href="javascript:void(0)" class="d d'+i+'">'+i+'</a>';
            s+='</div></div></div>';
            c.calendar=c.target.after(s).next().css('position','absolute')

            //日をクリックした時の処理
            c.calendar.click(function(evt){
                var target=$j(evt.target);
                if(target.attr('tagName')=='A' && target.hasClass('d')){
                    //lpadはexDate.jsの付属物
                    //左側を指定文字+文字数で埋める
                    var date=$j.exDate(
                        c.date.toChar('yyyymm')+$j.ex.lpad(target.text(),2,'0'),
                        'yyyymmdd'
                    );
                    //input 要素に年月日を設定する
                    c.target.val(date.toChar(c.format));
                    c.calendar.hide('fast');
                }           
            })
            //addMonths メソッドによる月の切り替え処理
            .find('a.prev,a.next').click(function(){
                c.date=c.date.addMonths($j(this).hasClass('next')?1:-1);
                o.setDate();
            })
            //input 要素に focus 時カレンダーを表示
            c.target.focus(function(){
                o.setDate(c.target.val());
                o.show();
            })
            c.adjuster=c.calendar.find('div.adjuster');
            return o;
        }
    })
    $j.fn.calendar = function(cfg){
        var o=this;
        return o.each(function(idx){
            new calendar(o.eq(idx),cfg)
        });
    }
})(jQuery);
//実行
jQuery(function($j){
    $j('input').calendar();
});

サンプルページ

注意
exDate.js の addMonths() に不具合があったので一部修正してます。exDate.js のデモページを以前開いてた場合古い exDate.js がキャッシュされてるため上記サンプルページが正しく動作しない可能性があります。そのような場合はサンプルページで画面を更新してみてください。


矢印のリンクで月を変えると animate しながら、日付ボックスの配置が変わるところが float 方式 + jQuery ならではかと。
INPUT 要素に初期値がある場合は、それに合わせた月のカレンダーが表示されます。
パラメータ指定で年月やフォーマットの指定も可能です。

exDate.js の addMonths()メソッドや toChar()メソッドのおかげで結構快適に作れましたが、場当たり的に作っているのでかゆいところが出てくるかもしれません。使いながら微調整してこうかと思います。