TimelineKit

Interaction

Drag & Drop

Events can be moved and resized by dragging. Move an event to a different time or to a different resource. Resize from the left or right edge to change duration.

// Intercept event move (e.g. to validate or snap)
scheduler.events.eventMoving$.subscribe(args => {
  // args: { event, newStartTime, newEndTime, newResource, cancel, adjustedStartTime, adjustedEndTime }

  // Cancel the move
  args.cancel = true;

  // Or adjust the target time (snap to hour)
  const start = new Date(args.newStartTime);
  start.setMinutes(0, 0, 0);
  args.adjustedStartTime = start;
  const end = new Date(args.newEndTime);
  end.setMinutes(0, 0, 0);
  args.adjustedEndTime = end;
});

// Intercept event resize
scheduler.events.eventResizing$.subscribe(args => {
  // args: { event, edge, newStartTime, newEndTime, cancel, adjustedStartTime, adjustedEndTime }

  // Prevent resizing shorter than 1 hour
  const duration = args.newEndTime.getTime() - args.newStartTime.getTime();
  if (duration < 3600000) {
    args.cancel = true;
  }
});

Context Menu

Right-click events let you build custom context menus for events, empty chart areas, and sheet rows. Use mouseEvent.preventDefault()to suppress the browser's default context menu.

// Right-click on the chart area (event or empty space)
scheduler.events.chartContextMenu$.subscribe(({ event, resource, date, mouseEvent }) => {
  mouseEvent.preventDefault();

  if (event) {
    // Clicked on an event
    showContextMenu(mouseEvent.clientX, mouseEvent.clientY, [
      { label: 'Edit', action: () => openEventDialog(event) },
      { label: 'Delete', action: () => scheduler.data.removeEvent(event) },
      { label: 'Move to...', action: () => showMoveDialog(event) },
    ]);
  } else {
    // Clicked on empty space — create a new event
    showContextMenu(mouseEvent.clientX, mouseEvent.clientY, [
      { label: 'New Event', action: () => {
        const newEvent = SchedulerEvent.fromAny({
          resourceId: resource.id,
          name: 'New Event',
          startTime: date,
          endTime: new Date(date.getTime() + 3600000), // +1 hour
        });
        scheduler.data.addEvent(newEvent);
      }},
      { label: 'Add Marker', action: () => scheduler.addMarker({ date, label: 'Marker' }) },
    ]);
  }
});

// Right-click on a sheet row
scheduler.events.sheetRowContextMenu$.subscribe(({ selectedResources, mouseEvent }) => {
  mouseEvent.preventDefault();
  showContextMenu(mouseEvent.clientX, mouseEvent.clientY, [
    { label: 'Freeze', action: () => selectedResources.forEach(r => scheduler.freezeResource(r)) },
    { label: 'Remove', action: () => scheduler.data.removeResources(selectedResources) },
  ]);
});

Selection

Click an event to select it. Hold Ctrl/Cmd to multi-select. Resources can be selected in the sheet panel.

// Get selected events
const selected = scheduler.selectedEvents;
const single = scheduler.selectedEvent; // last selected or null

// Set selection programmatically
const event = scheduler.data.getEventById('e1');
scheduler.selectedEvent = event;
scheduler.selectedEvents = [event1, event2];

// Clear selection
scheduler.selectedEvent = null;

// Get selected resources (from sheet)
const resources = scheduler.selectedResources;

// Listen for selection changes
scheduler.events.eventSelected$.subscribe(args => {
  // args: { event } or null
  console.log('Selected:', args?.event.name);
});

// Click on event bar
scheduler.events.eventClick$.subscribe(({ event }) => {
  showEventDetails(event);
});

Double-click & Delete

Handle double-click to open an edit dialog, and intercept deletions to show a confirmation prompt.

// Open edit dialog on double-click
scheduler.events.eventDblClick$.subscribe(({ event }) => {
  openEventDialog(event);
});

// Confirm before deleting (triggered by Delete key)
scheduler.events.eventDeleting$.subscribe((args) => {
  const confirmed = window.confirm(
    'Delete "' + args.event.name + '"?'
  );
  if (!confirmed) {
    args.cancel = true;
  }
});

Creating Events

Drag on an empty area of the chart to create a new event. The event is added to the resource row where you started dragging.

