FullCalendarをそこそこカスタマイズしてみる。

# 前書き

FullCalendarを結構使ったので、メモメモ。
Fullには使い切ってないけどね。
結構いろいろできるのね。まだまだよくわかんないけど。

## やること

表示したいカレンダーの内容は以下。

- 祝日(日本の)がわかるようにする（なんの日かは表示しなくていい。休みだ！とわかればいい）
- Google Calendarから予定をひっぱってきて表示する
- 月カレンダーと、月ごとの予定リストを切り替えて表示できる
- 大きいスクリーンサイズは月カレンダー、小さいスクリーンサイズは予定リストをデフォルト表示とする
- 予定リスト表示の場合、予定の場所もリストに表示
- 予定をタップしたら、Google Calendarにリンク、ではなく、ポップアップ詳細を表示。(この記載からは詳細を割愛)

（2020/10/28追記↓）

- カレンダーの表示期間を設定（どこまでも先のことを表示しない）

（2020/11/5 追記↓）

- 複数のイベントソースの読み込みに対して凡例をつける

(2020/11/11追記↓)

- 表示するイベントソースを切り替える

（2020/11/11追記　＆　2020/11/24　仕様変更↓）

- loadingアニメーションの表示

（2021/4/28追記）

- 1ビューに対する表示期間の変更


## 使うもの

- FullClaendar v5.0.1
- jQuery v3.5.1
- FontAwsome v5.4.2
- Sass(scss)

ReactやVueのようなフレームワーク、ライブラリ無し。
`<script>`内に記載。なので、FullClaendarのライブラリは全部入っている状態です。（日本語以外のファイルは入れてないけど）

※作ってる環境にjQuery入っているので、記載しましたが、脱jQuery方向で書いてあったります。
ふとめんどくさくなって、使ったりしてるので、リストアップに残してあります。


# さて、設定をしよう

## カレンダーのキャンバスを準備

```html
<div id="calendar"></div>
```

## 表示するカレンダーの設定（FullCalendarのオプション設定）
今回は
- 日本語化
- カレンダーの高さは内容（イベントの表示）に応じて可変
- カレンダーは月曜はじまり
- カレンダーの左上にはカレンダー-リストの表示切替ボタンを設置
- カレンダー真ん中には表示している年月を表示
- カレンダーの右上にはページャーと今日（今月）に戻るボタンを設置

という設定を入れようと思います。

```javascript
  document.addEventListener('DOMContentLoaded', function() {

    var calendar = new FullCalendar.Calendar(calendarEl, {
      locale: 'ja',
      height: 'auto',
      firstDay: 1,
      headerToolbar: {
        left: "dayGridMonth,listMonth",
        center: "title",
        right: "today prev,next"
      },
      buttonText: {
        today: '今月',
        month: '月',
        list: 'リスト'
      },
    });
    //キャンバスにレンダリング
    calendar.render();
  });
```

## Google Calendar からAPIを使って日本の祝日＆予定をもってくる

Google Calenadr APIの設定は割愛しますが、ブラウザ用のAPIを作って、キーを入力。
（もちろん、下記はダミーです。）

日本の祝日として公開されているカレンダーと、実際の予定（イベント）を、`eventSources`に設定します。

- 祝日の方にはイベントと分ける識別子として、クラスも付与してみます。
- 今回は認証使わないので、イベントの方のカレンダーもGoogle Clendar側で公開設定するのを忘れずに。


```javascript
（※ここまでに書いたソースは省略　以下このスタイルで記載します。）
    var calendar = new FullCalendar.Calendar(calendarEl, {
      googleCalendarApiKey: 'xxxxxxxxdummmmmmyyyyyyyyyxxxxxxxx',
      eventSources: [
        {
          googleCalendarId: 'ja.japanese#holiday@group.v.calendar.google.com',
          className: 'event_holiday'
        },
        {
          googleCalendarId: 'hoge-na-calendar@group.calendar.google.com',
        }
      ]
    });
```


## タイトル部分とかの日付表記フォーマットを指定する

