Implementing a Custom Calendar Component with Konva.js
This article explains how to build a canvas‑based calendar widget using Konva, covering the design of date cells, task‑range rendering, drag‑and‑drop interaction, event handling, and the public API, and provides the full source code for reference.
The article introduces a custom calendar component built on the HTML5 canvas using the Konva library, aiming to replicate the functionality seen in large‑scale enterprise products while remaining framework‑agnostic.
Feature Overview : the component draws a month view with weekday headers, displays dates from the previous and next months, supports task‑range visualization, and provides interactive features such as dragging to adjust task periods.
Technical Choices : Konva is used directly instead of wrapper libraries like react‑konva or vue‑konva, allowing the component to be integrated into any front‑end framework.
Core Configuration Interface :
export interface KonvaCalendarConfig {
// mode: read | edit (affects whether dates can be dragged)
mode: 'read' | 'edit';
// container selector
container: string;
// optional initial date
initDate?: Date | 'string';
}Instantiation example:
const bootstrap = new CanvasKonvaCalendar({
mode: 'edit',
container: '.smart-table-root',
initDate: new Date('2024-10-20')
});
bootstrap.setData([
{ startTime: '2024-09-30', endTime: '2024-09-30', fill: 'rgba(49,116,173,0.8)', description: '1', id: uuid() }
]);
bootstrap.on('ADDTASKRANGE', day => console.log('Add date', day));
bootstrap.on('CLICKRANGE', day => console.log('Select date', day));Calendar Rendering draws the year/month header, weekday titles, and then iterates over previous‑month, current‑month, and next‑month dates, creating a Konva.Group for each cell with a background rectangle, day number, and optional Chinese lunar text.
Task‑Range Rendering splits tasks that span multiple weeks into weekly chunks, assigns each chunk a unique origin identifier, and calculates the rectangle width as dayCount * cellWidth . Overlap handling uses a sizeMap (Map<number, string[][]>) to allocate separate y‑offsets for intersecting tasks.
// Example of split task data
[
{
"startTime": "2024-10-01",
"endTime": "2024-10-06",
"fill": "rgba(0,0,255,0.3)",
"description": "3",
"origin": "12345",
"id": "bb47c948-6ab0-4a47-8adf-13f8ed952643",
"day": 19
},
...
]Interaction Logic includes mouse events:
mousedown – identifies the target task group and records drag offsets.
dragMousemove – clones the target group, reduces its opacity, and moves the clone according to pointer movement.
mouseup – determines the drop cell, updates the task’s start/end dates, redraws the task layer, and cleans up temporary objects.
private mousedown(): void {
if (this.config.mode === 'read') return;
const result = this.findGroup(this.featureLayer, '.task-progress-group');
if (!result) return;
const { group, pointer, rect } = result;
this.recordsDragGroupRect = {
differenceX: pointer.x - rect.x,
differenceY: pointer.y - rect.y,
sourceX: rect.x,
sourceY: rect.y,
targetGroup: group,
startX: pointer.x,
startY: pointer.y
};
}The component also exposes a simple public API for updating data, registering custom events, exporting the calendar as an image, and navigating between months.
setData(ranges: Range[]): this { this.taskRanges = ranges; this.draw(); return this; }
nextMonth(): void { this.month++; if (this.month > 11) { this.month = 0; this.year++; } this.featureLayer.removeChildren(); this.draw(); }Finally, the author notes that the implementation may not follow best practices everywhere, invites community contributions after open‑sourcing the library, and provides links to the npm package and GitHub repository.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.