Skip to main content

Interface: PluginContextParts<TGantt, TDataset>

Defined in: view/plugin-ports.ts:152

The parts of one plugin's PluginContext that view/ owns — what GanttShell hands to options.buildPluginContext so it can build the whole thing. Declared in the groups a plugin reads, so api/gantt.ts adds the two api-level members and nothing else (#150).

#183: Parts, not Ports. Every other *Ports in this repo is one collaborator's seam back into its owner (ColumnChromePorts, GanttShellPorts below). This is not that. It is the plugin's own world, minus the two members view/ may not name. It is also the one name here a consumer reads, because api/index.ts exports it to keep the plugin surface member-by-member in etc/freegantt.api.md (#166, I11). PlainParts sets the suffix: the pieces a composite is made of.

#166: this is the member list for the plugin surface. api/plugin-context.ts's public PluginContextOf is PluginContextParts<TGantt, TDataset> plus dataset and gantt, and nothing else. It used to be a hand-typed copy of all twenty-five, doc comments included, with nothing checking the copy. So the doc a plugin author reads lives here now, beside the one declaration.

#191: TGantt/TDataset are what commands and registerKeybinding bind. api/plugin-context.ts bound them instead. So this interface published two members that were wrong for every consumer, and PluginContextOf had to Omit both back out. Both arguments default to unknown. A caller that binds neither — buildPluginPorts below is the only one — reads the two members unbound.

#155: every register* here returns a Disposer that removes exactly its own registration. The plugin's own DisposableStore already holds a copy, so a plugin that never calls it still disposes cleanly on uninstall. The return value is what lets a plugin retract a registration while it is still installed — a column it shows in one mode only. Calling it twice is safe.

Type Parameters​

TGantt​

TGantt = unknown

TDataset​

TDataset = unknown

Properties​

commands​

commands: CommandRegistryOf<TGantt, TDataset>

Defined in: view/plugin-ports.ts:170

The one command registry. register here is legal only while setup runs. run/available work any time, including after this plugin's own setup returns. A command this plugin registers lives exactly as long as the plugin. Uninstalling restores whatever the id held before. For an overridden core command that is core's own (#155).


disposables​

disposables: DisposableStore

Defined in: view/plugin-ports.ts:165

This plugin's own cleanup list — add a listener or a timer here instead of closing over it by hand in the returned Disposer. Disposed in reverse order, ahead of that returned Disposer.


events​

events: GanttEvents

Defined in: view/plugin-ports.ts:157

on/off over GanttEventMap, including the cancelable before* pairs. Not gated — a plugin may call on after its own view() returns. It is still tracked, the same as every other seam: the plugin's own DisposableStore holds the returned Disposer. Uninstalling the plugin, a plugins reassignment, or Gantt.destroy() all remove the handler.


interaction​

interaction: object

Defined in: view/plugin-ports.ts:171

registerKeyHandler​

registerKeyHandler: RegisterKeyHandler

C3, plans/reviews/2026-09-02-s5-start-fixes.md: binds chord straight to handler, through the same keymap registerKeybinding uses. It serves a caller with no Command to run. A plugin that builds its own Popup (over view.overlay below) is that caller. Its Escape dismissal then wins by the keymap's own newest-first order, the same way extensions/popup.ts's own dismissal does.

Two things differ from registerKeybinding. This one is callable any time the plugin is installed, not only while setup runs. That is because a popup opens and closes for as long as the plugin does, not once at startup. This one also returns its own disposer instead of auto-removing on plugin disposal. That is because a popup adds and removes its handler on every open()/close(), not once.

announceEntryEdit()​

announceEntryEdit(payload): void

Announces the committed edit. This raises entryEdit after the commit. It tells, it does not ask — no veto, and nothing to return.

Parameters​
payload​

EntryFieldEdit

Returns​

void

canWrite()​

canWrite(entry, field): WriteVerdict

The edit capability's one resolution (I14). It is the same answer move/resize already read through interaction/entry-gestures.ts's ctx.can. This surface exposes it because inlineEditing() is the first plugin that needs to ask it. Every other capability check lives inside core's own gesture wiring, which a plugin cannot reach.

#256: the question names a cell — one Entry, one Field — because that is what a write names. The same answer gates the bar's resize handles and its move. So a plugin that asks it here cannot disagree with the gesture that writes the same value. A refusal that carries a reason is one the user must be told about. A refusal with none is already visible, because nothing offered the write at all.

Parameters​
entry​

Entry

field​

FieldKey

Returns​

WriteVerdict

proposeEntryEdit()​

proposeEntryEdit(payload): boolean | Promise<boolean>

Proposes the edit, and the answer is a Veto. This raises beforeEntryEdit on this Gantt's own event bus. It returns exactly what the registered handlers returned: true/undefined (no veto), false, or an unsettled Promise (the async-veto shape, U8's async (…) => { await myDialog.open(...); return false }). Read the answer — a plugin that ignores it opens an editor the consumer refused. This is the one seam a plugin has to raise a before* pair it implements itself. Core's own gesture pipeline raises every other before* event, so this is scoped to one event name. A generic emit would let a plugin forge selectionChange, or any other event core itself owns.