ドキュメントはこちら https://fullcalendar.io/docs/date-formatting なのですが、
わかりにくくないですか？わたしだけですか。すみません。
各項目ごとに、年月日とか時間をプロパティとして設定値渡すそうです。

```javascript
    var calendar = new FullCalendar.Calendar(calendarEl, {
      views: {
        dayGridMonth: {
          titleFormat: { year: 'numeric', month: 'numeric' },
        },
        listMonth: {
          titleFormat: { year: 'numeric', month: 'numeric' },
          listDayFormat: { month: 'numeric', day: 'numeric', weekday: 'narrow' },
          listDaySideFormat: false
        }
      },
    });
```


## 曜日にあわせて日にち部分に色をつける

各曜日のclassが付与されているので、cssいじればOKです。

日付部分以外に影響与えないように書いたら、こんなのになりましたが、
もっといい方法があったのかも。

```scss
// 当月以外の日（月カレンダーに一緒に表示される前後の月）
.fc-day-other {
  .fc-daygrid-day-top, .fc-scrollgrid-sync-inner .fc-col-header-cell-cushion, .fc-list-day-cushion {
    color: $d-grey;
  }
}

// 土曜日
.fc-day-sat {
  .fc-daygrid-day-top, .fc-scrollgrid-sync-inner .fc-col-header-cell-cushion, .fc-list-day-cushion {
    color: $d-blue;
  }
}

// 日曜日
.fc-day-sun{
  .fc-daygrid-day-top, .fc-scrollgrid-sync-inner .fc-col-header-cell-cushion, .fc-list-day-cushion {
    color: $d-red;
  }
}
```

## 祝日にあわせて日にち部分に色をつける

過去に↓なことをやってみました。(Vue ver.ですが。)
https://qiita.com/niever66/items/bef24d1f23e9075eb09d

理由としては、「祝日のことは祝日データ読み込んだところに書けばいいんじゃない？」
と思ったからなのですが。。
リスト表示の場合、予定がありきで日付部分が作られるので、うまくいかなかったのです。

※うまくいかなかった中身は↓です。
「HTMLCollectionのlengthってなになのでしょう？https://teratail.com/questions/274367 」
日付部分のDOMをいじりたくても、レンダリング前の状態なゆえに、触れません。

ということで、書き換えです。`eventDidMount`を使います。
ちなみに、

- カレンダー表示の場合は、日付部分に色をつける
- 予定リスト表示の場合は、日付部分に色をつける＆曜日の表示の横に[祝]の字を追加

します。

※見直しながら思ったのですが、予定に全日が入ってこない前提だったので、予定リストのつくりが甘いです。

```javascript
    var calendar = new FullCalendar.Calendar(calendarEl, {
      eventDidMount: function(e) {
        let el = e.el;
        //普通のイベントとわけて考えるため、条件分岐。
        if ( el.classList.contains('event_holiday') ) {
          if ( e.view.type == "dayGridMonth" ) { //カレンダー(月)表示の場合
　　        //イベントが表示される場所の親をたどって各日の枠にたどり着いたらclassを授けよう
            el.closest('.fc-daygrid-day').classList.add('is_holiday');
          } else if ( e.view.type == "listMonth" ) { //予定リストの場合
　　        //祝日の兄要素（というか、前要素）にclassを授けよう
            el.previousSibling.classList.add('is_holiday');
　　　　　　 /*
            で、innerText変えたらいいんじゃない？と思ったのだけど、
　　　　　　 innerText変えるとinnerHTMLもHTML要素なくなって
　　　　　　 innerTextと同じ内容になってしまうのですが、、（なぜ？）
            ということで、文字列継ぎはぎします。
            */
            let t = el.previousSibling.innerText;
            let h = el.previousSibling.innerHTML.split(t);
            t = t.slice(0, -1) + '・祝' + t.slice(-1);
            el.previousSibling.innerHTML = h[0] + t + h[1];

　　　　　　 /*
            このままだと、祝日だけど、予定ないわ～、暇人だわ、ワシ。
            が、丸見えになるので、イベントが無い祝日の日付表示部分を削除。
　　　　　　 祝日イベントの次の行が予定でなく、日付だったら削除です。
            */
            if (el.nextSibling.classList.contains('fc-list-day')) {
              el.previousSibling.remove();
            }
            //祝日イベントも削除
　　　　　　 el.remove()
          }
        }
      }
    });
```

