Something went wrong

Thank you for being patient! We're working hard on resolving the issue

Parent Plugins - Lona Docs Log in

Parent Plugins

Parent plugins host subcolumn children in a shared grid layout. The calendar plugin is the primary example — it renders a time grid and lets child rows (calendar-list, task-list) place items within it.

Overview

A parent plugin:

  1. Discovers child rows from the row tree
  2. Asks each child for its rendering info via subcolumnProvider
  3. Lays out all children in a shared coordinate system
  4. Provides positioning APIs so children can show menus
Parent plugin (calendar)
  ├── Owns the grid layout (time columns, hour markers)
  ├── Owns the gesture manager (pointer tracking across cells)
  ├── Discovers children via row.children()
  └── Positions menus for children via SheetDelegate handles

Child plugin (calendar-list)
  ├── Provides items via subcolumnProvider.events()
  ├── Provides cell rendering via subcolumnProvider.bindCell()
  ├── Creates menu content (details, forms, context menus)
  └── Calls positioning APIs provided by parent

Discovering Children

In bindCell(), iterate the row's children and collect subcolumns:

bindCell($cell: CalendarColumn, context: BindCellContext): void {
  const { row, handle } = context;
  const subcolumns = this.getSubcolumns(row, handle.pluginManager);
  $cell.bind({ subcolumns, /* ... */ });
}

private getSubcolumns(
  row: SheetRow.Joined.Client,
  pluginManager: SheetDelegate.PluginManager,
): CalendarSubcolumn<any>[] {
  const children = row.children?.() ?? [];
  const subcolumns: CalendarSubcolumn<any>[] = [];

  for (const child of children) {
    const plugin = pluginManager.getPlugin(child.type);
    const sub = plugin.subcolumnProvider?.subcolumnFromRow(sheet, child, row);
    if (sub) subcolumns.push(sub);
  }

  return subcolumns;
}

This is fully generic — any plugin with a subcolumnProvider becomes a child. The parent doesn't know or care what content the child renders.

Rendering Children in the Grid

For cell-based subcolumns (renderer: "cells"), the parent creates calendar cells and lets each subcolumn bind them:

for (const subcolumn of subcolumns) {
  if (subcolumn.renderer !== "cells") continue;

  const items = subcolumn.events(timeRange);
  for (const item of items) {
    const $cell = CalendarColumnCell.make();

    // Child controls appearance
    subcolumn.bindCell($cell, item, { windowed, currentTime });

    // Parent controls position (time → pixel offset)
    const top = this.timeToPixels(item.startTime);
    const height = this.durationToPixels(item.duration);
    $cell.assignStyles({ top: `${top}px`, height: `${height}px` });

    $column.appendChild($cell);
  }
}

For span-based subcolumns (renderer: "spans"), the parent requests span data and renders horizontal bars across columns.

Providing Gesture Context

The parent creates gesture info for each cell. The gesture info carries the parent's coordinate data (column position, row ID) that children need for positioning menus:

// Parent creates gesture info with its coordinate system
const gestureItem = subcolumn.gestureItem(item);
const info = new GestureInfo(
  rowId,
  gestureItem,                            // from child
  column.withSubindex(subindex),          // from parent
  windowed,                               // from parent
);

// Bind cell to gesture manager
$cell.bindCallbacks(
  this.gestureManager.asCellDelegate(info),
);

The child provides gestureItem() — a wrapper around its item that the gesture system can inspect. The parent provides column and windowed — the coordinate data needed to position menus.

Gesture Manager

The parent owns a gesture manager that tracks pointer state across all cells. Gesture handlers are registered on this manager:

constructor() {
  this.gestureManager = new CalendarGesture.Manager<GestureInfo>();
}

The app layer registers gesture handlers that use both the child's content and the parent's positioning:

// App layer wiring
manager
  .addGesture(new ClickToOpenGesture(presenter, coordinator))
  .addGesture(new DragToCreateGesture(presenter, coordinator))
  .addGesture(new ContextMenuGesture(presenter, coordinator));

Aggregating Full-Day Items

If children provide fullDayEventsFromRow, the parent aggregates them for the all-day row:

subcolumnProvider: CalendarSubcolumn.Provider<EventItem, FullDayItem> = {
  subcolumnFromRow(sheet, row, parentRow) { /* ... */ },

  fullDayEventsFromRow(row, range): FullDayItem[] {
    const children = row.children?.() ?? [];
    const items: FullDayItem[] = [];
    for (const child of children) {
      const plugin = pluginManager.getPlugin(child.type);
      const childItems = plugin.subcolumnProvider?.fullDayEventsFromRow?.(child, range);
      if (childItems) items.push(...childItems);
    }
    return items;
  },
};

Different Layout Strategies

Parent plugins can implement different grid layouts while keeping the child interface identical:

Daily/Weekly Grid (PopoverHandle)

// Column-based positioning — scroll-aware
handle.popover.reveal(id, $content, { x: column, y: rowId }, { offsetY });

The daily grid uses ColumnIndex.Key.Resolved.WithSubindex for X coordinates. Each column is a date or time slot. Popovers track scroll position automatically.

Monthly Grid (StaticChildHandle)

// Pixel-based positioning — no column tracking
const $cell = this.$calendar.getCellForDate(date);
const rect = $cell.getBoundingClientRect();
const offset = { x: rect.left - calRect.left, y: rect.top - calRect.top };
handle.staticChild.reveal(id, $content, rowId, { offset });

The monthly grid uses raw pixel offsets computed from DOM rects. No column index, no scroll awareness — the entire month fits in one row.

Adding a New Layout

To create a new parent layout (e.g., a Gantt chart), implement a plugin that:

  1. Discovers children via row.children() + subcolumnProvider
  2. Renders items in your layout's coordinate system
  3. Provides gesture info with your coordinate data
  4. Shows menus using the appropriate SheetDelegate handle

The child subcolumns don't change — they still provide events(), bindCell(), and gestureItem() the same way.

See also