TimelineKit

Interaction

Drag & Drop

Entries can be moved and resized by dragging. Move an entry to a different time or day. Resize from the top or bottom edge in week/day views to change duration. In month view, drag entries between days.

// Intercept entry move (e.g. to validate or snap)
calendar.events.entryMoving$.subscribe(args => {
  // args: { entry, newStartTime, newEndTime, cancel }

  // Cancel the move
  args.cancel = true;

  // Or snap to 15-minute intervals
  const start = new Date(args.newStartTime);
  const minutes = Math.round(start.getMinutes() / 15) * 15;
  start.setMinutes(minutes, 0, 0);
  args.newStartTime = start;
});

// Intercept entry resize
calendar.events.entryResizing$.subscribe(args => {
  // args: { entry, newStartTime, newEndTime, edge, cancel }

  // Prevent resizing shorter than 15 minutes
  const duration = args.newEndTime.getTime() - args.newStartTime.getTime();
  if (duration < 15 * 60 * 1000) {
    args.cancel = true;
  }
});

Context Menu

Right-click events let you build custom context menus for entries and empty date areas. Use domEvent.preventDefault()to suppress the browser's default context menu.

// Right-click on an entry
calendar.events.entryContextMenu$.subscribe(({ entry, domEvent }) => {
  domEvent.preventDefault();
  showContextMenu(domEvent.clientX, domEvent.clientY, [
    { label: 'Edit', action: () => openEntryDialog(entry) },
    { label: 'Delete', action: () => calendar.data.removeItem(entry.item) },
    { label: 'Duplicate', action: () => {
      const clone = entry.item.clone(null);
      calendar.data.addItem(clone);
    }},
  ]);
});

// Right-click on an empty date/time
calendar.events.dateContextMenu$.subscribe(({ date, isAllDay, entry, domEvent }) => {
  domEvent.preventDefault();
  if (!entry) {
    showContextMenu(domEvent.clientX, domEvent.clientY, [
      { label: 'New Event', action: () => openCreateDialog(date, isAllDay) },
      { label: 'Go to Day', action: () => {
        calendar.goToDate(date);
        calendar.viewMode = 'day';
      }},
    ]);
  }
});

Double-click & "+N more"

Handle double-click to open an edit dialog. In month view, handle the "+N more" overflow click to show all events for that day.

// Open edit dialog on double-click
calendar.events.entryDblClick$.subscribe(({ entry }) => {
  openEntryDialog(entry);
});

// Handle "+N more" click in month view (switch to day view)
calendar.events.moreClick$.subscribe(({ date, entries }) => {
  calendar.goToDate(date);
  calendar.viewMode = 'day';
});

Selection

Click an entry to select it. Hold Ctrl/Cmd to multi-select.

// Get selected entries
const selected = calendar.selectedEntries;
const single = calendar.selectedEntry; // last selected or null

// Set selection programmatically
calendar.selectedEntry = entry;
calendar.selectedEntries = [entry1, entry2];

// Clear selection
calendar.selectedEntry = null;

// Listen for selection changes
calendar.events.selectedEntriesChanged$.subscribe(entries => {
  console.log('Selected:', entries.length, 'entries');
});

Creating Events

Drag on an empty area in week/day views to create a new entry. Click an empty date cell in month view. The new item is added to the default calendar.

// Intercept entry creation
calendar.events.entryCreating$.subscribe(args => {
  // args: { startTime, endTime, isAllDay, cancel }

  // Prevent creation on weekends
  const day = args.startTime.getDay();
  if (day === 0 || day === 6) {
    args.cancel = true;
  }
});

// Listen for date clicks (e.g. to open a creation dialog)
calendar.events.dateClick$.subscribe(args => {
  // args: { date, isAllDay, domEvent }
  console.log('Clicked on', args.date, args.isAllDay ? '(all-day)' : '');
});

Editing Recurring Events

When a recurring entry is moved, resized, or deleted, the operation can apply to a single occurrence, this and all following occurrences, or the entire series.

Recurrence Scope