カレンダー表示の場合、↑だと祝日イベントが表示されてしまいます。
jsの中で非表示処理したらよかったのだけど、めんどくさくなったので、cssで処理。

```scss
.fc-daygrid-event {
  &.event_holiday {
    display: none;
  }
}
```

## リストに項目を追加

予定リストの表示には、予定の場所も記載するのでした。
ついでに、アイコン表示も追加します。

ドキュメント https://fullcalendar.io/docs/event-render-hooks

`eventContent` っていうのがあったのですが、触ってみて、よくわからなかったので、
`eventDidMount`に追記して対応します。

```javascript
    var calendar = new FullCalendar.Calendar(calendarEl, {
      eventDidMount: function(e) {
        if ( el.classList.contains('event_holiday') ) {
          // ~~ 略（祝日の表記設定） ~~
        } else {
          if ( e.view.type == "listMonth" ) {
            // 予定の時間には「時計」アイコンを追加
            let t = el.querySelector('.fc-list-event-time');
            t.insertAdjacentHTML('afterbegin','<i class="far fa-clock">');
            // 位置情報を取得して、他の要素とあわせたHTML要素として出力
            //「地図のピン」アイコンを追加しておく
            let location = e.event._def.extendedProps.location;
            if ( location !== undefined ) {
              let cell = document.createElement('td');
              cell.classList.add('fc-list-event-location')
              cell.innerHTML = location;
              cell.insertAdjacentHTML('afterbegin','<i class="fas fa-map-marker-alt">');
              el.appendChild(cell);
            }
          }
        }
      },
    });
```

## スクリーンサイズに合わせて表示形式を変更する

読み込み時のスクリーンサイズに合わせてデフォルトの設定を変えます。

```javascript
  document.addEventListener('DOMContentLoaded', function() {
    let calSettings;

    //他の設定追加したくなったときように、objectにしてみた。
    //768pxで分岐させてみました。
    if ( window.innerWidth < 768 ) {
      calSettings = {initialView: 'listMonth'};
    } else {
      calSettings = {initialView: 'dayGridMonth'};
    }

    var calendar = new FullCalendar.Calendar(calendarEl, {
      initialView: calSettings.initialView,
    });
  });
```

スクリーンサイズが変わったときにも変更を加えます。
（やってみたけど、ユーザーにとって迷惑じゃね？）

```javascript
    var calendar = new FullCalendar.Calendar(calendarEl, {
      windowResize: function() {
        if ( window.innerWidth < 768 ) {
          calendar.changeView('listMonth');
        } else {
          calendar.changeView('dayGridMonth');
        }
      },
    });
```

## カレンダーの日付部分がうざいです

`locale: 'ja'`を入れいていると、月カレンダーの日付部分にいちいち`日`が付きます。うざいです。
`Formatter`の設定系にどうしてもこの項目の設定がみつかりません。
めちゃくちゃ悩みました。
が、結果としてめちゃくちゃあっさりできました。

```javascript
    var calendar = new FullCalendar.Calendar(calendarEl, {
      dayCellContent: function(e) {
        e.dayNumberText = e.dayNumberText.replace('日', '');
      },
    });
```


## カレンダー・リスト表示のフッターにもページャー追加

カレンダーだけなら気にならなかったんですけどね。リスト表示すると、下までスクロールして「次の月見るには上に戻るんかい！」感ハンパなかったので、ボタンを追加です。

```javascript
    var calendar = new FullCalendar.Calendar(calendarEl, {
      footerToolbar: {
        right: "prev,next"
      },
    });
```

ただ、これだと、スクロール位置はそのままでリスト・カレンダーだけ送られるので、
リスト・カレンダーの上部までスクロールも一緒に行ってほしいものです。

なので、カスタムボタンを追加しましょう。

