【Handsontable】セル内に日時入力(DateTimePicker)の表示

# はじめに
前回、weareoutman氏が作成した「[ClockPicker](https://weareoutman.github.io/clockpicker/)」を使用して、セル内に時刻入力(ClockPicker)の表示を実現しました。

https://qiita.com/yaju/items/25ea509c153af41e64e5

それならと日付と時刻の両方を満たす DateTimePicker に挑戦してみました。
今回は、バニラJavaScriptで作成されている DatetimePickerの「[flatpickr](https://flatpickr.js.org/)」で実現したいと思います。

https://qiita.com/yaju/items/2ec8195df692cc6bf032

# 環境
HandsontableはMITライセンス版のバージョン 6.2.2を使用しています。
一応、有償版バージョン 8.3.2でも動作は確認しています。

# CDN
```html
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" />

<script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ja.js"></script>
```

# 仕様
今回は日時(yyyy/mm/dd hh:mi形式)とし、分は10分単位で増分するようにしています。
下記のオプションの内容を変更すれば、時間のみにすることや秒を追加することも可能です。
https://tr.you84815.space/flatpickr/configOptions.html

```javascript
this.dateTimeInput = flatpickr(this.dateTime, {
      dateFormat : 'Y/m/d H:i',
      enableTime : true,
      minuteIncrement: 10,
      locale : 'ja',
      clickOpens : false,
      monthSelectorType : 'static',
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/724c847b-0144-ea2a-0155-939c3e512493.png)

例えば、秒を追加したい場合、dateFormatに秒の`S`を追加し、秒入力の有効`enableSeconds : true`、増分を`minuteIncrement: 1`にします。どうも秒用の増分オプションは存在せず、分と秒の増分が共有で使用されてしまっています。その為、分を10分単位にすると秒も10秒単位になってしまいます。それでもよければ、`minuteIncrement: 10`のままでもいいです。

```javascript
this.dateTimeInput = flatpickr(this.dateTime, {
      dateFormat : 'Y/m/d H:i:S',
      enableTime : true,
      enableSeconds : true,
      minuteIncrement: 1,
      locale : 'ja',
      clickOpens : false,
      monthSelectorType : 'static',
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/3ca9eabd-8a3f-6f39-8fdb-6f8c2ed8ba9a.png)

# 実装
<p class="codepen" data-height="500" data-default-tab="result" data-slug-hash="LYLpjEQ" data-user="yaju-the-encoder" style="height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/yaju-the-encoder/pen/LYLpjEQ">
  Handsontable datetimepicker</a> by やじゅ (<a href="https://codepen.io/yaju-the-encoder">@yaju-the-encoder</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# ソースコード
```html
<!DOCTYPE html>
<html lang="jp">
<body>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" />
<style>
/* 日曜日：赤 */
.flatpickr-calendar .flatpickr-innerContainer .flatpickr-weekdays .flatpickr-weekday:nth-child(7n + 1),
.flatpickr-calendar .flatpickr-innerContainer .flatpickr-days .flatpickr-day:not(.flatpickr-disabled):not(.prevMonthDay):not(.nextMonthDay):nth-child(7n + 1) {
    color: red;
}
 
/* 土曜日：青 */
.flatpickr-calendar .flatpickr-innerContainer .flatpickr-weekdays .flatpickr-weekday:nth-child(7),
.flatpickr-calendar .flatpickr-innerContainer .flatpickr-days .flatpickr-day:not(.flatpickr-disabled):not(.prevMonthDay):not(.nextMonthDay):nth-child(7n) {
    color: blue;
}

.flatpickr-current-month {
  display: flex;
  justify-content: center;
}

.cur-year {
  order : 1;
}

.cur-month:before  {
   content: '年　';
}

.cur-month {
  order: 2;
}

.flatpickr-current-month span.cur-month {
  font-weight : 300;
  padding-top : 4px;
}
</style>
</head>

<div id="grid"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ja.js"></script>
<script language="javascript" type="text/javascript">
(function(Handsontable){

  'use strict';

  const DateTimeEditor = Handsontable.editors.BaseEditor.prototype.extend();

  DateTimeEditor.prototype.init = function() {
    this.dateTime = document.createElement('input');
    this.dateTime.setAttribute('type', 'text');

    const that = this;
    this.dateTimeInput = flatpickr(this.dateTime, {
      dateFormat : 'Y/m/d H:i',
      enableTime : true,
      minuteIncrement: 10,
      locale : 'ja',
      clickOpens : false,
      monthSelectorType : 'static',
      onChange: function(selectedDates, dateStr, instance) {
        that.instance.setDataAtCell(that.row, that.col, dateStr);
      }
    })
  };

  DateTimeEditor.prototype.open = function () {
    if(this.dateTimeInput.isOpen) return;

    this.dateTimeInput.setDate(this.dateTime.value);
    this.dateTimeInput.open();

    let rect = this.TD.getBoundingClientRect();
    let offsetLeft = rect.left + window.pageXOffset || document.documentElement.scrollLeft;
    let offsetTop = rect.top + window.pageYOffset || document.documentElement.scrollTop;
    offsetTop += this.TD.offsetHeight;

    let pop = document.querySelector('div.flatpickr-calendar.animate.open.arrowTop.arrowLeft');
    pop.style.top = offsetTop + "px";
    pop.style.left = offsetLeft + "px";

    event.stopPropagation();
  };

  DateTimeEditor.prototype.close = function () {};

  DateTimeEditor.prototype.getValue = function(){
    return this.dateTime.value ;
  };

  DateTimeEditor.prototype.setValue = function(newValue){
    this.dateTime.value = newValue;
  };

  DateTimeEditor.prototype.focus = function () {};

  Handsontable.editors.DateTimeEditor = DateTimeEditor;
  Handsontable.editors.registerEditor('datetime', DateTimeEditor);
}(Handsontable));

let data = [['2021/08/27 15:30']];

let hot = new Handsontable(document.getElementById("grid"), {
  data: data,
  columns:[
     { type: 'text', width: 200, renderer: 'autocomplete', editor: 'datetime',
       config: {
         enableTime : true,
         dateFormat : 'Y/m/d H:i',
         minuteIncrement : 1,
         locale : 'ja',
         clickOpens : false,
         monthSelectorType : 'static'
       }
     }
  ],
  colHeaders: ["日時"],
  manualColumnResize: true,
  contextMenu: {
    items:{
      'row_below': { name: '１行挿入' },
      'remove_row': { name: '１行削除', disabled: function(){ return hot.countRows() < 2; }  },
      "hsep": "---------",
      'undo': { name: '戻る' },
    },
  },
});
</script>
</body>
</html>
```

# ポイント
## 脱jQuery
flatpickr自体がバニラJavaScriptで作成されているので、jQueryを使用しない方法で修正しました。
一番ネックだったのは、offsetの書き換え処理で下記サイトを参考にしました。

https://q-az.net/without-jquery-height-width-offset-scrolltop/

## イベント伝搬のキャンセル
Open処理のところでイベント伝搬のキャンセル「event.stopPropagation()」処理を入れないと、カレンダー画面が表示された途端にフォーカス外と判断されてクローズ処理が動作してしまいます。

これに気が付くまであーでもないこーでもないとカレンダーが表示されるまでに一苦労してました。

## 初期日時セット
セルに入力された内容でカレンダーの日時の状態にするためにセットしています。

```javascript
this.dateTimeInput.setDate(this.dateTime.value);
```

# 最後に
日時入力の需要があるか分からない機能ですが、あるに越したことはない。
本当はもう少し汎用的にカラムオプションに日時設定を追加するようにしても良かったんだけど、だんだん面倒くさいなってしまった。

今回の成果としては、脱jQueryでカスタムエディターを作成できたことですね。