Parameters​
payload​

EntryFieldEdit

Returns​

boolean | Promise<boolean>

registerKeybinding()​

registerKeybinding(binding): Disposer

Adds one KeyBinding. Legal only while setup runs — removed automatically when this plugin is disposed, the same lifetime every other register* gets. The returned Disposer removes it sooner, for a plugin that binds a chord only in one mode (#155). Ignoring the return value is the common case.

Parameters​
binding​

KeyBindingOf<TGantt, TDataset>

Returns​

Disposer


variants​

variants: object

Defined in: view/plugin-ports.ts:358

ADR 0018: one variant is one object, so one door installs it. ctx.variants.add(variant) is the plugin half of the GanttOptions.variants a consumer writes — one type, two doors, one shape. It replaced four registrations that each repeated the variant's name.

add()​

add(variant): Disposer

Installs one variant on this Gantt. when says which rows wear it, bars what shape it draws, paint how it looks, and can what you can do to it. Omit when and the variant answers for every row nothing newer matches.

The newest rule wins. This plugin's variant wins over core's own parent/leaf, and over any variant installed before it. The consumer's own GanttOptions.variants wins over every plugin's, whatever order the plugins installed in. Setup order between two plugins comes from requires — there is no ordering knob here.

Two plugins whose rules both answer yes for one row raise a 'variant-matched-twice' Error report naming both, in every build. It is not behind isDevMode(): that flag resolves when this repo builds dist/, so gating it would delete the line from every consumer. The library never arbitrates between plugins: the consumer chose which ones to install, so core reports and carries on.

when runs on the hover path, so keep it cheap. It is pure: it answers a question and draws nothing. Legal only while setup runs, and disposal removes the variant the same way every other register* seam's does.

Parameters​
variant​

EntryVariant

Returns​

Disposer


view​

view: object

Defined in: view/plugin-ports.ts:214

dom​

dom: GanttDom

Review N1/A3: this Gantt's own rendered DOM, as three questions — owns(node), targetUnder(node), and barFor(id)/cellFor(id, field). It is the whole plugin-to-DOM contract. extensions/ may not import render/, so before this seam every plugin retyped .fg-bar, .fg-row, data-bar-id and five more by hand. Nothing versioned them and nothing tested them. Renaming a class broke every plugin with a green build.

targetUnder returns { kind, element, entry?, field? }. kind is TargetKind, the same five words CommandTarget uses, so one vocabulary covers a resolved right-click and a command's own when. Live for the plugin's whole lifetime, not gated by RegistrationGate.

overlay​

overlay: MountLayer

The layer a plugin's own popup, tooltip or menu mounts into — the same primitive extensions/popup.ts's Popup is built on. It escapes the pane box, so content here may spill past a pane edge.

Live for the plugin's whole lifetime, not gated by RegistrationGate. The gate only covers one-shot register* calls, and a plugin presents and dismisses content for as long as it runs.

rowLayer​

rowLayer: MountLayer

#158: the grid's own row layer. It is for content that must stay glued to a row or a cell while the pane scrolls. An open cell editor is the case.

The Grid pane has no vertical scrollbar of its own. This layer follows the Timeline pane's scroll by one transform per frame. The pane scrolls horizontally around it. Content mounted here therefore travels with the rows on both axes, in the same frame — no scroll listener, and no lag behind the paint. Position it once against rowLayer.bounds.

Use overlay instead for content that must escape the pane box. This layer is clipped to the pane. A popup dismisses on a scroll rather than following it. Live for the plugin's whole lifetime, the same posture as overlay.

focusedCell()​

focusedCell(): { entryId: EntryId; field: FieldKey; } | undefined

Which cell real keyboard focus sits on right now — an entry id and a Field key. undefined when focus is not on a cell (a row, a bar, a header cell, the splitter, or nothing focused at all). This is a fact, not a node: focus is a view concern. This port is how a plugin reads it without touching view state or re-deriving it from the DOM itself. (inline-editing.ts's Enter handler is the first caller — ctx.view.focusedCell().)

Returns​

{ entryId: EntryId; field: FieldKey; } | undefined

onDomEvent()​

onDomEvent<K>(type, handler, options?): Disposer

Review A4: one scoped document listener. It filters to this Gantt (I2). It hands the handler the resolved DomTarget instead of a raw node. It registers its own removal in ctx.disposables, capture flag included, which a hand-written removeEventListener has to match by hand. Call: ctx.view.onDomEvent('dblclick', (event, target) => { … }). The returned Disposer removes it sooner, for a listener a plugin attaches per open popup.

Two Gantts on one page never answer each other's events, which is what a hand-written guard kept getting wrong (bug hunt B1). One case is wrong for this seam: a listener that must hear events outside this Gantt, a dismiss-on-outside-pointer, say. extensions/popup.ts keeps its own unscoped listener for exactly that.

Type Parameters​
K​

K extends keyof DocumentEventMap

Parameters​
type​

K

handler​

DomEventHandler<K>

options?​

DomEventOptions

Returns​

Disposer

registerDecoration()​

registerDecoration(layer, provider): Disposer

Registers a pure decoration provider into layer (underBars below the bar layer, overBars above). Legal only while setup runs. Disposing this plugin removes the provider automatically. A provider has no close()/unregister() of its own, so the plugin's own lifetime is its lifetime. Call: ctx.view.registerDecoration('underBars', (ctx) => ctx.time.eachDay(ctx.span).filter((day) => ctx.time.dayOfWeek(day) >= 6).map((day) => ({ kind: 'rangeBand', start: day, end: ctx.time.addDays(day, 1) }))). The returned Disposer removes the provider sooner (#155).

Parameters​
layer​

DecorationLayer

provider​

DecorationProvider

Returns​

Disposer

registerGridColumn()​

registerGridColumn(column): Disposer

Registers column on this Gantt's grid, appended after the consumer's own gridColumns in registration order. A duplicate field the consumer's own list already names is dropped (config beats a plugin). The Field it names still resolves through the ordinary Field registry (UnknownFieldError/FieldColumnNotDefinedError apply unchanged). Legal only while setup runs; removed automatically when this plugin is disposed. When two plugins register the same field, the newest registration wins, and disposing one plugin never disturbs the other plugin's registration. The returned Disposer removes the column sooner — what a plugin showing its column in one mode only calls (#155).

The column is this plugin's declaration, and it stays that way (#181). It never joins gantt.gridColumns, and it never joins a gridColumnsChange payload. A resize or a reorder of it commits and repaints, and still changes neither. So a consumer who saves gridColumns saves their own columns only. Declare the column again on the next install: a Document carries no plugin declaration to restore it from.

This plugin owns this column's width and its place (#189). The library reports column geometry; it stores it for nobody, the consumer included. A user resize of this column lives in session state and reaches no Document. To carry it across a reload, do what a consumer does with gridColumnsChange. Listen for that same event. Read your own column back from ctx.view.resolvedColumns(), by field — never from the payload, which reports the consumer's columns alone. Save the width wherever this plugin's own options say. Then pass it here on the next install. A plugin that skips this ships a column that resizes for the session only, which is a legitimate choice to make on purpose.

Parameters​
column​

GridColumnInput

Returns​

Disposer

registerRenderer()​

registerRenderer<P>(point, renderer): Disposer

Claims one of the four renderer points — bar, cell, header, tooltip. A consumer's own GanttOptions.*Renderer always wins over this. A consumer that wants a plugin's renderer to win removes its own instead.

cell, header and tooltip hold one slot each. A cell belongs to a column and a header to a band, so neither has a key to merge on. The bar point's per-kind map form holds one slot per kind. So a plugin that defines one kind and a plugin that defines another both install (review P2). The whole-point form — a function, not a map — stays exclusive. It answers every kind, so it refuses, and is refused by, any per-kind claim.

Two plugins claiming one slot throws RendererAlreadyRegisteredError, naming the slot and both plugin ids. Legal only while setup runs. Disposing the plugin frees every slot this call claimed. So uninstalling and re-installing one plugin is a legal sequence, and not a collision with its own earlier registration (#155). The returned Disposer frees them sooner.

Type Parameters​
P​

P extends RendererPoint

Parameters​
point​

P

renderer​

RendererFor<P>

Returns​

Disposer

renderElement()​

renderElement(description): HTMLElement

Builds a live node from an ElementDescription — the one seam extensions/ has to the reconciler. extensions/ may not import render/ itself. Never innerHTMLd except the description's own explicit html opt-in (I13).

Call: ctx.view.renderElement(description), then mount the node in either layer above.

Parameters​
description​

ElementDescription

Returns​

HTMLElement

resolvedColumns()​

resolvedColumns(): readonly GridColumn[]

Every Grid column this Gantt paints right now, in paint order. The consumer's own columns and every plugin's are both here, each with its Field defaults already merged. gantt.gridColumns answers a different question: what the consumer authored. A plugin that walks the grid wants this one. Call: for (const column of ctx.view.resolvedColumns()).

Returns​

readonly GridColumn[]

resolveTooltipColumns()​

resolveTooltipColumns(entry): readonly TooltipColumn[]

Every Grid column marked tooltip: true, resolved against this Gantt's current gridColumns/fields — header text and entry's formatted value for each. tooltips()'s default body appends these after name/dates. A consumer that builds its own tooltip content reads the same list, instead of re-resolving columns itself (view/grid-columns.ts stays out of reach). Empty when no column is marked tooltip: true.

Parameters​
entry​

Entry

Returns​

readonly TooltipColumn[]

resolveTooltipContent()​

resolveTooltipContent(entryId): ElementDescription | undefined

S5.5 (API gap found while building tooltips()): resolves what should paint entryId's tooltip body right now. It is the same precedence registerRenderer('tooltip', …)'s slot resolves at paint time. The consumer's own GanttOptions.tooltipRenderer always wins over a plugin's.

undefined means "paint the library's own default content instead". Three cases answer that way. First, no renderer is registered at either level. Second, the entry has no bar in the current frame, so there is no FrameBar to build a TooltipRendererContext from. A hover plugin works from the DOM after the fact, unlike bar/cell's render-pass callers. Third, the resolved renderer threw, and this method catches it and logs it in dev mode. That third answer is the same fallback render/dom/index.ts's own callRenderer gives bar/cell (issue #137).

tooltips() is this method's first caller. A feature that owns a renderer point reads the same resolution the render backend would, without reaching view/renderer-registry.ts directly.

Parameters​
entryId​

string | EntryId

Returns​

ElementDescription | undefined

variantFor()​

variantFor(entry): ResolvedVariant

ADR 0018, ADR 0022 §3: the variant this Gantt resolved for one row — the same answer the layout pass painted with. A plugin that builds a CommandContext of its own fills variant from .name here (extensions/features/context-menu.ts is the first caller; variant stays a string). A variant is per Gantt, so a row cannot answer it (I2).

Parameters​
entry​

Entry

Returns​

ResolvedVariant

Methods​

raiseError()​

raiseError(report): void

Defined in: view/plugin-ports.ts:162

Raises one Error report on this Gantt's error event. by is filled with this plugin's own id, so a subscriber can always tell which plugin spoke. Use severity: 'info' for a Refusal the plugin made on purpose, 'warning' for something it recovered from, 'error' for something it did not.

Parameters​

report​

PluginErrorReport

Returns​

void