```javascript
    const calendarEl = document.getElementById('calendar');
    let calendarEl_posT = $(calendarEl).offset().top - 18;　//18は上部ゆとり感にテキトウ。

    var calendar = new FullCalendar.Calendar(calendarEl, {
      customButtons: {
        nextWithScroll: {
          icon : 'fa-chevron-right',
          click: function(e) {
            calendar.next();
            $(window).scrollTop(calendarEl_posT); //なるべく使ってなかった急にjQuery!
          }
        },
        prevWithScroll: {
          icon : 'fa-chevron-left',
          click: function(e) {
            calendar.prev();
            $(window).scrollTop(calendarEl_posT);
          }
        }
      },
      footerToolbar: {
        right: "prevWithScroll,nextWithScroll"
      },
    });
```

ボタンアイコンについては　ドキュメント https://fullcalendar.io/docs/buttonIcons
themeSystemの方もbootstrapの方も、(わたしの環境のせい？)で記載してもアイコン表示されなかったので、
css側で追記します。（割愛）

## イベントをクリック

イベントをクリックしたら、放っておくとGoogle Calendarに画面遷移します。
遠慮願いたいです。

ということで、ポップアップを作りますが。。割愛します。

```javascript
  document.addEventListener('DOMContentLoaded', function() {
    
    var calendar = new FullCalendar.Calendar(calendarEl, {
      eventClick: function(info) {
        //カレンダーへのリンクはさせません。
        info.jsEvent.preventDefault();
        hogehoge(info);
      }
    });

    function hogehoge(info) {
      //なんかやりたいこと
    }

});
```

## カレンダーの表示期間を設定する

参考：https://fullcalendar.io/docs/validRange

どこまででも過去にさかのぼったり、未来を写さない仕様に変更です。
・始まりは（過去の）固定日
・終わりは来月末
にします。
「スクリーンサイズに合わせて表示形式を変更する」の時に使用したcalSettingsを活用してこちらで。
日付指定はDate型じゃないのよ、文字列なのよ。

```javascript
  document.addEventListener('DOMContentLoaded', function() {
    const today = new Date()
    const nextMonth_lastDate = new Date(today.getFullYear(), (today.getMonth()+2), 0 )
    calSettings.endDate = `${nextMonth_lastDate.getFullYear()}-${zeroPadding(nextMonth_lastDate.getMonth() + 2, 2)}-01`;

    var calendar = new FullCalendar.Calendar(calendarEl, {
      validRange: function() {
        return {
          start: '2020-07-01',
          end: calSettings.endDate
        };
      },
    });

  });
```

## 複数の予定用のgoogle calendar を読み込んで、凡例（LEGEND）をつける


意外となかったのよね、FullCalendarにlegend。（見落としてるだけかな～？）
ということで。

最初のほうに書いた、google calendarの読み込みに追記。google calendarごとにクラスを追加。

```javascript
    var calendar = new FullCalendar.Calendar(calendarEl, {
      eventSources: [
        {
          googleCalendarId: 'hoge-na-calendar@group.calendar.google.com',
          className: 'event-hoge'
        }
        {
          googleCalendarId: 'fuge-no-calendar@group.calendar.google.com',
          className: 'event-fuge'
        }
      ]
    });
```

カレンダーVIEWがDOMとしてできたところで、カレンダーの前にお手製のLEGEND要素を追加します。

```javascript

    var calendar = new FullCalendar.Calendar(calendarEl, {
      viewDidMount : function(e) {
        if (!e.el.previousElementSibling ) {
          cals = [
            { name: 'event-hoge', label: 'ほげな予定'},
            { name: 'event-fuge', label: 'ふげの予定'}
          ];
          let legend = '<ul class="fc-legend">';
          for ( let i = 0; i < cals.length; i++ ) {
            legend += `<li class="fc-legend-item ${cals.name}">${cals.label}"</li>`
          }
          legend +=  '</ul>'
          e.el.insertAdjacentHTML('beforebegin', legend);
        }
      },
    });
