年月日のSELECT要素を連携させる(年度表記にも対応)

(function () {

  //select要素のoptionを書き換える
  function replaceSelectOption (select, text, value, defValue) {
    //引数確認
    if (2 > arguments.length)
      throw new Error;
    if (! /^(SELECT|DATALIST)$/.test (select.tagName))
      throw new Error;
    if (! Array.isArray (text))
      throw new Error;
    if ('undefined' === typeof value)
      value = text;

    defValue = String (defValue) || '';
    
    var doc = select.ownerDocument;
    var opt = doc.createElement ('option');
    var fgm = doc.createDocumentFragment ();
    var i, I, o;
    
    //子要素の削除
    while (select.hasChildNodes ())
      select.removeChild (select.firstChild);
    
    //オプションの作成
    for (i = 0, I = text.length; i < I; i++) {
      o = opt.cloneNode (false);
      o.text  = String (text[i]);
      o.value = String (value[i]);

      if (o.value === defValue)
        o.selected = true;

      fgm.appendChild (o);
    }

    select.appendChild (fgm);
  }

  
  //与えられた数値の範囲を配列にして返す
  function range (start, end, step) {
    var result = [ ];
    var i;

    switch (arguments.length) {
    case 2 : step = (start <= end) ? 1: -1;
    case 3 : break;
    default : throw new Error (); break;
    }
    
    for (i = start; i <= end; i += step)
      result.push (i);
    
    return result;
  }


  //年のselectを書き換える
  function replaceSelectYear () {
    var D = this.date;
    var s = D.start.getFullYear ();
    var e = D.end.getFullYear ();
    var d = D.selected.getFullYear ();
    var T = [ ]; //texts
    
    //年度ならば、年月日それぞれの月(1〜3月)なのかを調べ基準年を -1 にする
    if (this.nendo) {
      if (3 > D.start.getMonth ())    s -= 1;
      if (3 > D.end.getMonth ())      e -= 1;
      if (3 > D.selected.getMonth ()) d -= 1;
    }

    //年の範囲を配列
    T = range (s, e);

    //設定オプションで処理
    if (this.yearReverse) T.reverse (); //年を降順
    if (this.blankSpace)  T.unshift (''); //空白行をつける

    replaceSelectOption (this.select.year, T, T, d);
  }
  
  
  function toMonth (n) { return 12 < n ? n -12: n; }

  //月のselectを書き換える
  function replaceSelectMonth () {
    var DT = this.date;
    var Y = parseInt (this.select.year.value, 10);
    var T = [ ]; //texts
    var V = [ ]; //values
    var y, m, s, e, d;
    
    if (this.nendo) {//月 (年度)
      //start
      m = DT.start.getMonth () + 1;
      y = DT.start.getFullYear () + ((4 > m) ? -1: 0);//4月未満なら年を1減らす
      s = (Y == y) ? (4 > m ? 12 + m: m): 4;
      
      //end
      m = DT.end.getMonth () + 1;
      y = DT.end.getFullYear () + ((4 > m) ? -1: 0);
      e = (Y == y) ? (4 > m ? 12 + m: m): 15;

      //selected
      m = DT.selected.getMonth () + 1;
      y = DT.selected.getFullYear () + ((4 > m) ? -1: 0);
      d = (Y == y) ? (4 > m ? 12 + m: m): '';

      V = range (s, e);
      T = V.map (toMonth); //テキストは配列 V から生成、13月は 1月に
    }
    else { //月(通常)
      d = DT.selected.getFullYear () === Y ? DT.selected.getMonth () +1: null;
      T = V = range (
        (DT.start.getFullYear () === Y ? DT.start.getMonth () +1: 1),
        (DT.end.getFullYear ()   === Y ? DT.end.getMonth () +1  : 12)
      );
    }

    if (this.blankSpace) {
      T.unshift ('');
      V.unshift ('');
    }

    replaceSelectOption (this.select.month, T, V, d);
  }


  //日のselectを書き換える
  function replaceSelectDay () {
    var DT = this.date;
    var Y = parseInt (this.select.year.value, 10);
    var M = parseInt (this.select.month.value, 10);
    var T = [ ]; //texts
    var y, m, s, e, d;
    
    if (this.nendo) {//年度で3月以下なら年は1増やして処理
      m = DT.start.getMonth () + 1;
      y = DT.start.getFullYear () + ((4 > m) ? -1: 0);//4月未満なら年を1減らす
      s = ((Y !== y) || (M !== (4 > m ? 12 + m: m))) ? DT.start.getDate (): 1;
      
      m = DT.end.getMonth () + 1;
      y = DT.end.getFullYear () + ((4 > m) ? -1: 0);
      e = (((Y !== y) || (M !== (4 > m ? 12 + m: m))) ? new Date (Y, M, 0): DT.end).getDate ();

      m = DT.selected.getMonth () + 1;
      y = DT.selected.getFullYear () + ((4 > m) ? -1: 0);
      d = ((Y !== y) || (M !== (4 > m ? 12 + m: m))) ? '': DT.selected.getDate ();

    }
    else {
       //設定値の年、月が同じなら日を設定する。そうでなければ末日にする
      s = (DT.start.getFullYear () !== Y || DT.start.getMonth () +1 !== M)
           ? 1
          : DT.start.getDate ();
      
      e = ((DT.end.getFullYear () !== Y || DT.end.getMonth () +1 !== M)
          ? new Date (Y, M, 0)
          : DT.end
          ).getDate ();

      d = (DT.selected.getFullYear () !== Y || DT.selected.getMonth () +1 !== M)
            ? ''
          : DT.selected.getDate ();
    }

    T = range (s, e);

    if (this.blankSpace) T.unshift ('');

    replaceSelectOption (this.select.day, T, T, d);
  }

  //________________

  // new Object
  function AuxiliaryFormDate (select, date, cbFunc, cbObj) {
    this.select      = select;
    this.date        = date;   // {year: e0, month: e1, day: e2 } 
    this.cbFunc      = cbFunc; // onchange 後に呼ばれる関数
    this.cbObj       = cbObj;  // function.call (cbObj, y, m, d) { ..
  }


  // reset ()  
  function reset_select () {
    replaceSelectYear.call (this);
    //以降書き換えは連動なのでので利用する
    handleEvent.call (this, { target: this.select.year });
  }

  /*
    parseInt (null, 10) => NaN;
    parseInt ('', 10)   => NaN;
    isNaN (null)        => false
    isNaN ('')          => false
  */
  
  // function call
  // mSelect, dSelect が無くても、ySelectの値が数値ならば関数を呼び出す
  function jump () {
    var S = this.select;
    var y = parseInt (S.year.value, 10);
    var m, d;
    
    if (! isNaN (y)) { //yが数値
      if (! S.month || ! isNaN (m = parseInt (S.month.value, 10))) { // mSelectが無いか、その値が数値なら
        if (! S.day || ! isNaN (d = parseInt (S.day.value, 10))) { // dSelectがないかその値が数値なら
          if (this.nendo) {
            if (this.funcArgType) {
              if (12 < m) {
                m -= 12;
                y += 1;
              }
            }
          }
        }
      }
      this.cbFunc.call (this.cbObj, y, m || null, d || null);
    }
  }


  // イベントハンドラ
  function handleEvent (event) {
    var t = event.target;
    var S = this.select;
    var y, m, d;
    
    if (S.year === t) {
      if (S.month) {
        replaceSelectMonth.call (this);
        t = S.month;
      }
    }

    if (S.month)
      if (S.month === t)
        if (S.day)
          replaceSelectDay.call (this);

    if (! this.disabled)
      if ('function' === typeof this.cbFunc) //関数があれば実行 month day の select 要素が無い場合を考慮
        jump.call (this);
  }

  
  // オブジェクトを作る
  function create (ySelect, mSelect, dSelect, startDate, endDate, selectedDate, options, cbFunc, cbObj) {
    var obj, tm0, tm1, tm2;

    if (1 > arguments.length)
      throw new Error;

    //日付の範囲をチェック
    tm0 = startDate.getTime ();
    tm1 = endDate.getTime ();
    tm2 = selectedDate.getTime ();
    if (tm2 < tm0 || tm1 < tm2 || tm1 < tm0)
      throw new Error ('日付の値が範囲外');
    
    //オブジェクトの生成
    obj = new AuxiliaryFormDate (
      {
        year    : ySelect,
        month   : mSelect,
        day     : dSelect
      },
      {
        start   : startDate    || new Date,
        end     : endDate      || new Date,
        selected: selectedDate || new Date
      },
      cbFunc,
      cbObj
    );

    //Objectの初期設定
    obj.disabled    = false;  // 関数の実行を無効にするか
    obj.blankSpace  = true;   // 選択時に空行を含めるか
    obj.yearReverse = true;   // 年を降順にするか
    obj.nendo       = false;  // 年度表記にするか true:text(表示)は1,2,3(月)で、value値は13,14,15(月)となる
    obj.funcArgType = false;  // 年度表記時に、関数の引数の年、月を通常に変換して渡すか
                              // 2014-15 -> 2015-03 にする意

    // オプションを上書きする
    if (options)
      if ('object' === typeof options)
        for (opt in options)
          if(options.hasOwnProperty (opt))
            obj[opt] = options[opt];
  
    //SELECT要素を書き換える
    obj.reset ();

    //イベントを取り付ける
    ySelect.addEventListener ('change', obj, false);
    if (mSelect)
      mSelect.addEventListener ('change', obj, false);
    // SELECT(日)がないものも許容している
    if (dSelect)
      dSelect.addEventListener ('change', obj, false);
    
    return obj;
  }

  //___________
  AuxiliaryFormDate.prototype.handleEvent = handleEvent;
  AuxiliaryFormDate.prototype.reset       = reset_select;

  //___________
  AuxiliaryFormDate.create                = create;

  //___________
  this.AuxiliaryFormDate = AuxiliaryFormDate;  
}) ();