万年カレンダーJavaScriptで作る

万年カレンダーJavaScriptで作る

<!DOCTYPE html>
<meta charset="utf-8">
<title></title>
<style>
table {
  margin: 1em 0;
}
thead td {
  font-weight: bold;
}
thead td, tbody td {
  text-align: center;
  border: 1px transparent solid;
}
tbody td:hover:not(.pDay):not(.nDay) {
  border: 1px #f80 solid;
}
tr td:first-of-type {
  color: red;
}
tr td:last-of-type {
  color:#099;
}
thead td { padding: 0 .5ex;}
caption label {
  padding: 0 .5ex;
  font-size: large;
  font-weight: bold;
}
caption button {
  background: transparent;
  border-style: none;
  font-size: large;
  padding: 0;
}
caption .hide {
  display: none;
}
tbody td.pDay, tbody td.nDay {
  color: silver;
  font-size: small;
  vertical-align: top;
}
tbody td.nDay {
  color: silver;
  font-size: small;
  vertical-align: bottom;
}
</style>


<table></table>
<table></table>
<table></table>

<script>

class Calendar {

  constructor (table, date, option = { }) {
    this.table = table;
    this.date = date;
    this.option = Object.assign ({ }, this.constructor.defaultOption (), option);
    this.view ();
  }


  view () {
    const
      {table, option, constructor: c } = this,
      {remove, getYMD, setNo, splice, toTBody, append} = c.tools ();

    let
      tbody = document.createElement ('tbody'),
      [y, m] = getYMD (this.current),//日付
      [,, pd, pw] = getYMD (new Date (y, m, 0)),//先月末日
      [,, nd, nw] = getYMD (new Date (y, m + 1, 0)),//今月末日
      pn = (pw + 1) % 7,//先月の繰越す日数
      nn = 6 - nw,//来月の取入れ日数
      days = [
        ...setNo (pn, pd - pn + 1),//連番(先月末)
        ...setNo (nd),//連番(今月)
        ...setNo (nn)//連番(翌月)
      ];

    remove (table.children);//子要素を削除
    append (table.createCaption (), option.caption);//キャプションをDOMで展開
    toTBody (table.createTHead (), option.weekName);//週名を展開
    toTBody (table.appendChild (tbody), [...splice (days, 7)]);//7日で区切る

    [...table.querySelectorAll ('caption label')]
      .forEach ((e, i)=> e.textContent = [y, option.monthName[m]][i]);

    let es = [...table.querySelectorAll ('tbody td')];
    if (pn) es.slice (0, pn).forEach (e=> e.classList.add ('pDay'));
    if (nn) es.slice (-nn).forEach (e=> e.classList.add ('nDay'));

    return this;
  }


  offsetMonth (n = 0) {
    this.current.setMonth (this.current.getMonth () + n);
    this.view ();
    return this;
  }


  set date (dt) {
    const {getYMD} = this.constructor.tools ();
    let [y, m] = getYMD (dt);
    this.current = new Date (y, m, 1);
    this._date = dt;
  }


  getDate (td) {
    const {zp, getYMD} = this.constructor.tools ();
    let
      [y, m, _, w] = getYMD (this.current),
      d = Number (td.textContent),
      ymd = [y, m+1, d],
      str = ymd.map ((a,b)=>zp(a,[4,2,2][b])).join ('-');
    return [new Date (y, m, d), str, ...ymd, w];
  }


  event (td) {
    if (td) {
      let
        cbFunc = this.option.cbFunc,
        args = this.getDate (td);
      if ('function' === typeof cbFunc)
        cbFunc.apply (this, args);
      if (this.option.clipboard)
        navigator.clipboard.writeText (args[1]).then(()=> console.log (args));
    }
  }


  handleEvent (event) {
    let e = event.target, p;

    switch (n.nodeName) {
    case 'BUTTON' :
      if (p = e.closest ('caption')) {
        let btNo = [...c.querySelectorAll('button')].indexOf (e);
        return this.offsetMonth ([-12, -1, 1, 12][btNo]);
      }
      break;

    case 'TD' :
      if (p = e.closest ('tbody'))
        return this.event (e);
      break;
    }
  }


  static tools () {
    return {
      zp: (a,b=2)=>String(a).padStart(b,'0'),
      remove: a=>[...a].map(a=>a.remove()),
      getYMD: a=>['FullYear','Month','Date','Day'].map(b=>a['get'+b]()),
      setNo:  function*(a,b=1,c=1,d=0){for(;d<a;d+=c)yield b+d},
      splice: function*(a,b=1){while(a.length)yield a.splice(0,b)},
      append: ((f=(a,b,c,{tag:T,child:C,...O}=b)=>Array.isArray(b)?b.reduce((a,b)=>f(a,b),a):(Object.assign(a.appendChild(c=document.createElement(T)),O),C&&f(c,C),a))=>f)(),
      toTBody: (a,ary)=>{
        const
          reg = /^(#?)(?:\[(\d+)?(?:\,(\d+)?)?\])?\s*(?:(.+)\s)*\s*(?:([+-]?(?:[1-9][0-9]{0,2}(?:\,?[0-9]{3})*)?(?:0?(?:\.\d*))?)|(.+))?$/,
          setAttr = (a,b,O=Object)=>O.assign(a,O.fromEntries(O.entries(b).filter(c=>'undefined'!==typeof c[1])));

        for (let row of ary) {
          let tr = a.insertRow ();
          for (let cell of row) {
            let
              [,thd, colSpan, rowSpan, className, num, text] = reg.exec (cell),
              td = tr.appendChild (document.createElement (thd ? 'th': 'td')),
              attr = {colSpan, rowSpan, className, textContent: text || num || ''};
            if (num != null)
              className += 'num'; 
            setAttr (td, attr);
            tr.appendChild (td);
          }
        }
      }
    };
  }


  static defaultOption () {
    return {
      weekName: [['Sun','Mon','Tue','Wed','Thu','Fri','Sat']],
      monthName: ['January','February','March','April','May','June','July','August','September','October','November','December'],
      caption: [
        { tag: 'button', type: 'button', textContent: '⏪', className: 'hide' },
        { tag: 'button', type: 'button', textContent: '<' },
        { tag: 'label', className: 'hide' },
        { tag: 'label'},
        { tag: 'button', type: 'button', textContent: '>'},
        { tag: 'button', type: 'button', textContent: '⏩', className: 'hide' }
      ],
      cbFunc: null,
      clipboard: true,
    };
  }


  static create (table = document.createElement ('table'), date = new Date, option = { }) {
    const calendar = new this (table, date, option);
    table.addEventListener ('click', calendar, false);
    return calendar;
  }
}


const
  TABLE = document.querySelectorAll ('table'),
  [a, b, c] = Array.from (TABLE, t=> Calendar.create (t));

b.offsetMonth (+1);
c.offsetMonth (+2);

</script>