カスタマイズ可能なカレンダーをJavaScriptで作る

先日、JavaScriptで条件分岐を使わずシンプルにカレンダーを作る記事を公開しました。

https://qiita.com/shingorow/items/b99d384eb35a2387e3da

今回はその続きです。

今回はカスタマイズ可能なカレンダーとして以下の機能を盛り込みました。

- 前月、翌月の日付を追加して一般的なカレンダー風にした
- 月曜日スタートに切替可能にした
- 週や日付のタグを変更可能にした
- 週や日付のHTML要素に任意のクラスを付加できるようにした

といったあたりですね。

ところで、前月翌月の日付を追加するようにしたら配列やループが少しシンプルになったのですが、機能が増えたらシンプルになるというのも不思議なもんですね。

## コード

前回同様、コードから御覧ください。

```js
// 設定
const isStartAtMonday = false;
const weekWrapper = {tag: 'tr', class: ''};
const dateWrapper = {tag: 'td', class: ''};

const date = new Date();

// 曜日の配列を作成。あとでHTMLクラスとして使う
const days = [
  {en: 'sun', ja: '日'},
  {en: 'mon', ja: '月'},
  {en: 'tue', ja: '火'},
  {en: 'wed', ja: '水'},
  {en: 'thu', ja: '木'},
  {en: 'fri', ja: '金'},
  {en: 'sat', ja: '土'}
];

let dayStartIndex = 0;
let dayEndIndex = days.length - 1;

// 今月の最終日を取得する
date.setMonth(date.getMonth() + 1);
date.setDate(0);
const lastDate = date.getDate();
const lastDay = date.getDay();

// 今月１日の曜日を取得する
date.setDate(1);
const firstDate = 1;
const firstDay = date.getDay();

// 月と年を取得
const currentYear = date.getFullYear();
const currentMonth = date.getMonth();

// 前月の日付で最初に表示される日を割り出すための `setDate()` の引数
let prevMonthStart = firstDate - firstDay;

// 翌月の日付で最後に表示される日を割り出すための 'setDate()' の引数
let nextMonthEnd = lastDate + (6 - lastDay);

// 月曜スタートのときの処理
if (isStartAtMonday) {
  dayStartIndex++;
  dayEndIndex++;
  prevMonthStart++;
  nextMonthEnd++;
}

// カレンダーの曜日欄を作る
const $dayListElement = $(`<${weekWrapper.tag}>`).addClass(weekWrapper.class);
for (let i = dayStartIndex; i <= dayEndIndex; i++) {
  const index = i % days.length;
  $dayListElement.append($(`<${dateWrapper.tag}>`).html(days[index].ja).addClass(`${days[index].en} ${dateWrapper.class}`));
};

// カレンダーの配列を作る
const dateList = [];
for (let i = prevMonthStart; i <= nextMonthEnd; i++) {
  const currentDate = new Date(currentYear, currentMonth, i);
  dateList.push({
    year: currentDate.getFullYear(),
    month: currentDate.getMonth(),
    date: currentDate.getDate(),
    day: currentDate.getDay()
  });
}

// カレンダーに何週分必要か計算
const weekCount = Math.ceil(dateList.length / 7);

// 空のカレンダーを作る
const $month = $('<div>')
const $week = $(`<${weekWrapper.tag}>`).addClass(weekWrapper.class);
days.forEach(day => {
  $week.append($(`<${dateWrapper.tag}>`).addClass(`${dateWrapper.class} date-item`));
})

for (let i = 0; i < weekCount; i++) {
  $month.append($week.prop('outerHTML'), '\n');
}

// カレンダーに日付を記入
dateList.forEach((item, index) => {
  if (item) {
    const activity = item.month === currentMonth ? 'active' : 'inactive';
    $month
      .find(`${dateWrapper.tag}.date-item`)
      .eq(index)
      .html(item.date)
      .addClass(`${item.year}-${item.month + 1}-${item.date} ${activity} ${days[item.day]['en']}`); 
  }
});

// カレンダーTABLEに反映させる
$(() => {
  $('#calendar header').html(`${currentYear}/${zerofill(currentMonth + 1)}`);
  $('#calendar thead').html($dayListElement);
  $('#calendar tbody').html($month.html());
});

const zerofill = (number, units = 2) => {
  let zeros = '';
  for (let i = 0; i < units; i++) {
    zeros += '0';
  }
  
  return (zeros + number.toString()).slice(-1 * units);
}
```

