はじめに
Handsontable のセル種類には、time
があり書式による入力バリデーションチェックは用意されているものの、Date
のように DatePicker が標準で用意されていません。
https://handsontable.com/docs/time-cell-type/#overview
https://handsontable.com/docs/date-cell-type/#overview
Handsontable のライバル製品である Jspreadsheet(旧jExcel)では、CUSTOM COLUMN TYPEのサンプルとして、ClockPickerを見ることが出来る。これは、weareoutman氏が作成した ClockPicker を使用している。
これなら、Handsontable でも同様なことができるし、既に作成している人がいるのでは無いかと検索してみると見つけました。しかし、7年前ということもあってデモサイトは既にリンク切れになっており、ソースコードを元に動かしても init処理のところでエラーになってしまいます。
さらに検索してみると、なんと日本の方(posturanさん)が直近(2021/08/07)で実現していました。
これを動かしてみると幾つか改善したい部分が見つかったので、参考にしつつ自前で作成することにしました。
環境
HandsontableはMITライセンス版のバージョン 6.2.2を使用しています。
一応、有償版バージョン 8.3.2でも動作は確認しています。
CDN+α
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.css">
<link rel="stylesheet" href="https://weareoutman.github.io/clockpicker/dist/jquery-clockpicker.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment-with-locales.min.js"></script>
<script src="https://weareoutman.github.io/clockpicker/dist/jquery-clockpicker.js"></script>
jquery-clockpickerは、weareoutman氏のgithubから直接取得しています。cdnjs上のでは正常に動作しませんでした。
仕様
データは(HH24:MI形式)、24時間制の前0埋め2桁の時(hour)と区切り文字に:
と前0埋め2桁の分(minute)を指定します。
let data = [
{ A:'09:08', B:'15:20', C:'18:45'}
];
columns の指定ですが、renderer: 'autocomplete', editor: 'time' を指定します。
rendererを autocomplete にしているのは、セルの右端に「▼」を表示したいためです。
10分や15分単位など分刻みを制限したい場合、sourceに配列で指定します。(posturanさんのアイディアを参考)
let hot = new Handsontable(document.getElementById("grid"), {
data: data,
columns:[
{ data: 'A', type: 'text', renderer: 'autocomplete', editor: 'time' },
{ data: 'B', type: 'text', renderer: 'autocomplete', editor: 'time', source: ['00','10','20','30','40','50'] },
{ data: 'C', type: 'text', renderer: 'autocomplete', editor: 'time', source: ['00','15','30','45'] }
],
実装
See the Pen Handsontable jquery-clockpicker by やじゅ (@yaju-the-encoder) on CodePen.
通常の時入力(左下図)と分入力(右下図)になります。
10分単位(左下図)と15分単位(右下図)を指定した場合
ソースコード
<!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://weareoutman.github.io/clockpicker/dist/jquery-clockpicker.min.css" />
<style>
</style>
</head>
<div id="grid"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment-with-locales.min.js"></script>
<script src="https://weareoutman.github.io/clockpicker/dist/jquery-clockpicker.js"></script>
<script language="javascript" type="text/javascript">
(function(Handsontable){
'use strict';
const TimeEditor = Handsontable.editors.BaseEditor.prototype.extend();
let choices;
let locate;
TimeEditor.prototype.init = function() {
this.clock = document.createElement('input');
this.clock.setAttribute('type', 'text');
const that = this;
this.clockInput = $(this.clock).clockpicker({
placement: 'bottom',
align: 'left',
autoclose: true,
afterShow: function(){
$('.clockpicker-minutes').find('.clockpicker-tick').show();
if(choices == undefined || choices.length == 0) return;
$('.clockpicker-minutes').find('.clockpicker-tick').filter(function(index, element){
return !($.inArray($(element).text(), choices) != -1)
}).hide();
},
afterDone: function(){
if(choices != undefined && choices.length > 0) {
let selectedMinutes = that.clockInput.val().split(":")[1];
if ($.inArray(selectedMinutes, choices) == -1) {
let time = that.clockInput.val().split(":");
let hour = time[0];
let minutes = choices[0];
for(minutes of choices) {
if(time[1] < minutes) break;
}
that.clockInput.val(hour + ':' + minutes)
that.clockInput.clockpicker('show').clockpicker('toggleView', 'minutes');
let $cpop = $('.clockpicker-popover');
$cpop.offset(locate);
return;
}
}
that.instance.setDataAtCell(that.row, that.col, that.clockInput.val());
}
});
};
TimeEditor.prototype.prepare = function () {
Handsontable.editors.BaseEditor.prototype.prepare.apply(this, arguments);
choices = this.cellProperties.source;
};
TimeEditor.prototype.open = function () {
//make sure that editor position matches cell position
let $td = $(this.TD);
let offset = $td.offset();
this.clockInput.clockpicker('show');
//remove clockpicker event handlers
$(document).off('click.clockpicker.cp1 focusin.clockpicker.cp1');
let $cpop = $('.clockpicker-popover');
locate = { top: offset.top + $td.height() + 10, left: offset.left };
$cpop.offset(locate);
$('.clockpicker-hours, .clockpicker-span-hours, .clockpicker-span-minutes').on('mousedown mouseup', function (event) {
event.stopPropagation();
});
$('.clockpicker-minutes').on('mousedown', function (event) {
event.stopPropagation();
});
};
TimeEditor.prototype.close = function () {
this.clockInput.clockpicker('hide');
};
TimeEditor.prototype.getValue = function(){
return $('.clockpicker-span-hours').text() + ':' + $('.clockpicker-span-minutes').text();
};
TimeEditor.prototype.setValue = function(newValue){
this.clock.value = newValue;
};
TimeEditor.prototype.focus = function () {};
Handsontable.editors.TimeEditor = TimeEditor;
Handsontable.editors.registerEditor('time', TimeEditor);
}(Handsontable));
let data = [
{ A:'09:08', B:'15:20', C:'18:45'}
];
let hot = new Handsontable(document.getElementById("grid"), {
data: data,
columns:[
{ data: 'A', type: 'text', renderer: 'autocomplete', editor: 'time' },
{ data: 'B', type: 'text', renderer: 'autocomplete', editor: 'time', source: ['00','10','20','30','40','50'] },
{ data: 'C', type: 'text', renderer: 'autocomplete', editor: 'time', source: ['00','15','30','45'] }
],
colHeaders: ["制限なし", "10分単位", "15分単位" ],
manualColumnResize: true,
contextMenu: {
items:{
'row_below': { name: '1行挿入' },
'remove_row': { name: '1行削除', disabled: function(){ return hot.countRows() < 2; } },
"hsep": "---------",
'undo': { name: '戻る' },
},
},
});
</script>
</body>
</html>
問題点の解決
参考サイトの問題点
Editor の beginEditing (編集開始)にならないと Clockpicker が画面に現れない点だ。
(中略)
focus 時処理で、this.beginEditing(); を実行することで、編集開始にしてしまう。
でも、この方法は、Clockpicker を表示するセルだけ、右クリックの
コンテキストメニュー、任意のコンテキストメニュー表示ができなくなる。これを解決させる方法がまだわからない。
https://oboe2uran.hatenablog.com/entry/2021/08/08/175117
今回はこの問題点を解決している。デモとしてコンテキストメニューを付けてある。
columnsのsource取得方法変更
修正前
修正前は、afterShowとafterDoneの2ヶ所で別々に取得していました。
let choices = that.instance.getCellMeta(that.row, that.col).source;
これは下記の方法で値は取得できたのですが、Open時に「Uncaught TypeError: Cannot read property 'readOnly' of undefined handsontable.full.min.js;35」のエラーとなり解決方法が不明だったからです。
TimeEditor.prototype.prepare = function (row, col, prop, td, originalValue, cellProperties) {
choices = cellProperties.source;
};
修正後
参考のソースコードの方法を取り入れることでエラーが出なくなりました。
参考:https://gist.github.com/ben-dalton/6fd4cad0917cad150324afc8060562a3
TimeEditor.prototype.prepare = function () {
Handsontable.editors.BaseEditor.prototype.prepare.apply(this, arguments);
choices = cellProperties.source;
};
最後に
正直、Custom Editorの作り方がまだよく分かっていない。raduvulturさんとposturanさんのソースコードがなければ辿り着かなかったかも知れない。
それでも、それなりのものが出来たので、今度はDateTimePickerに挑戦してみたくなった。