@gp-grid/angular 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ export const GRID_BODY_TEMPLATE = `<div
2
+ #scrollContainer
3
+ style="height: 100%; overflow: auto; position: relative;"
4
+ (scroll)="onScroll()">
5
+ <div
6
+ style="position: relative; min-width: 100%"
7
+ [style.width.px]="innerWidth()"
8
+ [style.height.px]="sizerHeight()">
9
+ <div
10
+ class="gp-grid-rows-wrapper"
11
+ [style.width.px]="innerWidth()"
12
+ [style.transform]="wrapperTransform()">
13
+ @for (slot of slotsArray(); track slot.slotId) {
14
+ @if (slot.rowIndex >= 0) {
15
+ <div
16
+ [class]="rowClass(slot.rowIndex, slot.rowData)"
17
+ style="position: absolute; top: 0; left: 0"
18
+ [style.transform]="'translateY(' + slot.translateY + 'px)'"
19
+ [style.width.px]="innerWidth()"
20
+ [style.height.px]="rowHeight()"
21
+ >
22
+ @for (entry of visibleColumnWithIndices(); track entry.originalIndex; let i = $index) {
23
+ @let editing = isEditing(slot.rowIndex, entry.originalIndex);
24
+ <div
25
+ [class]="cellClass(slot.rowIndex, entry.originalIndex, entry.column, slot.rowData)"
26
+ style="position: absolute; top: 0;"
27
+ [attr.data-row-index]="slot.rowIndex"
28
+ [attr.data-col-index]="entry.originalIndex"
29
+ [style.left.px]="columnPositions()[i]"
30
+ [style.width.px]="columnWidths()[i]"
31
+ [style.height.px]="rowHeight()"
32
+ (pointerdown)="cellPointerDown.emit({ rowIndex: slot.rowIndex, colIndex: entry.originalIndex, event: $event })"
33
+ (mouseenter)="cellPointerEnter.emit({ rowIndex: slot.rowIndex, colIndex: entry.originalIndex })"
34
+ (mouseleave)="cellPointerLeave.emit()"
35
+ (dblclick)="cellDoubleClick.emit({ rowIndex: slot.rowIndex, colIndex: entry.originalIndex })"
36
+ >
37
+ @if (editing) {
38
+ @let etpl = editTemplate(entry.column);
39
+ @if (etpl) {
40
+ <ng-container
41
+ [ngTemplateOutlet]="etpl"
42
+ [ngTemplateOutletContext]="{ $implicit: editParams(slot.rowData, entry.column, slot.rowIndex, entry.originalIndex) }">
43
+ </ng-container>
44
+ } @else {
45
+ <input
46
+ class="gp-grid-edit-input"
47
+ type="text"
48
+ [value]="editInitialValue()"
49
+ autofocus
50
+ (focus)="onEditFocus($event)"
51
+ (input)="editValueChange.emit(asInput($event).value)"
52
+ (keydown)="onEditKeyDown($event)"
53
+ (blur)="editCommit.emit()" />
54
+ }
55
+ } @else {
56
+ @let tpl = cellTemplate(entry.column);
57
+ @if (tpl) {
58
+ <ng-container
59
+ [ngTemplateOutlet]="tpl"
60
+ [ngTemplateOutletContext]="{ $implicit: cellParams(slot.rowData, entry.column, slot.rowIndex, entry.originalIndex) }">
61
+ </ng-container>
62
+ } @else {
63
+ {{ cellDisplay(slot.rowData, entry.column, slot.rowIndex, entry.originalIndex) }}
64
+ }
65
+ }
66
+ </div>
67
+ }
68
+ </div>
69
+ }
70
+ }
71
+ @if (fillHandlePosition(); as fhp) {
72
+ @if (editingCell() === null) {
73
+ <div
74
+ class="gp-grid-fill-handle"
75
+ [style.top.px]="fhp.top"
76
+ [style.left.px]="fhp.left"
77
+ (pointerdown)="fillHandlePointerDown.emit({ event: $event })">
78
+ </div>
79
+ }
80
+ }
81
+ @if (rowDropIndicator(); as rd) {
82
+ <div
83
+ class="gp-grid-row-drop-indicator"
84
+ [style.transform]="'translateY(' + rd.dropIndicatorY + 'px)'"
85
+ [style.width.px]="rowDropIndicatorWidth()"></div>
86
+ }
87
+ </div>
88
+ </div>
89
+ @if (totalRows() === 0) {
90
+ <div class="gp-grid-empty">No data to display</div>
91
+ }
92
+ </div>
93
+ `;
@@ -0,0 +1,206 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ input,
5
+ output,
6
+ computed,
7
+ TemplateRef,
8
+ } from "@angular/core";
9
+ import { NgTemplateOutlet } from "@angular/common";
10
+ import type {
11
+ HeaderData,
12
+ VisibleColumnInfo,
13
+ ColumnDefinition,
14
+ HeaderRendererParams,
15
+ SortDirection,
16
+ } from '@gp-grid/core';
17
+
18
+ export type HeaderRendererTemplate = TemplateRef<{ $implicit: HeaderRendererParams }>;
19
+
20
+ export interface HeaderSortEvent {
21
+ colId: string;
22
+ direction: SortDirection | null;
23
+ addToExisting: boolean;
24
+ }
25
+
26
+ export interface HeaderPointerDownEvent {
27
+ colIndex: number;
28
+ colWidth: number;
29
+ colHeight: number;
30
+ event: PointerEvent;
31
+ }
32
+
33
+ export interface FilterPointerDownEvent {
34
+ colIndex: number;
35
+ anchorEl: HTMLElement;
36
+ }
37
+
38
+ export interface ResizePointerDownEvent {
39
+ colIndex: number;
40
+ colWidth: number;
41
+ event: PointerEvent;
42
+ }
43
+
44
+ const TEMPLATE = `
45
+ <div
46
+ class="gp-grid-header"
47
+ [class.gp-grid-header--loading]="isLoading()"
48
+ [style.height.px]="headerHeight()">
49
+ <div
50
+ style="position: absolute; top: 0; left: 0;"
51
+ [style.transform]="transformStyle()"
52
+ [style.width.px]="innerWidth()"
53
+ [style.height.px]="headerHeight()">
54
+ @for (entry of visibleColumnsWithIndices(); track entry.originalIndex; let i = $index) {
55
+ @let colW = columnWidths()[i] ?? 0;
56
+ @let headerData = headers().get(entry.originalIndex);
57
+ @let tpl = headerTemplate(entry.column);
58
+ <div
59
+ class="gp-grid-header-cell"
60
+ [attr.data-col-index]="entry.originalIndex"
61
+ [style.left.px]="columnPositions()[i]"
62
+ [style.width.px]="colW"
63
+ [style.height.px]="headerHeight()"
64
+ (pointerdown)="onHeaderPointerDown($event, entry.originalIndex, colW)">
65
+ @if (tpl) {
66
+ <ng-container
67
+ [ngTemplateOutlet]="tpl"
68
+ [ngTemplateOutletContext]="{ $implicit: headerParams(entry.column, entry.originalIndex, headerData) }">
69
+ </ng-container>
70
+ } @else {
71
+ <span class="gp-grid-header-text">{{ entry.column.headerName ?? entry.column.field }}</span>
72
+ <span class="gp-grid-header-icons">
73
+ @if (sortingEnabled() && entry.column.sortable !== false) {
74
+ <span class="gp-grid-sort-arrows">
75
+ <span class="gp-grid-sort-arrows-stack">
76
+ <svg
77
+ [class]="'gp-grid-sort-arrow-up' + (headerData?.sortDirection === 'asc' ? ' active' : '')"
78
+ width="8" height="6" viewBox="0 0 8 6">
79
+ <path d="M4 0L8 6H0L4 0Z" fill="currentColor"/>
80
+ </svg>
81
+ <svg
82
+ [class]="'gp-grid-sort-arrow-down' + (headerData?.sortDirection === 'desc' ? ' active' : '')"
83
+ width="8" height="6" viewBox="0 0 8 6">
84
+ <path d="M4 6L0 0H8L4 6Z" fill="currentColor"/>
85
+ </svg>
86
+ </span>
87
+ @if ((headerData?.sortIndex ?? 0) > 0) {
88
+ <span class="gp-grid-sort-index">{{ headerData?.sortIndex }}</span>
89
+ }
90
+ </span>
91
+ }
92
+ @if (entry.column.filterable) {
93
+ <span
94
+ [class]="'gp-grid-filter-icon' + (headerData?.hasFilter ? ' active' : '')"
95
+ (pointerdown)="onFilterPointerDown($event, entry.originalIndex)">
96
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
97
+ <path d="M4 4h16l-6 8v5l-4 2v-7L4 4z"/>
98
+ </svg>
99
+ </span>
100
+ }
101
+ </span>
102
+ }
103
+ @if (entry.column.resizable !== false) {
104
+ <div
105
+ class="gp-grid-header-resize-handle"
106
+ (pointerdown)="onResizePointerDown($event, entry.originalIndex, colW)">
107
+ </div>
108
+ }
109
+ </div>
110
+ }
111
+ </div>
112
+ </div>
113
+ `;
114
+
115
+ @Component({
116
+ selector: 'gp-grid-header',
117
+ standalone: true,
118
+ imports: [NgTemplateOutlet],
119
+ changeDetection: ChangeDetectionStrategy.OnPush,
120
+ template: TEMPLATE,
121
+ })
122
+ export class GridHeaderComponent {
123
+ headerHeight = input.required<number>();
124
+ scrollLeft = input.required<number>();
125
+ contentWidth = input.required<number>();
126
+ totalWidth = input.required<number>();
127
+ isLoading = input.required<boolean>();
128
+ visibleColumnsWithIndices = input.required<VisibleColumnInfo[]>();
129
+ columnPositions = input.required<number[]>();
130
+ columnWidths = input.required<number[]>();
131
+ headers = input.required<Map<number, HeaderData>>();
132
+ sortingEnabled = input<boolean>(true);
133
+ headerRenderers = input<Record<string, HeaderRendererTemplate>>({});
134
+ globalHeaderRenderer = input<HeaderRendererTemplate | null>(null);
135
+
136
+ headerPointerDown = output<HeaderPointerDownEvent>();
137
+ filterPointerDown = output<FilterPointerDownEvent>();
138
+ resizePointerDown = output<ResizePointerDownEvent>();
139
+ headerSort = output<HeaderSortEvent>();
140
+ headerFilterOpen = output<{ colIndex: number; anchorEl: HTMLElement }>();
141
+
142
+ protected innerWidth = computed(() =>
143
+ Math.max(this.contentWidth(), this.totalWidth())
144
+ );
145
+
146
+ protected transformStyle = computed(() =>
147
+ `translateX(${-this.scrollLeft()}px)`
148
+ );
149
+
150
+ protected onHeaderPointerDown(event: PointerEvent, colIndex: number, colWidth: number): void {
151
+ this.headerPointerDown.emit({ colIndex, colWidth, colHeight: this.headerHeight(), event });
152
+ }
153
+
154
+ protected onFilterPointerDown(event: PointerEvent, colIndex: number): void {
155
+ event.stopPropagation();
156
+ const cell = (event.currentTarget as HTMLElement).closest('.gp-grid-header-cell') as HTMLElement | null;
157
+ if (cell) {
158
+ this.filterPointerDown.emit({ colIndex, anchorEl: cell });
159
+ }
160
+ }
161
+
162
+ protected onResizePointerDown(event: PointerEvent, colIndex: number, colWidth: number): void {
163
+ event.stopPropagation();
164
+ this.resizePointerDown.emit({ colIndex, colWidth, event });
165
+ }
166
+
167
+ protected headerTemplate(column: ColumnDefinition): HeaderRendererTemplate | null {
168
+ const renderer: unknown = column.headerRenderer;
169
+ if (renderer instanceof TemplateRef) {
170
+ return renderer as HeaderRendererTemplate;
171
+ }
172
+ if (typeof renderer === 'string') {
173
+ const registered = this.headerRenderers()[renderer];
174
+ if (registered) return registered;
175
+ }
176
+ return this.globalHeaderRenderer();
177
+ }
178
+
179
+ protected headerParams(
180
+ column: ColumnDefinition,
181
+ colIndex: number,
182
+ headerData: HeaderData | undefined,
183
+ ): HeaderRendererParams {
184
+ const sortable = this.sortingEnabled() && column.sortable !== false;
185
+ const filterable = column.filterable === true;
186
+ return {
187
+ column,
188
+ colIndex,
189
+ sortDirection: headerData?.sortDirection,
190
+ sortIndex: headerData?.sortIndex,
191
+ sortable,
192
+ filterable,
193
+ hasFilter: headerData?.hasFilter ?? false,
194
+ onSort: (direction, addToExisting) => {
195
+ if (sortable) {
196
+ const colId = column.colId ?? column.field;
197
+ this.headerSort.emit({ colId, direction, addToExisting });
198
+ }
199
+ },
200
+ onFilterClick: () => {
201
+ // The anchor is looked up via data-col-index — same pattern as the default filter icon.
202
+ // No-op here; consumers using a custom header template should use the exposed callback.
203
+ },
204
+ };
205
+ }
206
+ }
@@ -0,0 +1,140 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ input,
6
+ output,
7
+ } from '@angular/core';
8
+ import type {
9
+ CellValue,
10
+ ColumnDefinition,
11
+ ColumnFilterModel,
12
+ DragState,
13
+ VisibleColumnInfo,
14
+ } from '@gp-grid/core';
15
+ import { FilterPopupComponent } from './filter-popup.component';
16
+
17
+ export interface ActiveFilterPopup {
18
+ colIndex: number;
19
+ column: ColumnDefinition;
20
+ distinctValues: CellValue[];
21
+ currentFilter?: ColumnFilterModel;
22
+ anchorEl: HTMLElement | null;
23
+ }
24
+
25
+ const TEMPLATE = `
26
+ @if (filterPopup(); as fp) {
27
+ <gp-grid-filter-popup
28
+ [column]="fp.column"
29
+ [colIndex]="fp.colIndex"
30
+ [anchorEl]="fp.anchorEl!"
31
+ [distinctValues]="fp.distinctValues"
32
+ [currentFilter]="fp.currentFilter"
33
+ (apply)="filterApply.emit($event)"
34
+ (close)="filterClose.emit()"
35
+ />
36
+ }
37
+ @if (isResizing()) {
38
+ <div class="gp-grid-column-resize-line" [style.left.px]="resizeLineLeft()"></div>
39
+ }
40
+ @if (isLoading()) {
41
+ <div
42
+ style="position: absolute; left: 0; right: 0; bottom: 0; z-index: 50; pointer-events: none;"
43
+ [style.top.px]="headerHeight()">
44
+ <div class="gp-grid-loading-overlay"></div>
45
+ <div class="gp-grid-loading">
46
+ <div class="gp-grid-loading-spinner"></div>
47
+ </div>
48
+ </div>
49
+ }
50
+ @if (errorMessage(); as msg) {
51
+ <div class="gp-grid-error">Error: {{ msg }}</div>
52
+ }
53
+ @if (columnMove(); as cm) {
54
+ <div
55
+ class="gp-grid-column-move-ghost"
56
+ [style.left.px]="cm.currentX - cm.ghostWidth / 2"
57
+ [style.top.px]="cm.currentY - cm.ghostHeight / 2"
58
+ [style.width.px]="cm.ghostWidth"
59
+ [style.height.px]="cm.ghostHeight">
60
+ {{ columnMoveGhostText() }}
61
+ </div>
62
+ @if (columnMoveDropLeft() !== null) {
63
+ <div
64
+ class="gp-grid-column-drop-indicator"
65
+ [style.left.px]="columnMoveDropLeft()"
66
+ [style.height.px]="headerHeight()"></div>
67
+ }
68
+ }
69
+ @if (rowDragGhost(); as rd) {
70
+ <div
71
+ class="gp-grid-row-drag-ghost"
72
+ [style.left.px]="rd.currentX + 12"
73
+ [style.top.px]="rd.currentY - rowHeight() / 2"
74
+ [style.width.px]="rowDragGhostWidth()"
75
+ [style.height.px]="rowHeight()"></div>
76
+ }
77
+ `;
78
+
79
+ @Component({
80
+ selector: 'gp-grid-overlays',
81
+ standalone: true,
82
+ imports: [FilterPopupComponent],
83
+ changeDetection: ChangeDetectionStrategy.OnPush,
84
+ template: TEMPLATE,
85
+ })
86
+ export class GridOverlaysComponent {
87
+ filterPopup = input<ActiveFilterPopup | null>(null);
88
+ isLoading = input<boolean>(false);
89
+ errorMessage = input<string | null>(null);
90
+ headerHeight = input.required<number>();
91
+ rowHeight = input.required<number>();
92
+ dragState = input.required<DragState>();
93
+ visibleColumnWithIndices = input.required<VisibleColumnInfo[]>();
94
+ columnPositions = input.required<number[]>();
95
+ scrollLeft = input.required<number>();
96
+ effectiveColumns = input.required<ColumnDefinition[]>();
97
+ totalWidth = input.required<number>();
98
+
99
+ filterApply = output<{ colId: string; filter: ColumnFilterModel | null }>();
100
+ filterClose = output<void>();
101
+
102
+ protected isResizing = computed(() => this.dragState().dragType === 'column-resize');
103
+
104
+ protected resizeLineLeft = computed<number>(() => {
105
+ const cr = this.dragState().columnResize;
106
+ if (cr === null) return 0;
107
+ const visibleIndex = this.visibleColumnWithIndices().findIndex(
108
+ v => v.originalIndex === cr.colIndex
109
+ );
110
+ if (visibleIndex === -1) return 0;
111
+ const positions = this.columnPositions();
112
+ return (positions[visibleIndex] ?? 0) + cr.currentWidth - this.scrollLeft();
113
+ });
114
+
115
+ protected columnMove = computed(() => {
116
+ if (this.dragState().dragType !== 'column-move') return null;
117
+ return this.dragState().columnMove;
118
+ });
119
+
120
+ protected rowDragGhost = computed(() => {
121
+ if (this.dragState().dragType !== 'row-drag') return null;
122
+ return this.dragState().rowDrag;
123
+ });
124
+
125
+ protected columnMoveGhostText = computed<string>(() => {
126
+ const cm = this.columnMove();
127
+ if (cm === null) return '';
128
+ const column = this.effectiveColumns()[cm.sourceColIndex];
129
+ return column?.headerName ?? column?.field ?? '';
130
+ });
131
+
132
+ protected columnMoveDropLeft = computed<number | null>(() => {
133
+ const cm = this.columnMove();
134
+ if (cm === null || cm.dropTargetIndex === null) return null;
135
+ const positions = this.columnPositions();
136
+ return (positions[cm.dropTargetIndex] ?? 0) - this.scrollLeft();
137
+ });
138
+
139
+ protected rowDragGhostWidth = computed<number>(() => Math.min(300, this.totalWidth()));
140
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./grid-header.component";
2
+ export * from "./grid-body.component";
3
+ export * from "./filter-popup.component";
4
+ export * from "./grid-overlays.component";
@@ -0,0 +1,82 @@
1
+ import { createMutableClientDataSource } from "@gp-grid/core";
2
+ import type {
3
+ RowId,
4
+ CellValue,
5
+ MutableDataSource,
6
+ ParallelSortOptions,
7
+ } from "@gp-grid/core";
8
+
9
+ export interface CreateGridDataOptions<TData> {
10
+ /** Function to extract a unique ID from each row. Required. */
11
+ getRowId: (row: TData) => RowId;
12
+ /** Debounce time for batching transactions in ms. Default 50. */
13
+ debounceMs?: number;
14
+ /** Use Web Worker for sorting large datasets (default: true) */
15
+ useWorker?: boolean;
16
+ /** Options for parallel sorting (only used when useWorker is true) */
17
+ parallelSort?: ParallelSortOptions | false;
18
+ }
19
+
20
+ export interface GridDataApi<TData> {
21
+ /** The data source to pass to <gp-grid [dataSource]="dataSource" />. */
22
+ dataSource: MutableDataSource<TData>;
23
+ /** Update a single row by ID with partial data. */
24
+ updateRow: (id: RowId, data: Partial<TData>) => void;
25
+ /** Add rows to the data source. */
26
+ addRows: (rows: TData[]) => void;
27
+ /** Remove rows by ID. */
28
+ removeRows: (ids: RowId[]) => void;
29
+ /** Update a single cell value. */
30
+ updateCell: (id: RowId, field: string, value: CellValue) => void;
31
+ /** Clear all data from the data source. */
32
+ clear: () => void;
33
+ /** Get a row by its ID. */
34
+ getRowById: (id: RowId) => TData | undefined;
35
+ /** Get the current total row count. */
36
+ getTotalRowCount: () => number;
37
+ /** Force immediate processing of queued transactions. */
38
+ flushTransactions: () => Promise<void>;
39
+ }
40
+
41
+ /**
42
+ * Angular helper for efficient grid data mutations.
43
+ *
44
+ * Wraps `createMutableClientDataSource` to provide a simple API for
45
+ * updating grid data without triggering full pipeline rebuilds.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * private readonly grid = createGridData(initialRows, {
50
+ * getRowId: (row) => row.id,
51
+ * });
52
+ *
53
+ * // Template:
54
+ * // <gp-grid [dataSource]="grid.dataSource" [columns]="columns" />
55
+ *
56
+ * // Update a row imperatively:
57
+ * this.grid.updateRow(42, { name: 'New name' });
58
+ * ```
59
+ */
60
+ export const createGridData = <TData = unknown>(
61
+ initialData: TData[],
62
+ options: CreateGridDataOptions<TData>,
63
+ ): GridDataApi<TData> => {
64
+ const ds = createMutableClientDataSource<TData>(initialData, {
65
+ getRowId: options.getRowId,
66
+ debounceMs: options.debounceMs,
67
+ useWorker: options.useWorker,
68
+ parallelSort: options.parallelSort,
69
+ });
70
+
71
+ return {
72
+ dataSource: ds,
73
+ updateRow: ds.updateRow,
74
+ addRows: ds.addRows,
75
+ removeRows: ds.removeRows,
76
+ updateCell: ds.updateCell,
77
+ clear: ds.clear,
78
+ getRowById: ds.getRowById,
79
+ getTotalRowCount: ds.getTotalRowCount,
80
+ flushTransactions: ds.flushTransactions,
81
+ };
82
+ };
@@ -0,0 +1,150 @@
1
+ import {
2
+ AutoScrollDriver,
3
+ DataSourceOwner,
4
+ GridCore,
5
+ InputEventAdapter,
6
+ PendingRowDragController,
7
+ applyBatchInstructions,
8
+ scrollCellIntoView,
9
+ } from '@gp-grid/core';
10
+ import type {
11
+ ColumnDefinition,
12
+ DataSource,
13
+ HighlightingOptions,
14
+ } from '@gp-grid/core';
15
+ import type { GpGridViewModel } from './gp-grid-view-model';
16
+
17
+ export interface GpGridBindingsDeps {
18
+ vm: GpGridViewModel;
19
+ isBrowser: boolean;
20
+ getContainer: () => HTMLElement | null;
21
+ getBody: () => HTMLElement | null;
22
+ getRowHeight: () => number;
23
+ getHeaderHeight: () => number;
24
+ }
25
+
26
+ /**
27
+ * Owns the core grid instance plus every framework-agnostic adapter the
28
+ * Angular component drives (auto-scroll, pending row-drag, input events,
29
+ * data source ownership). The component becomes a thin shell that holds
30
+ * lifecycle + Angular template bindings and delegates state work here.
31
+ */
32
+ export class GpGridBindings<TData = unknown> {
33
+ readonly dataSourceOwner = new DataSourceOwner<TData>();
34
+ readonly autoScroll: AutoScrollDriver;
35
+ readonly pendingRowDrag: PendingRowDragController;
36
+ readonly input: InputEventAdapter<TData>;
37
+
38
+ coreRef: GridCore<TData> | null = null;
39
+ private unsubscribe: (() => void) | null = null;
40
+ private resizeObserver: ResizeObserver | null = null;
41
+
42
+ constructor(private readonly deps: GpGridBindingsDeps) {
43
+ this.autoScroll = new AutoScrollDriver(
44
+ () => this.deps.getBody(),
45
+ (event) => this.input.dragMove(event),
46
+ );
47
+ this.pendingRowDrag = new PendingRowDragController({
48
+ getCore: () => this.coreRef,
49
+ getContainer: this.deps.getContainer,
50
+ isBrowser: this.deps.isBrowser,
51
+ onDragConfirmed: (state) => this.deps.vm.dragState.set(state),
52
+ });
53
+ this.input = new InputEventAdapter<TData>({
54
+ getCore: () => this.coreRef,
55
+ getBodyEl: this.deps.getBody,
56
+ autoScroll: this.autoScroll,
57
+ pendingRowDrag: this.pendingRowDrag,
58
+ onDragStateChange: (state) => this.deps.vm.dragState.set(state),
59
+ });
60
+ }
61
+
62
+ attach(core: GridCore<TData>): void {
63
+ this.coreRef = core;
64
+ this.unsubscribe = core.onBatchInstruction((instructions) => {
65
+ const vm = this.deps.vm;
66
+ const maps = applyBatchInstructions(
67
+ instructions,
68
+ vm.slots(),
69
+ vm.headerState(),
70
+ vm.batchSetters,
71
+ );
72
+ vm.slots.set(new Map(maps.slots));
73
+ vm.headerState.set(new Map(maps.headers));
74
+ });
75
+
76
+ core.initialize();
77
+ core.input.updateDeps({
78
+ getHeaderHeight: this.deps.getHeaderHeight,
79
+ getRowHeight: this.deps.getRowHeight,
80
+ getColumnPositions: () => this.deps.vm.columnPositions(),
81
+ getColumnCount: () => this.deps.vm.visibleColumnWithIndices().length,
82
+ getOriginalColumnIndex: (visibleIndex) =>
83
+ this.deps.vm.visibleColumnWithIndices()[visibleIndex]?.originalIndex ?? visibleIndex,
84
+ });
85
+ }
86
+
87
+ observeViewport(container: HTMLElement, bodyEl: HTMLElement): void {
88
+ this.deps.vm.viewportWidth.set(container.clientWidth);
89
+ this.resizeObserver = new ResizeObserver((entries) => {
90
+ const entry = entries[0];
91
+ if (entry) this.deps.vm.viewportWidth.set(entry.contentRect.width);
92
+ });
93
+ this.resizeObserver.observe(container);
94
+ this.coreRef?.setViewport(0, 0, container.clientWidth, bodyEl.clientHeight);
95
+ }
96
+
97
+ destroy(): void {
98
+ this.autoScroll.stop();
99
+ this.pendingRowDrag.cancel();
100
+ this.pendingRowDrag.releaseLocks();
101
+ this.unsubscribe?.();
102
+ this.resizeObserver?.disconnect();
103
+ this.coreRef?.destroy();
104
+ this.dataSourceOwner.destroy();
105
+ this.coreRef = null;
106
+ }
107
+
108
+ syncHighlighting(opts: HighlightingOptions | null): void {
109
+ const core = this.coreRef;
110
+ if (core?.highlight && opts) {
111
+ core.highlight.updateOptions(opts as HighlightingOptions<TData>);
112
+ }
113
+ }
114
+
115
+ syncColumns(cols: ColumnDefinition[]): void {
116
+ const core = this.coreRef;
117
+ if (core === null) return;
118
+ if (this.dataSourceOwner.syncColumns(cols)) core.setColumns(cols);
119
+ }
120
+
121
+ syncRows(rows: TData[], dataSource: DataSource<TData> | null): void {
122
+ const core = this.coreRef;
123
+ if (core === null) return;
124
+ const newDs = this.dataSourceOwner.syncRows(rows, dataSource);
125
+ if (newDs !== null) core.setDataSource(newDs);
126
+ }
127
+
128
+ applyPendingScroll(): void {
129
+ const top = this.deps.vm.pendingScrollTop();
130
+ const body = this.deps.getBody();
131
+ if (top !== null && body) {
132
+ body.scrollTop = top;
133
+ this.deps.vm.pendingScrollTop.set(null);
134
+ }
135
+ }
136
+
137
+ scrollToRow(row: number): void {
138
+ const core = this.coreRef;
139
+ const body = this.deps.getBody();
140
+ if (core === null || body === null) return;
141
+ scrollCellIntoView(
142
+ core,
143
+ body,
144
+ row,
145
+ this.deps.getRowHeight(),
146
+ this.deps.vm.slots(),
147
+ this.deps.vm.rowsWrapperOffset(),
148
+ );
149
+ }
150
+ }