DEMO

<p class="codepen" data-height="265" data-theme-id="0" data-default-tab="js,result" data-user="shingorow" data-slug-hash="qzrQwd" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Simple jQuery Calendar">
  <span>See the Pen <a href="https://codepen.io/shingorow/pen/qzrQwd/">
  Simple jQuery Calendar</a> by Shingo Matsui (<a href="https://codepen.io/shingorow">@shingorow</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## 今回のポイント

前回の続きということで、前回から大きく変わった点を中心にお届けしていきます。

### 前月と翌月の日付を追加

一般的なカレンダー同様、前月と翌月の日付を追加しました。

その際、```date.setMonth()``` を使おうとすると処理が複雑になるので一工夫しています。

```js
// 前月の日付で最初に表示される日を割り出すための `setDate()` の引数
let prevMonthStart = firstDate - firstDay;

// 翌月の日付で最後に表示される日を割り出すための 'setDate()' の引数
let nextMonthEnd = lastDate + (6 - lastDay);
```

JavaScriptの Date インスタンスって ```setDate()``` で現在の月の日にち範囲以外の数字を引数に与えると勝手に前月とか翌月までいってくれるんですよね。

ということで、例えば前月の何日から取得するかは、最初の日（1日）から最初の曜日を表すインデックスを引いてあげると ```setDate()``` にわたすべき引数を割り出せます。

![前月の日付計算](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66754/6caa3d0d-9728-a756-f88a-1e3e22a262c7.jpeg)

翌月の日付に関しても似たような考え方で計算しています。

### 月曜スタートを可能にした

コードの冒頭に ```isStartMonday``` という変数がありますね。これを ```true``` にするとカレンダーが月曜スタートになります。

で、月曜スタートにすることで日付がどうなるかというと、1日ずれるんですよね。

そこで、```prevMonthStart``` と ```nextMonthEnd``` をインクリメントすればあっさり月曜スタートになります。

日付はそれでいいとしても、曜日欄をどうするかですね。

曜日欄を構成するループのために2つの変数が用意してあります。

```js
let dayStartIndex = 0;
let dayEndIndex = days.length - 1;
```

ループの開始と終わりのインデックスですね。

先程日付をずらしたように、それぞれのインデックスをインクリメントすると、月曜スタートにはなりますが、ここで問題が。

そう、土曜日のあとがないのでエラーになるわけですね。

そこで一工夫。

ループを回す際に、配列の最後まで行ったら最初に折り返す仕掛けを入れています。

```js
const index = i % days.length;
```

ループの変数を配列の長さで割ったときの剰余をインデックスに指定するというもの。

こうすると、最後の配列の次が配列の長さと同じになって、余りが0。配列の最初に戻れますね。

## 思いの外シンプルに

ということで、変更点としてはこんなところでしょうか。

あとは、日付の要素にタグとクラスを選択できるようにしたことでテーブル以外にも例えばBootstrapでグリッドにするとかそういうのも対応できるし、そうするとスマホのときは1列に並べるとかもできますね。

これにあと追加するとしたら、ボタンで前後の月に移動する機能ですね。

それに関しては、処理全体を関数でくるんで、現在の月の変更を検知したら新しい月を基準にして処理を実行するような仕組みを作れば良さそうです。

それと、祝日か。

すべての日付要素に年月日のクラスを付けたので、祝日データを用意して、該当するクラスを色付けするというシンプルな処理でできるでしょうね。

ということで、もっといい方法があるよとか、わかりにくいぞーなどあればコメントを下さい。

ではまた。