```


(実際はCMSにgoole calendarの情報設定して読みこんてるから、改めて変数に設定してないのだけど、記載用に書き換えたもので。。。）
google calendarのクラス名とかもろもろ、FullCalendarの設定の外に置いておいて、eventSourcesの読み込みと、legendの設定両方から読み込むのもできるかもしれません。

## ボタンのクリックで別のイベントソースに表示を切り替える

凡例を押すと切り替わるとか表示を切り替えたいことあるよね。今回は凡例ではなく、切り替え専用ボタンを押したら切り替わるスタイルで、イベントソースAとBを行ったり来たりするようにします。

### なので、ボタンの設置

```HTML
<button type="button" id="setEventCal">切り替えボタン</button>
```

### 切り替えるイベントをセット

```javascript
    gCal_for_event = {
      type_a: [
        {
          'googleCalendarId': 'hoge-na-calendar@group.calendar.google.com',
          'className': 'event-hoge'
        },
        {
          'googleCalendarId': 'fuge-na-calendar@group.calendar.google.com',
          'className': 'event-fuge'
        }
      ],
      type_b: [
        ~省略~
      ]
    }
```

### FullCalendarのオプション修正

初期表示はどっちにするかを決めつつ、FullCalendarのイベントソースの値を変数にする。

```javascript
    calSettings.area = 'type_a';    

    var calendar = new FullCalendar.Calendar(calendarEl, {
      eventSources: gCal_for_event[calSettings.area],
    });
```

### クリックイベント
ボタンを押したら、現在どっちかを変数に入れて、FullCalendarに与えているイベントソースを更新。

```javascript

    const AreaSettingTrigger = document.getElementById('setEventCal');
    AreaSettingTrigger.addEventListener('click', function(e) {
      if ( calSettings.area == 'type_a' ) {
        calSettings.area = 'type_b';
      } else {
        calSettings.area = 'type_a';
      }
      calendar.setOption('eventSources', gCal_for_event[calSettings.area]);
    });
```

### 読み込む間があくから、loadingもつけようかな

カレンダーのDOMに枠を作って、Lottie(Bodymovin)で作ったものを表示。読み込んだら枠削除。
Lottie・Bodymovinについては詳細省略するけど、headでLottieを読み込むのを忘れずに。
ということで、~~表示切替用の関数。~~ FullCalendarの`loading`オプション。（ https://fullcalendar.io/docs/loading ）
ajax読み込み開始したら`true`で、終わったら`false`を引数に渡すらしい。


```javascript

    var calendar = new FullCalendar.Calendar(calendarEl, {
      loading: function( isLoading ) {
        if ( isLoading ) {
          let d = '<div id="loading" class="loading"><div id="loading-anime" class="loading_inner"></div></div>'
          calendarEl.insertAdjacentHTML('beforebegin', d);                
          let loading = bodymovin.loadAnimation({
            container: document.getElementById('loading-anime'), 
            renderer: 'svg',
            loop: true,
            autoplay: true,
            path: '<?php echo get_theme_file_uri('/assets/js/loading.json'); ?>'
          });
        } else {
          document.getElementById('loading').remove();
        }
      },
    });

```

FullCalendarのオプションで、イベントソースの読み込み終わったら発火する`eventSourceSuccess `と`eventSourceFailure`があるから、読み込み成功と失敗で分けるときは、`loading`が`false`の場合の内容をこちらに記載するとよさそう。

https://fullcalendar.io/docs/eventSourceSuccess
https://fullcalendar.io/docs/eventSourceFailure


### 凡例を切り替えなくては

なんか重複するけど、あくまでFullCalendarのオプションに渡す値だった`gCal_for_event`の他に、凡例用のオブジェクトを作成。

```javascript

    cal_legend = {
      type_a: [
        { 'name': 'event-hoge', 'label': 'ほげな予定' },
        { 'name': 'event-fuge', 'label': 'ふげの予定' }
      ],
      type_b: [
        ~省略~
      ]
    }

```

なんか、切り替えごとに作り直すより、CSSで表示切替のほうが楽な気持ちだったので、
CSSの切り替えの関数つくりました。


```javascript
    function setting_legend ( areaSetting ) {
      $('#FCLegend .fc-legend').each(function(){
        if ( $(this).data('area') == areaSetting ) {
          $(this).css('display', 'flex');
        } else {
          $(this).css('display', 'none');
        }
      })
    }
