商用利用が可能なJavaScript製のカレンダーUIライブラリーEvent Calendarを使って、複数のリソースの予約状況を並べて表示するデモを作ってみました。さらに、ステップ・バイ・ステップで、デモを作っていく様子をご紹介します。
Event Calendarを使う
Event Calendarは複数のリソースを並べて表示できるMITライセンスのカレンダーライブラリーです。素のJavaScript(Vanilla JS)で書かれていて軽量です。READMEが少し不足しているのか、あまりスターがついていないのが残念です。
まずはじめに、Event Calendarを使ったデモをご紹介します。
- 3つのリソース(Room 1, Room 2, Room 3)の予約状況を表示します。
- 重複する予約はできません。
- Editable Eventはドラッグやサイズ変更が可能です。
- Uneditable Eventはマウス操作では変更できません。クリックして表示されるダイアログから変更します。
- カレンダー上の空いている時間をドラッグすると、新しく予約できます。
- 画面右上にある矢印ボタンで日付を変更できます。今日から1週間分のみ表示できます。
See the Pen Event Calendar Demo by Hirokazu Takatama (@takatama) on CodePen.
このデモを作るまでの過程を、3ステップに分けて紹介していきます。
Step1: カレンダーを表示し、空き時間をドラッグして予定を追加
今回はCDNを使ってライブラリーを読み込みます。Event Calendarの他に、日付の計算するためにday.jsも利用します。
<body>
にカレンダーを表示する<div>
(id="ec")を挿入します。
<head>
<link href="https://cdn.jsdelivr.net/npm/@event-calendar/build@1.5.0/event-calendar.min.css" rel="stylesheet">
</head>
<body>
<div id="ec"></div>
<script src="https://cdn.jsdelivr.net/npm/@event-calendar/build@1.5.0/event-calendar.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.9/dayjs.min.js"></script>
</body>
表示するリソースresources
、予定events
を定義し、new EventCalendar(element, options)
でカレンダーを生成します。
空き時間をドラッグしたときに予定を追加できるよう、カレンダーのオプションでselectable: true
にして、新しい予定が追加されたときに呼び出されるselect: function(event)
を定義します。
select関数の中では、addEvent(event)
で予定を追加します。その際、予定のevent.id
とevent.resourceIds[]
を設定しています。
const resources = [{
id: 1, title: 'Room 1'
}, {
id: 2, title: 'Room 2'
}, {
id: 3, title: 'Room 3'
}];
function createDate(hours, minutes) {
const now = new Date();
now.setHours(hours);
now.setMinutes(minutes);
now.setSeconds(0);
return now;
}
const events = [{
id: 1,
resourceIds: [1],
start: createDate(9, 0),
end: createDate(10, 30),
title: 'Editable Event',
editable: true,
}, {
id: 2,
resourceIds: [2],
start: createDate(11, 0),
end: createDate(13, 30),
title: 'Uneditable Event',
// editable: false, // not work
startEditable: false,
durationEditable: false,
backgroundColor: 'red',
}];
const ec = new EventCalendar(document.getElementById('ec'), {
resources,
events,
view: 'resourceTimeGridDay',
allDaySlot: false,
slotMinTime: '08:00:00',
slotMaxTime: '19:00:00',
nowIndicator: true,
selectable: true,
select: function(event) {
addEvent(event);
}
});
function addEvent(event) {
event.id = new Date().getTime();
event.resourceIds = [ event.resource.id ];
ec.addEvent(event);
ec.unselect();
}
Step2: 重複する予約は許可しない
重複する予約の追加や変更ができないようにします。
重複する予定がないか調べるために、新しく追加する(変更する)予定と、同じリソースで、かつ、時間帯が重なっている予定を取得するgetOverlappingEvents()
を追加します。この関数を使って、重複する予定の存在を確認するhasOverlappingEvents()
と、調査対象の予定自体を除いた他の重複する予定を確認するhasOtherOverlappingEvents()
を定義します。
そして、カレンダー上に新しい予定を追加したときに呼び出されるselect
の中で、重複する予定があれば予定の追加は中断します。また、ドロップ操作をしたときのeventDrop
と、予定をドラッグ操作で変更したときのeventResize
では、重複する予定があるときに、その操作をなかったことにします(revert)。
// resources, createDate(), events はそのまま
// ★追加
function getOverlappingEvents(event) {
// select event has event.resource.id
// eventDrop event has event.resourceIds
const rId = event.resource ? event.resource.id : event.resourceIds[0];
return ec.getEvents().filter(e => e.resourceIds[0] == rId && e => e.start < event.end && event.start < e.end);
}
// ★追加
function hasOverlappingEvents(event) {
return getOverlappingEvents(event).length > 0;
}
// ★追加
function hasOtherOverlappingEvents(event) {
return getOverlappingEvents(event).filter(e => e.id != event.id).length > 0
}
const ec = new EventCalendar(document.getElementById('ec'), {
resources,
events,
view: 'resourceTimeGridDay',
allDaySlot: false,
slotMinTime: '08:00:00',
slotMaxTime: '19:00:00',
nowIndicator: true,
selectable: true,
select: function(event) {
// ★追加
if (hasOverlappingEvents(event)) {
ec.unselect();
return;
}
addEvent(event);
},
// ★追加
eventDrop: function ({ event, revert }) {
if (hasOtherOverlappingEvents(event)) revert();
},
// ★追加
eventResize: function ({ event, revert }) {
if (hasOtherOverlappingEvents(event)) revert();
},
});
function addEvent(event) {
event.id = new Date().getTime();
event.resourceIds = [ event.resource.id ];
ec.addEvent(event);
ec.unselect();
}
Step3: この先1週間の予定だけ表示する
ずっと先の予約ができると困る場合があります。そこで、表示できる期間を制限してみます。
カレンダーのオプションで、画面右上にある矢印ボタンで日付を変更したときに呼び出されるdatesSet
を追加します。start
が表示している日付です。
toggleDateButtonsFor7Days()
で、start
が今日から1週間分のときだけボタンを有効にします。無効にしたときにボタンの表示が変わるよう、CSSも追加しておきます。
const ec = new EventCalendar(document.getElementById('ec'), {
...
},
// ★追加
datesSet: function ({ start }) {
toggleDateButtonsFor7Days(start);
}
});
// ★追加
function toggleDateButtonsFor7Days(start) {
const next = document.querySelector('.ec-next');
const prev = document.querySelector('.ec-prev');
const now = dayjs();
const targetDate = dayjs(start);
prev.disabled = targetDate.isBefore(now);
next.disabled = targetDate.isAfter(now.add(6, 'day'));
}
.ec-prev:disabled, .ec-next:disabled {
background-color: #ced4da;
}
Step4: モーダルダイアログで予定のコメントを入力する
モーダルダイアログを作るのに便利なHTMLタグ<dialog>
を使って、予定を作るときにコメントを入力できるようにします。カレンダーのオプションselect
では(addEvent()
の代わりに)showModal()
を呼び出します。dialog
をsubmit
するときに、addEvent()
で予定を追加します。
また、予定をクリックしたときに、ダイアログを表示します。カレンダーのオプションに、eventClick
を追加して、showModal()
を呼び出します。このままdialog
をsubmit
するとaddEvent()
が呼ばれますが、予定の追加ではなく更新にしたいので、event.id
がある場合は更新だと判断して、カレンダーのupdateEvent()
を呼び出すように変更します。
<dialog id="dialog">
<form id="form" method="dialog">
<h2>Make Reservation</h2>
<h3 id="room-name"></h3>
<h3 id="date"></h3>
<div class="row">
<label for="start">Start:</label>
<input type="time" id="start" name="start-time">
</div>
<div class="row">
<label for="end">End:</label>
<input type="time" id="end" name="end-time">
</div>
<div class="row">
<label for="comment">Comment:</label>
<textarea id="comment" name="comment"></textarea>
</div>
<menu>
<button id="cancel" type="reset">Cancel</button>
<button id="confirm" type="submit">Confirm</button>
</menu>
</form>
</dialog>
dialog {
width: 400px;
padding: 20px;
border: solid 1px;
}
.row {
display: flex;
margin: 10px;
}
.row label {
width: 90px;
}
textarea {
width: 250px;
}
const ec = new EventCalendar(document.getElementById('ec'), {
...
select: function (event) { // on create event
if (hasOverlappingEvents(event)) {
ec.unselect();
return;
}
// ★変更
showModal(event);
},
...
// ★追加
eventClick: function ({ event }) {
showModal(event);
},
});
function addEvent(event) {
// ★追加
if (event.id) {
ec.updateEvent(event);
return;
}
event.id = new Date().getTime();
event.resourceIds = [ event.resource.id ];
ec.addEvent(event);
ec.unselect();
}
const dialog = document.querySelector('dialog');
function showModal(event) {
function getResourceTitle(event) {
const resourceId = event.resource ? event.resource.id : event.resourceIds[0];
const resource = resources.find(r => r.id == resourceId);
return resource ? resource.title : '';
}
document.getElementById('room-name').innerText = getResourceTitle(event);
const startDate = dayjs(event.start);
document.getElementById('date').innerText = startDate.format('YYYY/MM/DD');
document.getElementById('start').value = startDate.format('HH:mm');
document.getElementById('end').value = dayjs(event.end).format('HH:mm');
document.getElementById('comment').value = event.title || '';
dialog.event = event;
dialog.showModal();
}
document.getElementById('form').onsubmit = function(e) {
e.preventDefault();
const event = dialog.event;
const startTime = document.getElementById('start').value.split(':');
event.start.setHours(Number(startTime[0]));
event.start.setMinutes(Number(startTime[1]));
const endTime = document.getElementById('end').value.split(':');
event.end.setHours(Number(endTime[0]));
event.end.setMinutes(Number(endTime[1]));
const comment = document.getElementById('comment').value;
event.title = comment;
addEvent(event);
dialog.close();
}
document.getElementById('cancel').onclick = function() {
dialog.close();
};
完成形
最後に、完成したコードを掲載します。
Powered by Event Calendar <a target="_blank" href="https://github.com/vkurko/calendar">https://github.com/vkurko/calendar</a>
<div id="ec"></div>
<dialog id="dialog">
<form id="form" method="dialog">
<h2>Make Reservation</h2>
<h3 id="room-name"></h3>
<h3 id="date"></h3>
<div class="row">
<label for="start">Start:</label>
<input type="time" id="start" name="start-time">
</div>
<div class="row">
<label for="end">End:</label>
<input type="time" id="end" name="end-time">
</div>
<div class="row">
<label for="comment">Comment:</label>
<textarea id="comment" name="comment"></textarea>
</div>
<menu>
<button id="cancel" type="reset">Cancel</button>
<button id="confirm" type="submit">Confirm</button>
</menu>
</form>
</dialog>
<script src="https://cdn.jsdelivr.net/npm/@event-calendar/build@1.5.0/event-calendar.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.9/dayjs.min.js"></script>
.ec-prev:disabled, .ec-next:disabled {
background-color: #ced4da;
}
dialog {
width: 400px;
padding: 20px;
border: solid 1px;
}
.row {
display: flex;
margin: 10px;
}
.row label {
width: 90px;
}
textarea {
width: 250px;
}
const resources = [
{ id: 1, title: 'Room 1' }, { id: 2, title: 'Room 2' }, { id: 3, title: 'Room 3'}
];
function createDate(hours, minutes) {
const now = new Date();
now.setHours(hours);
now.setMinutes(minutes);
now.setSeconds(0);
return now;
}
const events = [{
id: 1,
resourceIds: [1],
start: createDate(9, 0),
end: createDate(10, 30),
title: 'Editable Event',
editable: true,
}, {
id: 2,
resourceIds: [2],
start: createDate(11, 0),
end: createDate(13, 30),
title: 'Uneditable Event',
// editable: false, // not work
startEditable: false,
durationEditable: false,
backgroundColor: 'red',
}];
function getOverlappingEvents(event) {
// select event has event.resource.id
// eventDrop event has event.resourceIds
const rId = event.resource ? event.resource.id : event.resourceIds[0];
return ec.getEvents().filter(e => e.resourceIds[0] == rId && e => e.start < event.end && event.start < e.end);
}
function hasOverlappingEvents(event) {
return getOverlappingEvents(event).length > 0;
}
function hasOtherOverlappingEvents(event) {
return getOverlappingEvents(event).filter(e => e.id != event.id).length > 0
}
const ec = new EventCalendar(document.getElementById('ec'), {
resources,
events,
view: 'resourceTimeGridDay',
allDaySlot: false,
slotMinTime: '08:00:00',
slotMaxTime: '19:00:00',
nowIndicator: true,
selectable: true,
select: function(event) {
if (hasOverlappingEvents(event)) {
ec.unselect();
return;
}
showModal(event);
},
eventDrop: function ({ event, revert }) {
if (hasOtherOverlappingEvents(event)) revert();
},
eventResize: function ({ event, revert }) {
if (hasOtherOverlappingEvents(event)) revert();
},
datesSet: function ({ start }) {
toggleDateButtonsFor7Days(start);
},
eventClick: function ({ event }) {
showModal(event);
},
});
function toggleDateButtonsFor7Days(start) {
const next = document.querySelector('.ec-next');
const prev = document.querySelector('.ec-prev');
const now = dayjs();
const targetDate = dayjs(start);
prev.disabled = targetDate.isBefore(now);
next.disabled = targetDate.isAfter(now.add(6, 'day'));
}
function addEvent(event) {
if (event.id) {
ec.updateEvent(event);
return;
}
event.id = new Date().getTime();
event.resourceIds = [ event.resource.id ];
ec.addEvent(event);
ec.unselect();
}
const dialog = document.querySelector('dialog');
function showModal(event) {
function getResourceTitle(event) {
const resourceId = event.resource ? event.resource.id : event.resourceIds[0];
const resource = resources.find(r => r.id == resourceId);
return resource ? resource.title : '';
}
document.getElementById('room-name').innerText = getResourceTitle(event);
const startDate = dayjs(event.start);
document.getElementById('date').innerText = startDate.format('YYYY/MM/DD');
document.getElementById('start').value = startDate.format('HH:mm');
document.getElementById('end').value = dayjs(event.end).format('HH:mm');
document.getElementById('comment').value = event.title || '';
dialog.event = event;
dialog.showModal();
}
document.getElementById('form').onsubmit = function(e) {
e.preventDefault();
const event = dialog.event;
const startTime = document.getElementById('start').value.split(':');
event.start.setHours(Number(startTime[0]));
event.start.setMinutes(Number(startTime[1]));
const endTime = document.getElementById('end').value.split(':');
event.end.setHours(Number(endTime[0]));
event.end.setMinutes(Number(endTime[1]));
const comment = document.getElementById('comment').value;
event.title = comment;
addEvent(event);
dialog.close();
}
document.getElementById('cancel').onclick = function() {
dialog.close();
};