thisOnly this occurrence. Creates a RecurrenceException with overrides.
thisAndFollowingThis and all future occurrences. Splits the series at this point.
allAll occurrences. Modifies the master CalendarItem directly.
// Intercept deletion of a recurring entry
calendar.events.entryDeleting$.subscribe(args => {
  // args: { entry, isRecurring, scope, cancel }
  // scope is 'this' | 'thisAndFollowing' | 'all' (for recurring items)

  if (args.isRecurring && args.scope === 'all') {
    const confirmed = window.confirm('Delete all occurrences?');
    if (!confirmed) args.cancel = true;
  }
});

Keyboard Navigation

Built-in keyboard shortcuts for common actions:

Arrow keysNavigate between entries or dates.
EnterOpen / confirm the selected entry.
EscapeCancel current operation or deselect.
DeleteDelete selected entries (triggers entryDeleting$).
Ctrl+ZUndo
Ctrl+Y / Ctrl+Shift+ZRedo
Ctrl+CCopy selected entries
Ctrl+XCut selected entries
Ctrl+VPaste entries

Filtering

Filter visible entries by toggling calendar visibility in the sidebar or programmatically.

// Toggle calendar visibility (built-in via sidebar calendar list)
const workCal = calendar.data.getCalendarById('c1');
workCal.isVisible = false; // hides all items in this calendar

// Show only specific calendars
calendar.data.getCalendarById('c2').isVisible = true;

Undo & Redo

Full undo/redo support for all data changes — entry moves, resizes, additions, removals, and property modifications.

calendar.undo();
calendar.redo();

// Observe availability (e.g. for toolbar button state)
calendar.canUndo$.subscribe(canUndo => {
  undoButton.disabled = !canUndo;
});
calendar.canRedo$.subscribe(canRedo => {
  redoButton.disabled = !canRedo;
});

// Listen for undo/redo
calendar.events.undone$.subscribe(() => console.log('Undone'));
calendar.events.redone$.subscribe(() => console.log('Redone'));

Copy & Paste

Copy and paste entries using the clipboard.

// Copy selected entries
await calendar.copyEntries();

// Copy specific entries
await calendar.copyEntries([entry1, entry2]);

// Cut (copy + remove)
await calendar.cutEntries();

// Paste
await calendar.pasteEntries();

Visible Range & Lazy Loading

Listen for changes to the visible date range to load data on demand. This fires when the user navigates or switches view mode.

// Load events on demand as the user navigates
calendar.events.viewDateRangeChanged$.subscribe(({ start, end }) => {
  // Fetch events for the new visible range from your backend
  fetchEvents(start, end).then(items => {
    calendar.data.loadItems(items);
  });
});

Synchronizing Changes

Use change events to synchronize the calendar state with a backend API or database. Events fire for every user interaction — dragging, resizing, creating, deleting, and editing.

// Track item changes (moves, resizes, property edits)
calendar.events.itemChanged$.subscribe(({ item }) => {
  api.updateItem(item.id, {
    calendarId: item.calendarId,
    title: item.title,
    startTime: item.startTime,
    endTime: item.endTime,
    isAllDay: item.isAllDay,
    recurrenceRule: item.recurrenceRule,
  });
});

// Track new items
calendar.events.itemAdded$.subscribe(({ item }) => {
  api.createItem({
    id: item.id,
    calendarId: item.calendarId,
    title: item.title,
    startTime: item.startTime,
    endTime: item.endTime,
  });
});

// Track removed items
calendar.events.itemRemoved$.subscribe(({ item }) => {
  api.deleteItem(item.id);
});

// Track calendar changes (name, color, visibility)
calendar.events.calendarChanged$.subscribe(({ calendar: cal }) => {
  api.updateCalendar(cal.id, { name: cal.name, color: cal.color });
});

calendar.events.calendarAdded$.subscribe(({ calendar: cal }) => {
  api.createCalendar({ id: cal.id, name: cal.name, color: cal.color });
});

calendar.events.calendarRemoved$.subscribe(({ calendar: cal }) => {
  api.deleteCalendar(cal.id);
});