```

そして、FullCalendarのオプションで作ってた凡例の部分を書き換え。
type_aとtype_b用の凡例をそれぞれ`<ul>`で作って全体を`<div>`で囲っておいた。
で、最終的にCSS切り替え関数を実行。

```javascript

    var calendar = new FullCalendar.Calendar(calendarEl, {
      viewDidMount : function(e) {
        if (!e.el.previousElementSibling ) {
          var legend = '<div id="FCLegend" class="fc-legend-box">';
          for (const [key, value] of Object.entries(cal_legend)) {
            legend += '<ul class="fc-legend" data-area="' + key + '">';
            for ( let i = 0; i < value.length; i++ ) {
              legend += `<li class="fc-legend-item ${value[i].name}"><label><input type="checkbox" checked />${value[i].label}</label></li>`
            }
            legend +=  '</ul>'
          }
          legend +=  '</div>'
          e.el.insertAdjacentHTML('beforebegin', legend);
          setting_legend ( calSettings.area );
        }
      },
    });

```

## 1ビューに対する表示期間の変更

カレンダーにイベントがもりもりになってきたので、過去のそんなに見ないし、先々のもまぁまぁ見れたらいいかな気運がでてきました。（本当はもっと根本的な変更が求められるところですが…）

ということで、１か月分まるっと表示していたのを

- カレンダー表示： 今週から2週間
- イベントリスト表示： 今日から1週間

に変更します。
※ページ送りした結果見れる範囲は今まで通り。

カスタムビューを使って、範囲の変更だけならかなり簡単にできます。
(ドキュメント：https://fullcalendar.io/docs/custom-view-with-settings )


日付フォーマットの設定に使っていた `views`の部分で
オリジナルのビューを作成します。デフォルトのビューは使わなくなったので削除

```javascript
var calendar = new FullCalendar.Calendar(calendarEl, {
  views: {
    dayGrid2Weeks: {
      type: 'dayGrid',
      duration: { weeks: 2 },
      titleFormat: { month: 'numeric', day: 'numeric' },
    },
    list1Week: {
      type: 'listWeek',
      titleFormat: { month: 'numeric', day: 'numeric' },
      listDayFormat: { month: 'numeric', day: 'numeric', weekday: 'narrow' },
      listDaySideFormat: false,
      duration: { days: 7 }
    },
    // ↓削除
    dayGridMonth: {
    },
    listMonth: {
    }
    // ↑削除
  }
});
```

この時、各カスタムビューの設定に`type`＝使うデフォルトテンプレートを設定しないと
`Uncaught Error: viewType "dayGrid2Weeks" is not available. Please make sure you've loaded all neccessary plugins`
というエラーが出ます。

settingではなく、js使うとテンプレートからいじれるっぽいのかな？
（ドキュメント： https://fullcalendar.io/docs/custom-view-with-js ）

`duration`を使って表示したい期間を設定。
他はお好みで。

あとは、今まで「カレンダービューやリストビューで表示してね」って設定していたところを、あたらしいビュー名に変更したり、祝日の表示とかで条件としてビュー名を入れたところを変更。

```javascript
if ( window.innerWidth < 768 ) {
  calSettings = {initialView: 'list1Week'};
} else {
  calSettings = {initialView: 'dayGrid2Weeks'};
}

var calendar = new FullCalendar.Calendar(calendarEl, {
  windowResize: //省略
  eventDidMount: //省略
});
```


# ~~こちらができたものです。~~

 ~~対応したつもりがちょっと挙動がおかしかったり、まだUX的によろしくなかったり~~

 ~~まだまだ粗がありますが、~~ 
 ~~こちらがこのレシピで作ったカレンダーとなります。~~ 
 ~~（※いつの日にか変更加えてたら申し訳ない。。。）~~ 

 ~~株式会社hanane 花つみイベントカレンダー~~ 
もう動いてないページだったので、リンク削除。

 ~~お近くのご都合いい場所でであったら、立ち寄ってみてね！~~