import type { RowNode } from '../types/row.types'; import type { ColumnDef } from '../types/column.types'; import type { GridStore } from '../core/grid-store'; import type { EventBus } from '../event-bus/event-bus'; import type { IconRenderer } from '../icons/icon-renderer'; import type { RowSelectionEngine } from '../engines/selection/row-selection-engine'; import { EmptyDetailToggleMode } from '../types/master-detail.types'; import type { VDomStats } from './vdom/vdom.types'; import { type TreeToggleRenderConfig } from './tree-cell-renderer'; export interface BodyRendererOptions { showCheckboxes?: boolean; showSerialNumber?: boolean; /** When true, serial cells become AG Grid–style row-selection column entries. */ serialColumnSelection?: boolean; showVerticalBorders?: boolean; rowShading?: boolean; /** * Returns additional CSS classes for a data row. Re-evaluated whenever the * row is rendered so classes stay current as virtualized DOM is reused. */ rowClassFn?: (row: Record, index: number) => string; rowHeight?: number; api?: unknown; dateFormat?: string; timeZone?: string; currencySymbol?: string; locale?: string; showGroupsColumn?: boolean; autoGroupColWidth?: number; /** * `ColumnDef` for the innermost (deepest) grouping field. * * When provided, leaf data rows render an interactive cell in the auto-group * column showing the row's value for this field — selectable, editable, and * copyable exactly like any normal data cell. * * `null` (or omitted) when no grouping is active; a plain spacer is rendered * instead to maintain column alignment. */ leafGroupColDef?: ColumnDef | null; /** * Full, unfiltered column list — includes columns hidden by horizontal * virtualization AND columns hidden because they're the active group-by * field. Used to resolve a group row's `groupField` to its `ColumnDef` so a * custom `renderer.group` can be looked up even though that column itself * renders no cell of its own while grouped. */ allLeafColumns?: ColumnDef[]; /** * `false` when `GridOptions.editing.mode` is `'none'`. * * Only `boolean` columns read it: their inline checkbox renders disabled when * the grid cannot commit an edit, so it never looks clickable while being * inert. */ editingEnabled?: boolean; centerColStart?: number; centerLeftSpacerW?: number; centerRightSpacerW?: number; totalCenterCols?: number; /** * When Master/Detail is enabled, drives the expand/collapse toggle icon * rendered on `'data'` rows for the configured toggle column. */ masterDetail?: { toggleColumnId: string; isExpandedFn: (nodeId: string) => boolean; hasDetailFn: (rowData: Record) => boolean; /** What to render in the toggle column for a row `hasDetailFn` rejects — see {@link EmptyDetailToggleMode}. */ emptyToggleMode: EmptyDetailToggleMode; }; /** * When Tree Data is enabled, drives indentation (`data-level` on the row * element) and the expand/collapse toggle rendered on `'data'` rows with * children, in the configured toggle column. */ treeData?: TreeToggleRenderConfig; } export declare class BodyRenderer { private store; private eventBus; private iconRenderer; private rowSelectionEngine; private cellRenderer; private renderedRowMap; /** * Viewport Virtual DOM — the mirror of the rows currently in the DOM. * * Kept in sync at the end of every {@link renderRows} pass so that real-time * data updates can be applied as individual cell patches instead of row * rebuilds. See `src/renderer/vdom`. */ private readonly vdom; /** Reused buffer feeding `ViewportVDom.sync` — avoids an array per render. */ private readonly syncRefs; private leftContent; private centerContent; private rightContent; /** Whether serial-column row selection is active (enables the block outline). */ private serialColumnSelection; private readonly lastCenterRange; /** * Widths of the two center virtual-scroll spacers as currently painted. * * Column widths can change without the visible column *set* changing (a * resize of an off-screen column, a flex re-resolve), and the spacers stand in * for exactly those off-screen widths — so they need a dirtiness signal of * their own or the center panel silently drifts out of horizontal alignment. */ private readonly lastSpacerW; /** * The ordered column ids last painted into each panel, plus the global column * index the panel started at. * * A column change is detected by comparing against these rather than by being * told about it, so every path that alters the layout — a reorder, a pin, a * hide, or the horizontal virtual window sliding by one column — takes the * same in-place reconcile route with no caller opt-in. */ private readonly lastPanelColIds; private readonly lastPanelColOffset; /** * Every visible column by id, refreshed at the top of each {@link renderRows}. * * Row-level delegated listeners resolve a clicked cell's `ColumnDef` through * this map. Closing over the panel's column array instead would go stale the * moment a row survives a column change — which, now that rows are reconciled * rather than rebuilt, is the normal case rather than the exception. */ private readonly colDefById; /** * The options of the most recent {@link renderRows} pass. * * Delegated listeners read this rather than a captured `options` object so a * row that survives a re-render — which, with in-place column reconciliation, * is now the normal case — never resolves against a stale `leafGroupColDef`. */ private lastOptions; /** * Signature of the options that decide a row's *shape* rather than its cells. * * The column reconciler only knows how to add, move and remove data cells. A * serial column, a checkbox, the auto-group cell and the tree/master-detail * decorations are built once per row and sit outside that contract, so when * one of them is switched on or off the rows have to be rebuilt outright — * reconciling would leave the panels misaligned by exactly one cell. */ private lastRowShapeKey; private leftSticky; private centerSticky; private rightSticky; /** `nodeId`s of the rows currently parked in the sticky containers — a single Master/Detail master row, or a stack of Tree Data ancestor rows. */ private stuckNodeIds; /** Panels participating in hover, cached from `setPanels` for class updates. */ private hoverPanels; /** `nodeId` of the row currently showing `pg-row--hover`, or `null`. */ private hoveredNodeId; /** Last known pointer viewport coordinates, used to re-hit-test on scroll. */ private pointerX; private pointerY; /** `true` while the pointer is over the body — gates scroll-driven hover sync. */ private pointerInside; constructor(store: GridStore, eventBus: EventBus, iconRenderer: IconRenderer, rowSelectionEngine: RowSelectionEngine); setPanels(leftContent: HTMLElement | null, centerContent: HTMLElement, rightContent: HTMLElement | null): void; /** * Sets (or clears) the hovered row by `nodeId`, moving the `pg-row--hover` * class across all panels in one pass. No-op when the target is unchanged, so * it is cheap to call every scroll frame. * * Resolves the row's parts through `renderedRowMap` — an O(1) lookup of the * exact three elements — rather than a `querySelectorAll` sweep per panel. * Falls back to a scoped query only for a row that is rendered but not in the * map (a Master/Detail or Tree row parked in the sticky overlay, whose DOM is * re-parented out of the content panels by `setStickyRows`). */ private setHoveredRow; /** Adds/removes `pg-row--hover` on every panel part of one row. */ private applyHoverClass; /** * Re-evaluates which row sits under the last-known pointer position and * updates the hover accordingly. Called at the start of every `renderRows` * (before row classes are derived) so that while the body scrolls under a * stationary cursor (wheel/momentum), the row now beneath the pointer becomes * the hovered one. Cheap when idle: a single `elementFromPoint` only while the * pointer is inside the body. */ refreshHoverAtPointer(): void; /** Wires the per-panel sticky-row overlay containers. Called once from `GridRenderer` when `masterDetail.enabled`. */ setStickyContainers(left: HTMLElement | null, center: HTMLElement | null, right: HTMLElement | null): void; /** * Parks each entry's row in the sticky overlay (pinned at the panel's own * top, ignoring the scroll transform, stacked in array order) — releasing * whatever was previously stuck but isn't in `entries` back into normal * scrolled flow. A single entry reproduces the old Master/Detail behavior; * multiple entries stack Tree Data's ancestor-row chain, each at its own * `top` (see `TreeStickyRowTracker`). * * Moves the *actual* cached DOM nodes (not clones) so every existing * listener, selection class, and edit-in-progress state carries over * untouched — this only ever runs on already-rendered rows. */ setStickyRows(entries: ReadonlyArray<{ nodeId: string; top: number; }>): void; renderRows(rows: RowNode[], leftCols: ColumnDef[], centerCols: ColumnDef[], // visible slice only rightCols: ColumnDef[], options?: BodyRendererOptions): void; /** * Refreshes {@link colDefById} for the columns reachable from a rendered cell. * * Prefers `allLeafColumns` (which also carries columns hidden by horizontal * virtualization) and falls back to the three panel slices when the caller did * not supply it. */ private refreshColumnIndex; /** * Resolves a clicked cell's `ColumnDef` from its `data-col-id`. * * @param colId - Value of the cell's `data-col-id` attribute. * @param row - Row the cell belongs to, used to pick the auto-group variant. */ private resolveCellColumn; /** * `true` when a panel's painted columns no longer match the requested ones. * * Compares the global index base as well as the id sequence: a change to the * number of columns in an earlier panel shifts every `data-col-index` in this * one even though its own ids are untouched. */ private panelNeedsReconcile; /** Records the columns just painted into a panel, reusing the stored array. */ private commitPanelColumns; /** * Brings one already-rendered panel row's data cells in line with a new * ordered column list, **without rebuilding the row**. * * Every column that survives keeps its exact cell element — and with it the * DOM produced by a custom renderer, an in-flight ``, a `` a * sparkline has already painted, an open editor, and the focus ring. Only * columns that genuinely entered the layout get a new element, and only * columns that genuinely left it are detached. * * Cells are placed with a single right-to-left pass anchored on the trailing * virtual-scroll spacer, so a cell that is already in the right place costs a * sibling comparison and no DOM write at all: reordering *n* columns by one * position moves one node, not *n*. * * @param rowEl - The panel's row element. * @param row - Row being reconciled (always a `'data'` row). * @param panel - Which panel `rowEl` belongs to. * @param cols - Columns this panel must show, in display order. * @param colOffset - Global index of this panel's first column. * @returns Whether any cell element was created or detached. */ private reconcilePanelCells; /** Applies a virtual-scroll spacer's pixel width without touching its class list. */ private sizeSpacer; /** * Reconciles the Virtual DOM with the rows currently in the DOM. * * Rows whose panel elements were reused keep their recorded cells, so a pure * scroll costs a map write per row and nothing else. * * @param rows - Rows in the current render window. * @param options - Render options carrying the column list and formatting. */ private syncVirtualDom; /** * Builds the render context the Virtual DOM needs to reproduce a cell exactly * as the initial render produced it. * * Returns `null` when the column list is unavailable — the diff has nothing * to resolve against and must be skipped rather than guessed at. */ private buildVDomContext; /** * Diffs rendered rows against their last-rendered values and writes only the * cells that changed. * * This is the real-time update path: no row is rebuilt, no cell element is * replaced, and every piece of cell state (focus, open editor, selection, * hover, custom-renderer DOM) survives untouched. * * @param nodeIds - Rows to diff, or `null` for the whole viewport. * @param options - Render options carrying the column list and formatting. * @returns The number of cells written to the DOM. */ patchCells(nodeIds: Iterable | null, options: BodyRendererOptions): number; /** `true` when the row is currently rendered and tracked by the Virtual DOM. */ isRowRendered(nodeId: string): boolean; /** Virtual DOM counters — see {@link VDomStats}. */ getVDomStats(): VDomStats; /** Zeroes the Virtual DOM counters without discarding the tracked tree. */ resetVDomStats(): void; /** * Recomputes the row-selection block outline. For each rendered row it toggles * the `pg-row--sel-*` edge classes so the primary-coloured border (rows.css) * traces only the *outer* boundary of each contiguous selected run — * top/bottom where the run starts/ends, left/right on the block's outermost * panel parts. Neighbour checks use the full `visibleRows` (via `rowIndex`) so * runs extending beyond the virtualised window don't sprout interior lines. * * Only active when the serial-column selection feature is enabled; plain * checkbox selection keeps its background-only highlight. */ refreshRowSelectionEdges(): void; updateRowSelection(nodeId: string, selected: boolean): void; /** * Advances the tracked virtual-column range without touching any DOM. * * Call this when the column range has logically changed but the body rows must * NOT be rebuilt — specifically during an active column resize, where * `ColumnStyleManager` has already re-sized every cell through CSS. The next * ordinary `renderRows` reconciles whatever the range actually became, in * place, so nothing here needs to anticipate it. * * @param cStart - New first visible center-column index. * @param cEnd - New last visible center-column index (exclusive). */ syncCenterRange(cStart: number, cEnd: number): void; clear(): void; /** * Evicts only the specified rows from the render cache so they are fully * rebuilt on the next paint cycle. Rows whose `nodeId` is not in the set * are untouched — their DOM is reused as-is, so custom cell renderers * (images, flags, progress bars, etc.) are NOT re-executed for them. * * Use this instead of `clear()` after in-place data mutations (fill, cut, * paste, undo/redo) where only a known subset of rows changed. * * @param nodeIds - Set of row node IDs whose cache entries should be evicted. */ invalidateRowsByNodeId(nodeIds: Set): void; destroy(): void; /** * The live render cache: `nodeId` → the row's per-panel DOM parts. * * Exposed read-only for `RowAnimator`, which needs the exact elements that * currently represent each row. Handing over this map means the animator does * no `querySelectorAll` of its own, and — because these are the same reused * nodes across renders — it also guarantees the FLIP operates on DOM that * survived the sort rather than on freshly-built replacements. */ getRenderedRows(): ReadonlyMap; private buildPanelRow; private buildSingleRow; /** * Builds one data-row cell, fully decorated. * * Shared by the initial row build and by {@link reconcilePanelCells} so a cell * created because its column just entered the layout is byte-for-byte the same * element the initial render would have produced — there is no second, subtly * different construction path to drift. * * @param row - Row the cell belongs to. * @param col - Column being rendered. * @param colIndex - Global column index across all panels. */ private buildDataCell; /** * Builds a horizontal virtual-scroll spacer. * * Always emitted — even at zero width — so {@link reconcilePanelCells} has a * stable anchor to position cells against and a stable element to re-size when * the window moves, instead of having to synthesise one mid-reconcile. * * @param markerClass - {@link SPACER_START_CLASS} or {@link SPACER_END_CLASS}. * @param width - Combined px width of the columns this spacer stands in for. */ private buildSpacer; /** * Renders the auto-group column cell for a **leaf data row**. * * When `options.leafGroupColDef` is set (i.e. grouping is active), this cell * shows the row's actual value for the deepest grouping field and participates * fully in cell selection (colIndex −1), keyboard navigation, editing, and * copy/cut/paste — behaving exactly like any normal data cell. * * When `leafGroupColDef` is absent (no grouping active) a non-interactive * spacer cell is rendered to maintain column alignment with group header rows. * * @param el - Row container element to append the cell into. * @param row - Leaf data `RowNode` being rendered. * @param options - Renderer options; `autoGroupColWidth` controls cell width. */ private buildLeafGroupCell; private buildGroupRowContent; /** * Builds the label cell for a group **footer** row. * * Unlike the header, there is no expand/collapse toggle — the cell shows * a Σ-prefixed group value to signal "total for this group". * The cell participates in cell selection (colIndex −1) identically to the * group header's label cell. */ private buildGroupFooterContent; /** * Append one `pg-cell` per column to `el` for a group row. * * - Columns with `type === 'currency'` **and** `aggFunc` set receive a * `pg-cell--agg` cell showing the formatted aggregate value. * - All other columns receive an empty `pg-cell` to maintain column * alignment with data rows. * * Column widths are automatically applied by the {@link ColumnStyleManager} * via the `[data-col-id]` CSS rules — no inline width needed here. */ private buildGroupAggregateCells; /** * Format a computed aggregate value for display. * * - For `count` the value is emitted as a plain integer string. * - For all other functions the value is routed through {@link formatValue} * so the column's currency symbol, locale, and precision are applied. * * @param value - Raw numeric aggregate result. * @param col - Column definition (used for type and formatting options). * @param options - Renderer options (locale, currency symbol, etc.). */ private formatAggValue; private updatePanelRow; /** * Inserts the Master/Detail expand/collapse toggle as a sibling of * `.pg-cell__inner` (never inside it) — `.pg-cell__inner` is wiped and * rebuilt wholesale by cell-edit start/stop (`GridCore.startCellEdit` / * `renderCellValue`), which would silently destroy a toggle placed inside it * the first time this column is edited. * * Rows the consumer's `hasDetail` rejects still get *something* here by * default: the toggle takes up real width in the cell, so omitting it * outright shifts that row's text left and leaves the toggle column's edge * ragged wherever detail-less rows sit between expandable ones. See * {@link EmptyDetailToggleMode} for the three ways that is resolved. */ private applyMasterDetailToggle; /** Places `el` immediately before the cell's value wrapper, falling back to the cell's start when the wrapper has not been built yet. */ private insertBeforeCellInner; /** * Delegated row listeners resolve a clicked cell's column and formatting * options at event time, not at build time. * * @param el - Panel row element the listeners are delegated from. * @param row - Row the element represents. */ private attachRowListeners; /** Avoids a DOM mutation when an attribute already has the required value. */ private setAttributeIfChanged; private getRowClass; } //# sourceMappingURL=body-renderer.d.ts.map