年・月・日の入力を SELECT 要素で連動する(年度指定も可)
年・月・日の入力を SELECT 要素で連動する(年度指定も可)
年月日の入力を簡単にする。
マウスホイールにも対応。
年月日の範囲も指定可能。例:dateObj を [-7, 0, 10] の様な引数にすると、[一週間前, 今日, 10日後] の様な指定も可能。
使い方
chainDateSelector.create ([select element,,], [beginDateObj, selectedDateObj, endDateObj], callBackFunc, option);
オプション
chainDateSelector.option を参照のこと
ちょっとだけ気に入ったコード
function sprintf (fmt, ...ary) { let j = 0; return fmt.replace (/\%(?:(\d+)\$)?(?:(.)?([1-9][0-9]?))?([ds])/g, (_, i = ++j, spc='', len)=> String (ary[i - 1] == null ? '': ary[i - 1]).padStart (len, spc)); } //exp: console.log (sprintf ('%3$02d 日 %2$02d月 %1$d 年', year, month, day));
コード
/*********************************************************** * ChainDateSelector * select要素を年、月、日とし、連携して機能する * changeイベントが発生するたびに、コールバック関数を呼び出す * ChainDateSelector.create ([selct,..], [begin_date, selected_date, end_date], cbFunc, option); * ***********************************************************/ class ChainDateSelector { constructor (select, date, cbFunc = null, option = ChainDateSelector.getDefaultOption ()) { this.selects = select; //[ year[, month [, day]]]. 3 < length の場合不正な動きをする this.dates = date; // [start, current, end] this.cbFunc = cbFunc; this.option = option; this.disabled = false; //連動の抑止をしない //初期化 initialize: { this.replace ();//select要素の中身を生成 //イベント設定 for (let e of select) { e.addEventListener ('change', this, false); if (option.wheel) e.addEventListener ('wheel', this, false); } //最初にコールバック関数を呼び出すか? if (option.cbFunc_init_start) this.call (); } } //____________________________________ //コールバック関数を指定した形を引数として呼び出す call (type = this.option.valueType) { if (this.cbFunc) { let vals = this.getValue (type); (Array.isArray (vals)) ? this.cbFunc (...vals) : this.cbFunc (vals); } return this; } //____________________________________ //現在の年月日の値をtypeで指定した形式で返す getValue (type = this.option.valueType) { const { sprintf, getYMD, date_min, date_max } = ChainDateSelector.tools (); let OP = this.option, nd = OP.nendo, [y = null, m = null, d = null, ny = null, nm = null] = this.selects.map (e=> '' == e.value ? null: Number (e.value)), Y = y, dt, fmt = [ ], fg, rst = null;//Y,Mは実数値、y,mは年度により変化する if (! (fg = null == y)) { //y が null つまり fg= ture の時は、date object を生成しない switch (this.selects.length) {//select要素の数で処理を分岐 case 1 : m = nd ? 3: 0; [y, m, d] = getYMD (date_min (date_max (this.dates[0], new Date (y, m, 1)), this.dates[2])); fmt = OP.valueJpFormatY; break; case 2 : if (! (fg = null == m)) { d = 1; //select要素が year&month 場合はその年月の朔日を返す fmt = OP.valueJpFormatYM; } break; case 3 : if (! (fg = (null == m || null == d))) { fmt = OP.valueJpFormatYMD; } break; } ny = y; if (! fg) { nm = m - 4; if (nd) {//年度処理 if (12 < m) { m -= 12; Y++ ; } } else { if (m < 4) { nm += 12; ny -= 1; } } } } dt = fg ? null: new Date (Y, m - 1, d); switch (type) { case 1 : case 'DateObject' : return dt; case 2 : case '{y,m,d}' : return { y, m, d }; case 3 : case '[y,m,d]' : return [ y, m, d ]; case 4 : case 'YMD' : return fg ? '': sprintf (fmt[0], y, m, d); case 5 : case 'Y-M-D': return fg ? '': sprintf (fmt[1], y, m, d); case 6 : case 'Y年M年D日' : return fg ? '': sprintf (nd ? fmt[2]: fmt[3], y, m, d); case 7 : case 'NENDO[Y,M,D]' : return [ny, nm, d]; //便宜上4月を0とし、3月を11とする case 0 : default : return [y, m, d, dt, {y, m, d}, [ny, nm, d]]; } } //____________________________________ //select要素を随時書き換える //引数が null の場合は ”年”の select 要素も書き換える。 //実際には引数に SEL_Day 要素を与えても書き換えしないが、指定することでコールバック関数を呼び出す replace (target = null, onCall = false) { if (this.disabled) return this; const { getDate, sprintf, replaceSelect } = ChainDateSelector.tools (); let [SEL_Year, SEL_Month, SEL_Day] = this.selects, [ya, yb, yc] = this.dates.map (d => d.getFullYear ()), [ma, mb, mc] = this.dates.map (d => d.getMonth ()), nd = this.option.nendo ? 3: 0, opts, a, b, c, y, m; //年度処理 if (nd) { if (ma < 3) { ya -= 1; ma += 12; } if (mb < 3) { yb -= 1; mb += 12; } if (mc < 3) { yc -= 1; mc += 12; } } switch (target) { case null : default : //年の書き換え opts = [ ];//optionを構成するための配列、一括で作成登録 for (let i = ya; i <= yc; i++)//年が小さい順に作成し、必要に応じてソートする opts.push ([sprintf (this.option.yearFormat, i), i, i === yb, i === yb]); if (this.option.sortYear) opts.reverse (); //大きい順にする if (this.option.firstYear) opts.unshift (this.option.firstYear);//最初のoption replaceSelect (SEL_Year, opts); // ! break; //このまま次に case SEL_Year : //月の書き換え if (SEL_Month) { opts = [ ]; y = Number (SEL_Year.value); a = y === ya ? ma: nd;//最小 b = y === yb ? mb: -1;//月が defaultValue なのか判別に使用する c = y === yc ? mc: 11 + nd;//最大 for (let i = a; i <= c; i++) opts.push ([sprintf (this.option.monthFormat, (i % 12) + 1), i + 1, i === b, i === b]); if (this.option.sortMonth) opts.reverse (); //大きい順にする if (this.option.firstMonth) opts.unshift (this.option.firstMonth);//最初のoption replaceSelect (SEL_Month, opts); } // ! break; //このまま次に case SEL_Month : //日の書き換え if (SEL_Day) { opts = [ ]; y = Number (SEL_Year.value); m = Number (SEL_Month.value) -1; a = y === ya && m === ma ? this.dates[0].getDate () : 1; b = y === yb && m === mb ? this.dates[1].getDate (): -1; c = y === yc && m === mc ? this.dates[2].getDate (): (new Date (y, m + 1 , 0)).getDate ();//末日; for (let i = a; i <= c; i++) opts.push ([sprintf (this.option.dayFormat, i), i, i === b, i === b]); if (this.option.sortDay) opts.reverse (); //大きい順にする if (this.option.firstDay) opts.unshift (this.option.firstDay);//最初のoption replaceSelect (SEL_Day, opts); } // ! break; //このまま次に case SEL_Day : //SELECT要素の day が無い場合を考慮し、最後の要素として判断し実行する。 //注意 selects.length が4以上の場合正常に機能しない let last = this.selects[this.selects.length -1]; if (target == last || onCall) this.call (); break; } return this; } //____________________________________ //年月日のselect 要素にオフセット値を加える setSelect (dt = this.getValue ('DateObject'), oy = 0, om = 0, od = 0) { if (this.disabled) return this; const { getYMD, date_min, date_max } = ChainDateSelector.tools (); dt = new Date (dt); //複写したものを使う //各オフセット値を設定 if (oy) dt.setFullYear (oy + dt.getFullYear ()); if (om) dt.setMonth (om + dt.getMonth ()); if (od) dt.setDate (od + dt.getDate ()); //範囲チェック dt = date_min (date_max (this.dates[0], dt), this.dates[2]); let [y, m, d] = getYMD (dt), [sy, sm, sd] = this.selects; if (this.option.nendo && 4 > m) { m = m + 12; y = y - 1;//年度調整 } sy.value = y; this.replace (sy);//y をセットしたうえで、後の月・日を書き換える if (sm) { sm.value = m; this.replace (sm);//m をセットしたうえで、日を書き換える if (sd) { sd.value = d; this.replace (sd);//d をセット(してから実行) } } return this; } //____________________________________ //イベント処理 handleEvent (event) { if (this.disabled) return; let { target, type } = event; switch (type) { case 'change' : this.replace (target, true); break; case 'wheel' : event.preventDefault (); let w = event.deltaY < 0 ? 1: -1, dt = this.getValue ('DateObject') || this.dates[w + 1],//dtが未定な場合 w により初期値を決定 idx = this.selects.indexOf (target), args = [null, [dt,w], [dt,,w], [dt,,,w]][1 + idx]; if (args) this.setSelect (...args); break; } } //========================================================= static tools () { return { //サーバーの時刻を取得する getUTCDateByServer: function () { let xhr = new XMLHttpRequest (); xhr.open ('GET', '#', false); xhr.send (null); return new Date (xhr.getResponseHeader ('Date')); }, //年・月・日の値を配列で返す getYMD: function (d = new Date, o = [0, 1, 0]) { return ['getFullYear', 'getMonth', 'getDate'].map ((fn,i)=> d[fn]()+o[i]); }, //DateObjectをコピー&オフセット日を加算して返す getDate: function (d = new Date, o = 0) { d = new Date (d); d.setDate (d.getDate () + o); return d; }, //php の sprintf を模した関数 sprintf: function (fmt, ...ary) { let j = 0; return fmt.replace (/\%(?:(\d+)\$)?(?:(.)?([1-9][0-9]?))?([ds])/g, (_, i = ++j, spc='', len)=> String (ary[i - 1] == null ? '': ary[i - 1]).padStart (len, spc)); }, //select要素の中身を書き換える replaceSelect: function (select, opts = []) { select.innerText = null; for (let op of opts.reverse ()) select.insertBefore (new Option (...op), select.firstChild); }, isInt: n=> /^[+\-]?[1-9]?[0-9]+/.test (String (n)),//整数なのか? date_min: (a, b)=> a < b ? a: b,//最小の日付 date_max: (a, b)=> a < b ? b: a,//最大の日付 isDate: d=> '[object Date]' === Object.prototype.toString.call (d),//DateObject? }; } //____________________________________ //オブジェクトの生成: バリデーションも行う static create (_select = [ ], _date = [ ], _cbFunc = null, _option = { }) { const { getUTCDateByServer, isInt, isDate, getDate } = ChainDateSelector.tools (); let obj, select = [ ], date, option = Object.assign ({ }, ChainDateSelector.getDefaultOption (), _option); //arguments validation select: { //select要素数は1~3個まで許容 for (let e of _select)//要素名をチェック if ('select-one' === e.type || 'DATALIST' === e.tagName) if (3 === select.push (e)) break;//3個まで if (select.length < 1)//要素数をチェック throw new Error ('対象となる SELECT(DATALIST) 要素が不足です'); } date: { if (! Array.isArray (_date)) throw new Error ('範囲となる日付を見つけられませんでした'); let dt = option.referenceDateServer ? getUTCDateByServer (): new Date, [begin = dt, selected = 0, end = 0] = _date; //dates が数値ならば基準(dt)からのオフセット日とする if (! begin || isInt (begin)) begin = getDate (dt, begin || 0); else if (! isDate (begin)) throw new Error ('DateObject以外が指定されました'); if (! selected || isInt (selected)) selected = getDate (dt, selected || 0); else if (! isDate (selected)) throw new Error ('DateObject以外が指定されました'); if (! end || isInt (end)) end = getDate (dt, end || 0); else if (! isDate (end)) throw new Error ('DateObject以外が指定されました'); date = [begin, selected, end]; } nendo: {//年度の場合は、表示書式を変更する if (option.nendo) option.yearFormat = '%04d 年度'; } obj = new ChainDateSelector (select, date, _cbFunc, option); return obj; } //____________________________________ //初期値 static getDefaultOption (key = null) { const option = { wheel : true, //マウスのホイールを有効にする referenceDateServer : true, //サーバーの時間を取得する, false:端末のパソコンの時間を基準とする cbFunc_init_start : true, //コールバック関数を最初に実行するか? valueType : 0, //:y,m,d,... , 'YMD',1, 'Y-M-D',2, 'Y年M月D日',3, '[y,m,d]',4, '{y,m,d}',5 sortYear : true, //年賀大きい順にソート sortMonth : false, //月を小さい順から sortDay : false, //日を小さい順から yearFormat : '%04d 年', monthFormat : '%02d 月', dayFormat : '%02d 日', valueJpFormatY : ['%04d', '%04d', '%d年度', '%d年'],//select が「年」の実の場合 valueJpFormatYM : ['%04d%02d', '%04d-%02d', '%d年度%02d月', '%d年%02d月'],//select が「年月」の実の場合 valueJpFormatYMD : ['%04d%02d%02d', '%04d-%02d-%02d', '%d年度%02d月%02d日', '%d年%02d月%02d日'], firstYear : ['--', '', true, true], //SELECT年の最初のoption要素 firstMonth : ['--', '', true, true], //SELECT月の最初のoption要素 firstDay : ['--', '', true, true], //SELECT日の最初のoption要素 nendo : false,//true: 年度表記にする }; return key ? option[key]: Object.assign ({ }, option); } }