スプレッドシートに自作カレンダーを実装する

## 目的
　Googleスプレッドシートでセルのデータの入力形式を「日付」にしたとき、セルをダブルクリックするとカレンダーが表示されます。このカレンダーが個人的に使いづらいと感じたので、自作カレンダーを表示させるスクリプトを作りました。同じツールの組み合わせでカレンダーを作る他の記事はありますが、自分が欲しい結果にはなっていなかったので、本記事で作り方を共有、解説します。

## 使用するツール

* Google スプレッドシート
* GAS (Google Apps Script)
* Full Calendar

### Google スプレッドシート

　Google が提供する表計算ソフト。MicrosoftのExcelのように使えます。

### GAS (Google Apps Script)

　Google Apps Script は、Google が提供するサービスを自動化できるツールです。これでスプレッドシートに拡張機能を追加します。頭文字を取って「GAS」と呼ばれることがあるので以降はGASと表記します。

### Full Calendar

　カレンダーの要となる Full Calendar はオープンソースのjavascriptライブラリであり、カレンダーのUIが作れます。今回は、jsdelivrのCDNを使わせていただきました。

## 既存カレンダーの使いづらいところ

### 1. ダブルクリックが面倒
　カレンダーをポップアップさせる方法は「セルをダブルクリックする」ですが1回クリックするだけでポップアップさせたいところです。
　2回連続でクリックすれば良いだけなんですが、1回クリックしただけでカレンダー表示したほうがクリック回数は少ないので嬉しいです。
### 2. カレンダーが小さい
　とにかくカレンダーが小さい！なんと拡大サイズを200%にしてもカレンダーの大きさは変わりません。こっちの問題点を主に改善していきたいです。

<table>
    <tr>
        <td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3459057/65a0c1fb-7af5-b223-f4f2-8d5830a1bf30.png"></td>
        <td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3459057/54c0bb4e-1dca-55a5-6bf7-00865039d1b8.png"></td>
    </tr>
    <tr>
        <th align="center">拡大率100%</th><th>拡大率200%</th>
    </tr>
</table>

## 実装手順

### 1. 関数実装

　GASにより Full Calendar を任意のタイミングで表示できるようにします。手始めにポップアップを表示させる関数を用意します。「拡張機能」→「Apps Script」でスクリプト編集画面に入ると最初から「コード.gs」が用意されています。このスクリプトが開始地点ですので main.gs と命名します。そして以下のように編集します。

```javascript:main.gs
function onChange() {
  const mySpreadSheet = SpreadsheetApp.getActiveSpreadsheet();
  const mySheet = mySpreadSheet.getActiveSheet();
  // 選択されているセルの行番号、列番号を取得する。
  const targetCell = mySheet.getActiveCell();
  const row=targetCell.getRow();
  const col=targetCell.getColumn();
  // ポップアップ（Full Calendar を使うときは違う書き方）
  Browser.msgBox("タイトル","行："+ row +"　列：" + col, Browser.Buttons.OK);
}
```

### 2. 何をしたら関数を実行するか

　「セルをダブルクリックする」操作を簡単にします。私が考えたのは隣接したセルにチェックボックスを配置してチェックボックスをクリックした瞬間( FALSE → TRUE )に関数が走り出す、というものです。チェックボックスはメニュー「挿入」から「チェックボックス」の順にクリックすればセルに配置できます。

### 3. 関数の実行タイミング設定

　スプレッドシートのトリガー設定を定義します。トリガーには種類があり、スプレッドシートを開いたときの「起動時」、セルを変更したときの「変更時」、セルを編集したときの「編集時」、フォームを送ったときの「フォーム送信時」です。「変更時」と「編集時」という言葉は似たようなものですがスプレッドシートではトリガーの条件が異なります。本記事では「変更時」を設定します（「編集時」でも本記事のソースコードが動くことは確認済みです）。
　設定方法はGIFの通りです。エラー通知設定をいじっていますが変更しなくても大丈夫です。

![TriggerSetting.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3459057/641c7e05-2bd2-8889-ddd4-57ff23a28b1c.gif)

### 4. スクリプト承認

　編集しているシートで初めてスクリプトを動かすとき、もしくは今回のトリガーを設定するときに「承認」を求められます。「GAS 承認」で検索すると対応方法が色々出てきますので、今回は説明を割愛します。
　たまに下の画像のようにエラーが出ますが、通信環境を確かめてからめげずに保存ボタンを押しましょう。

![ScriptError.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3459057/57910df8-1091-4890-0c8f-fa16beb96517.png)

### 5. 動かしてみる

　これで準備が完了しました。試しにチェックボックスをクリックします。今は単純なポップアップですが、onChange関数が走っていることを確認できます。
　この後、onChange関数の処理を変更してカレンダーが出るようにしていきます。

![EasyOnChange.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3459057/b605e28f-0a9a-43e4-6b74-e115f7789aba.gif)

## プログラム説明

### HTML
　「エディタ」でファイルを追加します。スクリプト or HTML からはHTMLを選択します。ファイル名は任意の名前で問題ありません。今回はFullCalendar.htmlとします。 HTMLの中身は下記に示す通りです。
　このソースコードの一部を解説します。