// Intercept event creation (e.g. to validate or customize)
scheduler.events.eventCreating$.subscribe(args => {
  // args: { resource, startTime, endTime, cancel, adjustedStartTime, adjustedEndTime }

  // Prevent creation on certain resources
  if (args.resource.type === 'equipment') {
    args.cancel = true;
    return;
  }

  // Snap to hour boundaries
  const start = new Date(args.startTime);
  start.setMinutes(0, 0, 0);
  args.adjustedStartTime = start;
  const end = new Date(args.endTime);
  end.setMinutes(0, 0, 0);
  args.adjustedEndTime = end;
});

// Listen for new events
scheduler.events.eventAdded$.subscribe(({ event }) => {
  console.log('New event:', event.name, 'on', event.resourceId);
});

Keyboard Navigation

Built-in keyboard shortcuts for common actions:

Ctrl+ZUndo
Ctrl+Y / Ctrl+Shift+ZRedo
Ctrl+CCopy selected events
Ctrl+XCut selected events
Ctrl+VPaste events
Ctrl++ / Ctrl+=Zoom in
Ctrl+-Zoom out
DeleteDelete selected events (triggers eventDeleting$)

Filtering

Filter resources to show only matching rows. Hidden resources and their events are excluded from the view.

// Filter by resource type
scheduler.filter = (resource) => resource.type === 'person';

// Filter by name
scheduler.filter = (resource) => resource.name.toLowerCase().includes('alice');

// Filter by custom property
scheduler.filter = (resource) => resource.getPropertyValue('department') === 'Engineering';

// Clear filter (show all)
scheduler.clearFilter();

Frozen Resources

Pin resources to the top of the view. Frozen resources stay visible while scrolling.

const resource = scheduler.data.getResourceById('r1');

// Freeze (pin to top)
scheduler.freezeResource(resource);

// Check if frozen
scheduler.isResourceFrozen(resource); // true

// Unfreeze
scheduler.unfreezeResource(resource);

Sorting Resources

Sort resources programmatically using any comparison function. The sort order is preserved in the view.

// Sort alphabetically by name
scheduler.sortResources((a, b) => a.name.localeCompare(b.name));

// Sort by resource type, then by name
scheduler.sortResources((a, b) => {
  const typeOrder = { person: 0, room: 1, equipment: 2, vehicle: 3 };
  const ta = typeOrder[a.type] ?? 99;
  const tb = typeOrder[b.type] ?? 99;
  if (ta !== tb) return ta - tb;
  return a.name.localeCompare(b.name);
});

// Sort by number of events (busiest first)
scheduler.sortResources((a, b) => {
  const aCount = scheduler.data.getEventsForResource(a.id).length;
  const bCount = scheduler.data.getEventsForResource(b.id).length;
  return bCount - aCount;
});

Undo & Redo

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

scheduler.undo();
scheduler.redo();

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

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

Copy & Paste

Copy and paste events using the clipboard. Pasted events are added to the same resource.

// Copy selected events
await scheduler.copy();

// Cut (copy + remove)
await scheduler.cut();

// Paste
const result = await scheduler.paste();
// result: { events: SchedulerEvent[], eventIdMap: Map<string, SchedulerEvent> } or null

// Listen for paste
scheduler.events.eventsPasted$.subscribe(({ events, eventIdMap }) => {
  console.log('Pasted', events.length, 'events');
});

Synchronizing Changes

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

// Track event changes (moves, resizes, name edits)
scheduler.events.eventChanged$.subscribe(({ event }) => {
  api.updateEvent(event.id, {
    resourceId: event.resourceId,
    name: event.name,
    startTime: event.startTime,
    endTime: event.endTime,
  });
});

// Track new events (created by dragging on empty space)
scheduler.events.eventAdded$.subscribe(({ event }) => {
  api.createEvent({
    id: event.id,
    resourceId: event.resourceId,
    name: event.name,
    startTime: event.startTime,
    endTime: event.endTime,
  });
});

// Track removed events
scheduler.events.eventRemoved$.subscribe(({ event }) => {
  api.deleteEvent(event.id);
});

// Track resource changes
scheduler.events.resourceChanged$.subscribe(({ resource }) => {
  api.updateResource(resource.id, { name: resource.name });
});

scheduler.events.resourceAdded$.subscribe(({ resource }) => {
  api.createResource({ id: resource.id, name: resource.name, type: resource.type });
});

scheduler.events.resourceRemoved$.subscribe(({ resource }) => {
  api.deleteResource(resource.id);
});

For bulk operations (initial load, import), use load() and save() to transfer the full state as JSON instead of reacting to individual events.