```html:FullCalendar.html
<!DOCTYPE html>
<html lang='ja'>
  <head>
    <meta charset='utf-8' />
    <script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.js" charset="UTF=8"></script>
    <link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.css" rel="stylesheet">
    <script>
      let selectDate = ""; // 日付データを格納

      // HTMLからGASの関数に値を渡す
      function passingValuesFromHTMLtoGAS(s,row,col){
        google.script.run.withSuccessHandler(function(){
          // Full Calendar から日付を選択したらウィンドウを閉じる
          google.script.host.close();
        }).withFailureHandler(function(){
          // 万が一失敗してもウィンドウを閉じる
          google.script.host.close();
        }).updateCell(s,row, col); // GAS側の関数
      }

      // FUll Calendar の設定をこの中で行う
      document.addEventListener('DOMContentLoaded', function() {
        const calendarEl = document.getElementById('calendar');
        const calendar = new FullCalendar.Calendar(calendarEl, {
          locale: 'ja', // 日本語
          initialView: 'dayGridMonth',
          dateClick: (e)=>{// 日付マスのクリックイベント
            selectDate = e.date;

            // GASから受け取った値をHTMLの関数に渡す
            // .value の値は文字列なので数値変換する必要あり
            const selectRow = Number(document.getElementById("selectRow").value);
            const selectCol = Number(document.getElementById("selectCol").value);
            passingValuesFromHTMLtoGAS(selectDate.toLocaleDateString(),selectRow,selectCol);
	        }
        });
        calendar.render();
      });

    </script>
  </head>
  <body>
    <div id='calendar'></div>

    <input type="hidden" name="row" id="selectRow" value="<?= row ?>" />
    <input type="hidden" name="col" id="selectCol" value="<?= col ?>" />

  </body>
</html>
```

#### HTML ⇔ GAS　値の受け渡し

　HTMLからGASにデータを渡すために少し工夫しています。inputタグのvalueに\<?=  ?>を指定してGASからHTMLに値を受け渡します。受け渡す値はクリックされたチェックボックスがあるセルの行番号、列番号です。
　inputタグをhiddenにすることで、カレンダーポップアップ上に表示せず、行番号と列番号を保持することができます。

#### document.addEventListener

　イベント処理を実行するにはdocument.addEventListenerに走らせたい処理を定義します。Full Calendar を使うときは DOMContentLoaded を指定する必要があります。

#### dateClick: (e)=>{}

　Full Calendar の日付欄をクリックしたときのイベント処理を定義します。カレンダーで選択された日付と、クリックされたセルの行番号、列番号を取得して、関数"passingValuesFromHTMLtoGAS"に引数として渡しています。

#### google.script.run.withSuccessHandler

　HTMLからGASの関数を呼び出すためのイベントハンドラです。"updateCell"はGAS側に書かれている対象の関数です。関数が正常に終了したらこのハンドラに定義した処理をします。日付を選択したらポップアップを自動で消えるようにしたいので google.script.host.close() を呼びます。

### GAS

　そして、main.gsをHTMLに対応した記述に変更します。始めに作った onChange の中身を書き換えると以下のようなソースコードになります。onChangeのBrowser.msgBox()を別の処理に置き換え、一度しか定義しない変数( mySpreadSheet )と複数の関数で使いまわす変数( mySheet )はグローバル変数に変更しています。

```javascript:main.gs
const mySpreadSheet = SpreadsheetApp.getActiveSpreadsheet();
const mySheet = mySpreadSheet.getActiveSheet();

// セルを変更したとき

function onChange() {
  // 選択されているセルの行番号、列番号を取得する
  const targetRange = mySheet.getCurrentCell();
  const row = targetRange.getRow();
  const col = targetRange.getColumn();
  // A列以外のセルなら onChange は何もせず終了する
  if(col != 1){
    return;
  }

  // HTMLと連携する
  const fullCalendar = HtmlService.createTemplateFromFile("FullCalendar.html");
  fullCalendar.row = row;
  fullCalendar.col = col;
  const html = fullCalendar.evaluate();
  SpreadsheetApp.getUi().showModalDialog(html, "日付を指定");
}

// HTML側ポップアップのFull Calendarで選択した日付をチェックボックスの右隣のセルの値を更新
// チェックボックスを空(false)にする
function updateCell(s,row,col){
  // 右隣のセルに値を入力する
  mySheet.getRange(row, col).setValue(false);
  mySheet.getRange(row, col + 1).setValue(s);
}
```

## 結果画面

　チェックボックスをクリックしてポップアップしたカレンダーから日付をクリックして選びます。

![SelectDate.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3459057/d09ba2ce-4b5e-da72-f8f2-6fc8c6013c76.gif)

　するとチェックボックスの右隣のセルに日付が入力されます。これでスプレッドシートに自作カレンダーを実装できました。

## おわりに

　スプレッドシートにFullCalendarを組み込んだことでCDNの便利さを知りました。自分が欲しいと思っていたカレンダーを素早く実装できて、モチベーションが下がらずに済みました。何か実現したい機能があれば、まずはライブラリが提供されていないか確認するのが良いということを学びました。
　今回はカレンダーから日付を選ぶだけですが、特定の日付しか選べなくしたり、土日と祝日に色付けしたり、機能・視覚面の拡張ができると思います。