argent-grid 0.1.0
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.
- package/.github/workflows/pages.yml +68 -0
- package/AGENTS.md +179 -0
- package/README.md +222 -0
- package/demo-app/README.md +70 -0
- package/demo-app/angular.json +78 -0
- package/demo-app/e2e/benchmark.spec.ts +53 -0
- package/demo-app/e2e/demo-page.spec.ts +77 -0
- package/demo-app/e2e/grid-features.spec.ts +269 -0
- package/demo-app/package-lock.json +14023 -0
- package/demo-app/package.json +36 -0
- package/demo-app/playwright-test-menu.js +19 -0
- package/demo-app/playwright.config.ts +23 -0
- package/demo-app/src/app/app.component.ts +10 -0
- package/demo-app/src/app/app.config.ts +13 -0
- package/demo-app/src/app/app.routes.ts +7 -0
- package/demo-app/src/app/demo-page/demo-page.component.css +313 -0
- package/demo-app/src/app/demo-page/demo-page.component.html +124 -0
- package/demo-app/src/app/demo-page/demo-page.component.ts +366 -0
- package/demo-app/src/index.html +19 -0
- package/demo-app/src/main.ts +6 -0
- package/demo-app/tsconfig.json +31 -0
- package/ng-package.json +8 -0
- package/package.json +60 -0
- package/plan.md +131 -0
- package/setup-vitest.ts +18 -0
- package/src/lib/argent-grid.module.ts +21 -0
- package/src/lib/components/argent-grid.component.css +483 -0
- package/src/lib/components/argent-grid.component.html +320 -0
- package/src/lib/components/argent-grid.component.spec.ts +189 -0
- package/src/lib/components/argent-grid.component.ts +1188 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +92 -0
- package/src/lib/rendering/canvas-renderer.ts +962 -0
- package/src/lib/rendering/render/blit.spec.ts +453 -0
- package/src/lib/rendering/render/blit.ts +393 -0
- package/src/lib/rendering/render/cells.ts +369 -0
- package/src/lib/rendering/render/index.ts +105 -0
- package/src/lib/rendering/render/lines.ts +363 -0
- package/src/lib/rendering/render/theme.spec.ts +282 -0
- package/src/lib/rendering/render/theme.ts +201 -0
- package/src/lib/rendering/render/types.ts +279 -0
- package/src/lib/rendering/render/walk.spec.ts +360 -0
- package/src/lib/rendering/render/walk.ts +360 -0
- package/src/lib/rendering/utils/damage-tracker.spec.ts +444 -0
- package/src/lib/rendering/utils/damage-tracker.ts +423 -0
- package/src/lib/rendering/utils/index.ts +7 -0
- package/src/lib/services/grid.service.spec.ts +1039 -0
- package/src/lib/services/grid.service.ts +1284 -0
- package/src/lib/types/ag-grid-types.ts +970 -0
- package/src/public-api.ts +22 -0
- package/tsconfig.json +32 -0
- package/tsconfig.lib.json +11 -0
- package/tsconfig.spec.json +8 -0
- package/vitest.config.ts +55 -0
|
@@ -0,0 +1,962 @@
|
|
|
1
|
+
import { GridApi, IRowNode, Column, ColDef, SparklineOptions } from '../types/ag-grid-types';
|
|
2
|
+
|
|
3
|
+
// Import new rendering modules from the index
|
|
4
|
+
import {
|
|
5
|
+
// Types
|
|
6
|
+
GridTheme,
|
|
7
|
+
ColumnPrepResult,
|
|
8
|
+
// Theme
|
|
9
|
+
DEFAULT_THEME,
|
|
10
|
+
getFontFromTheme,
|
|
11
|
+
mergeTheme,
|
|
12
|
+
// Walker
|
|
13
|
+
walkColumns,
|
|
14
|
+
walkRows,
|
|
15
|
+
walkCells,
|
|
16
|
+
getVisibleRowRange,
|
|
17
|
+
getPinnedWidths,
|
|
18
|
+
getColumnAtX,
|
|
19
|
+
getRowAtY,
|
|
20
|
+
// Blitting
|
|
21
|
+
BlitState,
|
|
22
|
+
calculateBlit,
|
|
23
|
+
shouldBlit,
|
|
24
|
+
// Cells
|
|
25
|
+
truncateText,
|
|
26
|
+
prepColumn,
|
|
27
|
+
getFormattedValue,
|
|
28
|
+
getValueByPath,
|
|
29
|
+
// Lines
|
|
30
|
+
drawRowLines,
|
|
31
|
+
drawColumnLines,
|
|
32
|
+
drawRangeSelectionBorder,
|
|
33
|
+
getColumnBorderPositions,
|
|
34
|
+
} from './render';
|
|
35
|
+
import { DamageTracker } from './utils/damage-tracker';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* CanvasRenderer - High-performance canvas rendering engine for ArgentGrid
|
|
39
|
+
*
|
|
40
|
+
* Renders the data viewport using HTML5 Canvas for optimal performance
|
|
41
|
+
* with large datasets (100,000+ rows at 60fps)
|
|
42
|
+
*
|
|
43
|
+
* Features:
|
|
44
|
+
* - Virtual scrolling (only renders visible rows)
|
|
45
|
+
* - requestAnimationFrame batching
|
|
46
|
+
* - Device pixel ratio support
|
|
47
|
+
* - Row buffering for smooth scrolling
|
|
48
|
+
* - Blitting optimization for frame-to-frame efficiency
|
|
49
|
+
* - Damage tracking for partial redraws
|
|
50
|
+
*/
|
|
51
|
+
export class CanvasRenderer<TData = any> {
|
|
52
|
+
private canvas: HTMLCanvasElement;
|
|
53
|
+
private ctx: CanvasRenderingContext2D;
|
|
54
|
+
private gridApi: GridApi<TData>;
|
|
55
|
+
private rowHeight: number;
|
|
56
|
+
private scrollTop = 0;
|
|
57
|
+
private scrollLeft = 0;
|
|
58
|
+
|
|
59
|
+
get currentScrollTop(): number { return this.scrollTop; }
|
|
60
|
+
get currentScrollLeft(): number { return this.scrollLeft; }
|
|
61
|
+
|
|
62
|
+
private animationFrameId: number | null = null;
|
|
63
|
+
private renderPending = false;
|
|
64
|
+
private rowBuffer = 5;
|
|
65
|
+
private totalRowCount = 0;
|
|
66
|
+
private viewportHeight = 0;
|
|
67
|
+
private viewportWidth = 0;
|
|
68
|
+
|
|
69
|
+
// Theme system
|
|
70
|
+
private theme: GridTheme;
|
|
71
|
+
|
|
72
|
+
// Performance tracking
|
|
73
|
+
private lastRenderDuration = 0;
|
|
74
|
+
get lastFrameTime(): number { return this.lastRenderDuration; }
|
|
75
|
+
|
|
76
|
+
// Damage tracking
|
|
77
|
+
private damageTracker = new DamageTracker();
|
|
78
|
+
|
|
79
|
+
// Blitting state
|
|
80
|
+
private blitState = new BlitState();
|
|
81
|
+
|
|
82
|
+
// Column prep results cache
|
|
83
|
+
private columnPreps: Map<string, ColumnPrepResult<TData>> = new Map();
|
|
84
|
+
|
|
85
|
+
// Event listener references for cleanup
|
|
86
|
+
private scrollListener?: (e: Event) => void;
|
|
87
|
+
private resizeListener?: () => void;
|
|
88
|
+
private mousedownListener?: (e: MouseEvent) => void;
|
|
89
|
+
private mousemoveListener?: (e: MouseEvent) => void;
|
|
90
|
+
private clickListener?: (e: MouseEvent) => void;
|
|
91
|
+
private dblclickListener?: (e: MouseEvent) => void;
|
|
92
|
+
private mouseupListener?: (e: MouseEvent) => void;
|
|
93
|
+
|
|
94
|
+
// Callbacks
|
|
95
|
+
onCellDoubleClick?: (rowIndex: number, colId: string) => void;
|
|
96
|
+
onRowClick?: (rowIndex: number, event: MouseEvent) => void;
|
|
97
|
+
onMouseDown?: (event: MouseEvent, rowIndex: number, colId: string | null) => void;
|
|
98
|
+
onMouseMove?: (event: MouseEvent, rowIndex: number, colId: string | null) => void;
|
|
99
|
+
onMouseUp?: (event: MouseEvent, rowIndex: number, colId: string | null) => void;
|
|
100
|
+
|
|
101
|
+
constructor(
|
|
102
|
+
canvas: HTMLCanvasElement,
|
|
103
|
+
gridApi: GridApi<TData>,
|
|
104
|
+
rowHeight: number = 32,
|
|
105
|
+
theme?: Partial<GridTheme>
|
|
106
|
+
) {
|
|
107
|
+
this.canvas = canvas;
|
|
108
|
+
this.ctx = canvas.getContext('2d')!;
|
|
109
|
+
this.gridApi = gridApi;
|
|
110
|
+
this.rowHeight = rowHeight;
|
|
111
|
+
this.theme = mergeTheme(DEFAULT_THEME, { rowHeight }, theme || {});
|
|
112
|
+
|
|
113
|
+
this.setupEventListeners();
|
|
114
|
+
this.resize();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Update the theme
|
|
119
|
+
*/
|
|
120
|
+
setTheme(theme: Partial<GridTheme>): void {
|
|
121
|
+
this.theme = mergeTheme(DEFAULT_THEME, { rowHeight: this.rowHeight }, theme);
|
|
122
|
+
this.damageTracker.markAllDirty();
|
|
123
|
+
this.scheduleRender();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get current theme
|
|
128
|
+
*/
|
|
129
|
+
getTheme(): GridTheme {
|
|
130
|
+
return this.theme;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private setupEventListeners(): void {
|
|
134
|
+
const container = this.canvas.parentElement;
|
|
135
|
+
if (container) {
|
|
136
|
+
this.scrollListener = this.handleScroll.bind(this);
|
|
137
|
+
container.addEventListener('scroll', this.scrollListener, { passive: true });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.mousedownListener = this.handleMouseDown.bind(this);
|
|
141
|
+
this.mousemoveListener = this.handleMouseMove.bind(this);
|
|
142
|
+
this.clickListener = this.handleClick.bind(this);
|
|
143
|
+
this.dblclickListener = this.handleDoubleClick.bind(this);
|
|
144
|
+
this.mouseupListener = this.handleMouseUp.bind(this);
|
|
145
|
+
|
|
146
|
+
this.canvas.addEventListener('mousedown', this.mousedownListener);
|
|
147
|
+
this.canvas.addEventListener('mousemove', this.mousemoveListener);
|
|
148
|
+
this.canvas.addEventListener('click', this.clickListener);
|
|
149
|
+
this.canvas.addEventListener('dblclick', this.dblclickListener);
|
|
150
|
+
this.canvas.addEventListener('mouseup', this.mouseupListener);
|
|
151
|
+
|
|
152
|
+
let resizeTimeout: number;
|
|
153
|
+
this.resizeListener = () => {
|
|
154
|
+
clearTimeout(resizeTimeout);
|
|
155
|
+
resizeTimeout = setTimeout(() => this.resize(), 150) as any;
|
|
156
|
+
};
|
|
157
|
+
window.addEventListener('resize', this.resizeListener);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private handleScroll(): void {
|
|
161
|
+
const container = this.canvas.parentElement;
|
|
162
|
+
if (!container) return;
|
|
163
|
+
|
|
164
|
+
const oldScrollTop = this.scrollTop;
|
|
165
|
+
const oldScrollLeft = this.scrollLeft;
|
|
166
|
+
|
|
167
|
+
this.scrollTop = container.scrollTop;
|
|
168
|
+
this.scrollLeft = container.scrollLeft;
|
|
169
|
+
|
|
170
|
+
// Update blit state
|
|
171
|
+
const lastScroll = this.blitState.updateScroll(this.scrollLeft, this.scrollTop);
|
|
172
|
+
|
|
173
|
+
// Check if we should blit
|
|
174
|
+
const { left, right } = getPinnedWidths(this.getVisibleColumns());
|
|
175
|
+
const blitResult = calculateBlit(
|
|
176
|
+
{ x: this.scrollLeft, y: this.scrollTop },
|
|
177
|
+
lastScroll,
|
|
178
|
+
{ width: this.viewportWidth, height: this.viewportHeight },
|
|
179
|
+
{ left, right }
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (blitResult.canBlit && this.blitState.hasLastFrame()) {
|
|
183
|
+
// Blitting is possible - the render will copy from last frame
|
|
184
|
+
this.damageTracker.markAllDirty(); // For now, still do full redraw but with blit
|
|
185
|
+
} else {
|
|
186
|
+
// Full redraw needed
|
|
187
|
+
this.damageTracker.markAllDirty();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.scheduleRender();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setTotalRowCount(count: number): void {
|
|
194
|
+
this.totalRowCount = count;
|
|
195
|
+
this.damageTracker.markAllDirty();
|
|
196
|
+
this.updateCanvasSize();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
setViewportDimensions(width: number, height: number): void {
|
|
200
|
+
this.viewportWidth = width;
|
|
201
|
+
this.viewportHeight = height;
|
|
202
|
+
this.damageTracker.markAllDirty();
|
|
203
|
+
this.updateCanvasSize();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private updateCanvasSize(): void {
|
|
207
|
+
const dpr = window.devicePixelRatio || 1;
|
|
208
|
+
|
|
209
|
+
const width = this.viewportWidth || this.canvas.clientWidth;
|
|
210
|
+
const height = this.viewportHeight || this.canvas.clientHeight || 600;
|
|
211
|
+
|
|
212
|
+
this.canvas.width = width * dpr;
|
|
213
|
+
this.canvas.height = height * dpr;
|
|
214
|
+
this.canvas.style.width = `${width}px`;
|
|
215
|
+
this.canvas.style.height = `${height}px`;
|
|
216
|
+
|
|
217
|
+
if (typeof this.ctx.setTransform === 'function') {
|
|
218
|
+
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
219
|
+
} else {
|
|
220
|
+
this.ctx.scale(dpr, dpr);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Reset blit state on resize
|
|
224
|
+
this.blitState.reset();
|
|
225
|
+
this.scheduleRender();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
resize(): void {
|
|
229
|
+
const container = this.canvas.parentElement;
|
|
230
|
+
if (!container) return;
|
|
231
|
+
|
|
232
|
+
const rect = container.getBoundingClientRect();
|
|
233
|
+
this.viewportWidth = rect.width;
|
|
234
|
+
this.viewportHeight = rect.height;
|
|
235
|
+
|
|
236
|
+
this.updateCanvasSize();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
render(): void {
|
|
240
|
+
this.damageTracker.markAllDirty();
|
|
241
|
+
this.scheduleRender();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private scheduleRender(): void {
|
|
245
|
+
if (this.renderPending || this.animationFrameId !== null) return;
|
|
246
|
+
|
|
247
|
+
this.renderPending = true;
|
|
248
|
+
this.animationFrameId = requestAnimationFrame(() => {
|
|
249
|
+
this.doRender();
|
|
250
|
+
this.renderPending = false;
|
|
251
|
+
this.animationFrameId = null;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getAllColumns(): Column[] {
|
|
256
|
+
return this.getVisibleColumns();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private getVisibleColumns(): Column[] {
|
|
260
|
+
return this.gridApi.getAllColumns().filter(col => col.visible);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private prepareColumns(): void {
|
|
264
|
+
const columns = this.getVisibleColumns();
|
|
265
|
+
this.columnPreps.clear();
|
|
266
|
+
|
|
267
|
+
for (const column of columns) {
|
|
268
|
+
const colDef = this.getColumnDef(column);
|
|
269
|
+
this.columnPreps.set(column.colId, prepColumn(this.ctx, column, colDef, this.theme));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private doRender(): void {
|
|
274
|
+
const startTime = performance.now();
|
|
275
|
+
const width = this.viewportWidth || this.canvas.clientWidth;
|
|
276
|
+
const height = this.viewportHeight || this.canvas.clientHeight;
|
|
277
|
+
|
|
278
|
+
// Clear canvas
|
|
279
|
+
this.ctx.clearRect(0, 0, width, height);
|
|
280
|
+
|
|
281
|
+
// Get visible columns
|
|
282
|
+
const allVisibleColumns = this.getVisibleColumns();
|
|
283
|
+
const { left: leftWidth, right: rightWidth } = getPinnedWidths(allVisibleColumns);
|
|
284
|
+
|
|
285
|
+
// Calculate visible row range
|
|
286
|
+
const totalRows = this.totalRowCount || this.gridApi.getDisplayedRowCount();
|
|
287
|
+
const { startRow, endRow } = getVisibleRowRange(
|
|
288
|
+
this.scrollTop,
|
|
289
|
+
height,
|
|
290
|
+
this.rowHeight,
|
|
291
|
+
totalRows,
|
|
292
|
+
this.rowBuffer,
|
|
293
|
+
this.gridApi
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Prepare columns (sets font, caches colDef)
|
|
297
|
+
this.prepareColumns();
|
|
298
|
+
|
|
299
|
+
// Set common context properties
|
|
300
|
+
this.ctx.font = getFontFromTheme(this.theme);
|
|
301
|
+
this.ctx.textBaseline = 'middle';
|
|
302
|
+
|
|
303
|
+
// Render visible rows using walker
|
|
304
|
+
walkRows(startRow, endRow, this.scrollTop, this.rowHeight,
|
|
305
|
+
(rowIndex) => this.gridApi.getDisplayedRowAtIndex(rowIndex),
|
|
306
|
+
(rowIndex, y, rowHeight, rowNode) => {
|
|
307
|
+
if (!rowNode) return;
|
|
308
|
+
this.renderRow(rowIndex, y, rowNode, allVisibleColumns, width, leftWidth, rightWidth);
|
|
309
|
+
},
|
|
310
|
+
this.gridApi
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Draw grid lines
|
|
314
|
+
this.drawGridLines(allVisibleColumns, startRow, endRow, width, height, leftWidth, rightWidth);
|
|
315
|
+
|
|
316
|
+
// Draw range selections
|
|
317
|
+
this.drawRangeSelections(allVisibleColumns, leftWidth, rightWidth, width);
|
|
318
|
+
|
|
319
|
+
// Store current frame for blitting
|
|
320
|
+
this.blitState.setLastCanvas(this.canvas);
|
|
321
|
+
|
|
322
|
+
// Clear damage
|
|
323
|
+
this.damageTracker.clear();
|
|
324
|
+
|
|
325
|
+
this.lastRenderDuration = performance.now() - startTime;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private drawRangeSelections(
|
|
329
|
+
allVisibleColumns: Column[],
|
|
330
|
+
leftPinnedWidth: number,
|
|
331
|
+
rightPinnedWidth: number,
|
|
332
|
+
viewportWidth: number
|
|
333
|
+
): void {
|
|
334
|
+
const ranges = this.gridApi.getCellRanges();
|
|
335
|
+
if (!ranges) return;
|
|
336
|
+
|
|
337
|
+
for (const range of ranges) {
|
|
338
|
+
// Calculate Y boundaries
|
|
339
|
+
const startY = range.startRow * this.rowHeight - this.scrollTop;
|
|
340
|
+
const endY = (range.endRow + 1) * this.rowHeight - this.scrollTop;
|
|
341
|
+
|
|
342
|
+
// Calculate X boundaries
|
|
343
|
+
const startColIdx = allVisibleColumns.findIndex(c => c.colId === range.startColumn);
|
|
344
|
+
const endColIdx = allVisibleColumns.findIndex(c => c.colId === range.endColumn);
|
|
345
|
+
|
|
346
|
+
if (startColIdx === -1 || endColIdx === -1) continue;
|
|
347
|
+
|
|
348
|
+
let minX = Infinity;
|
|
349
|
+
let maxX = -Infinity;
|
|
350
|
+
|
|
351
|
+
// Calculate the total bounding box of all columns in the range
|
|
352
|
+
range.columns.forEach(col => {
|
|
353
|
+
const xPos = this.getColumnX(col, allVisibleColumns, leftPinnedWidth, rightPinnedWidth, viewportWidth);
|
|
354
|
+
minX = Math.min(minX, xPos);
|
|
355
|
+
maxX = Math.max(maxX, xPos + col.width);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if (minX === Infinity) continue;
|
|
359
|
+
|
|
360
|
+
drawRangeSelectionBorder(this.ctx, {
|
|
361
|
+
x: minX,
|
|
362
|
+
y: startY,
|
|
363
|
+
width: maxX - minX,
|
|
364
|
+
height: endY - startY
|
|
365
|
+
}, {
|
|
366
|
+
color: this.theme.bgSelection,
|
|
367
|
+
fillColor: this.theme.bgSelection + '40', // 25% opacity
|
|
368
|
+
lineWidth: 2
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private getColumnX(
|
|
374
|
+
targetCol: Column,
|
|
375
|
+
allVisibleColumns: Column[],
|
|
376
|
+
leftPinnedWidth: number,
|
|
377
|
+
rightPinnedWidth: number,
|
|
378
|
+
viewportWidth: number
|
|
379
|
+
): number {
|
|
380
|
+
if (targetCol.pinned === 'left') {
|
|
381
|
+
let x = 0;
|
|
382
|
+
for (const col of allVisibleColumns) {
|
|
383
|
+
if (col.colId === targetCol.colId) return x;
|
|
384
|
+
if (col.pinned === 'left') x += col.width;
|
|
385
|
+
}
|
|
386
|
+
} else if (targetCol.pinned === 'right') {
|
|
387
|
+
let x = viewportWidth - rightPinnedWidth;
|
|
388
|
+
for (const col of allVisibleColumns) {
|
|
389
|
+
if (col.pinned === 'right') {
|
|
390
|
+
if (col.colId === targetCol.colId) return x;
|
|
391
|
+
x += col.width;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
let x = leftPinnedWidth - this.scrollLeft;
|
|
396
|
+
for (const col of allVisibleColumns) {
|
|
397
|
+
if (!col.pinned) {
|
|
398
|
+
if (col.colId === targetCol.colId) return x;
|
|
399
|
+
x += col.width;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private renderRow(
|
|
407
|
+
rowIndex: number,
|
|
408
|
+
y: number,
|
|
409
|
+
rowNode: IRowNode<TData>,
|
|
410
|
+
allVisibleColumns: Column[],
|
|
411
|
+
viewportWidth: number,
|
|
412
|
+
leftWidth: number,
|
|
413
|
+
rightWidth: number
|
|
414
|
+
): void {
|
|
415
|
+
if (rowNode.detail) {
|
|
416
|
+
this.renderDetailRow(rowIndex, y, rowNode, viewportWidth);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const isEvenRow = rowIndex % 2 === 0;
|
|
421
|
+
|
|
422
|
+
// Draw row background
|
|
423
|
+
let bgColor = isEvenRow ? this.theme.bgCellEven : this.theme.bgCell;
|
|
424
|
+
if (rowNode.selected) {
|
|
425
|
+
bgColor = this.theme.bgSelection;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this.ctx.fillStyle = bgColor;
|
|
429
|
+
this.ctx.fillRect(0, Math.floor(y), viewportWidth, this.rowHeight);
|
|
430
|
+
|
|
431
|
+
// Render left pinned columns
|
|
432
|
+
const leftPinned = allVisibleColumns.filter(c => c.pinned === 'left');
|
|
433
|
+
this.renderColumns(leftPinned, 0, false, rowNode, y, viewportWidth, leftWidth, rightWidth, allVisibleColumns);
|
|
434
|
+
|
|
435
|
+
// Render center columns (with clipping)
|
|
436
|
+
const centerColumns = allVisibleColumns.filter(c => !c.pinned);
|
|
437
|
+
if (centerColumns.length > 0) {
|
|
438
|
+
this.ctx.save();
|
|
439
|
+
this.ctx.beginPath();
|
|
440
|
+
this.ctx.rect(
|
|
441
|
+
Math.floor(leftWidth),
|
|
442
|
+
Math.floor(y),
|
|
443
|
+
Math.floor(viewportWidth - leftWidth - rightWidth),
|
|
444
|
+
this.rowHeight
|
|
445
|
+
);
|
|
446
|
+
this.ctx.clip();
|
|
447
|
+
this.renderColumns(centerColumns, leftWidth, true, rowNode, y, viewportWidth, leftWidth, rightWidth, allVisibleColumns);
|
|
448
|
+
this.ctx.restore();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Render right pinned columns
|
|
452
|
+
const rightPinned = allVisibleColumns.filter(c => c.pinned === 'right');
|
|
453
|
+
this.renderColumns(rightPinned, viewportWidth - rightWidth, false, rowNode, y, viewportWidth, leftWidth, rightWidth, allVisibleColumns);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private renderDetailRow(
|
|
457
|
+
rowIndex: number,
|
|
458
|
+
y: number,
|
|
459
|
+
rowNode: IRowNode<TData>,
|
|
460
|
+
viewportWidth: number
|
|
461
|
+
): void {
|
|
462
|
+
const rowHeight = rowNode.rowHeight || 200;
|
|
463
|
+
|
|
464
|
+
// Draw detail background
|
|
465
|
+
this.ctx.fillStyle = '#f0f0f0';
|
|
466
|
+
this.ctx.fillRect(0, Math.floor(y), viewportWidth, rowHeight);
|
|
467
|
+
|
|
468
|
+
// Draw placeholder text
|
|
469
|
+
this.ctx.fillStyle = '#666';
|
|
470
|
+
this.ctx.font = `italic ${this.theme.fontSize}px ${this.theme.fontFamily}`;
|
|
471
|
+
this.ctx.fillText(
|
|
472
|
+
'Detail View Placeholder (Master/Detail support implemented)',
|
|
473
|
+
Math.floor(this.theme.cellPadding * 4),
|
|
474
|
+
Math.floor(y + rowHeight / 2)
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// Reset font
|
|
478
|
+
this.ctx.font = getFontFromTheme(this.theme);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private renderColumns(
|
|
482
|
+
columns: Column[],
|
|
483
|
+
startX: number,
|
|
484
|
+
isScrollable: boolean,
|
|
485
|
+
rowNode: IRowNode<TData>,
|
|
486
|
+
y: number,
|
|
487
|
+
viewportWidth: number,
|
|
488
|
+
leftWidth: number,
|
|
489
|
+
rightWidth: number,
|
|
490
|
+
allVisibleColumns: Column[]
|
|
491
|
+
): void {
|
|
492
|
+
let x = startX;
|
|
493
|
+
|
|
494
|
+
for (const col of columns) {
|
|
495
|
+
const cellX = isScrollable ? x - this.scrollLeft : x;
|
|
496
|
+
const cellWidth = col.width;
|
|
497
|
+
|
|
498
|
+
// Skip if outside viewport (for center columns)
|
|
499
|
+
if (isScrollable && (cellX + cellWidth < leftWidth || cellX > viewportWidth - rightWidth)) {
|
|
500
|
+
x += cellWidth;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
this.renderCell(col, cellX, y, cellWidth, rowNode, allVisibleColumns);
|
|
505
|
+
x += cellWidth;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private renderCell(
|
|
510
|
+
column: Column,
|
|
511
|
+
x: number,
|
|
512
|
+
y: number,
|
|
513
|
+
width: number,
|
|
514
|
+
rowNode: IRowNode<TData>,
|
|
515
|
+
allVisibleColumns: Column[]
|
|
516
|
+
): void {
|
|
517
|
+
const prep = this.columnPreps.get(column.colId);
|
|
518
|
+
if (!prep) return;
|
|
519
|
+
|
|
520
|
+
const cellValue = column.field ? getValueByPath(rowNode.data, column.field) : undefined;
|
|
521
|
+
// Check for sparkline
|
|
522
|
+
if (prep.colDef?.sparklineOptions) {
|
|
523
|
+
this.drawSparkline(cellValue, x, y, width, this.rowHeight, prep.colDef.sparklineOptions);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const formattedValue = getFormattedValue(
|
|
528
|
+
cellValue,
|
|
529
|
+
prep.colDef,
|
|
530
|
+
rowNode.data,
|
|
531
|
+
rowNode,
|
|
532
|
+
this.gridApi
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
if (!formattedValue) return;
|
|
536
|
+
|
|
537
|
+
this.ctx.fillStyle = this.theme.textCell;
|
|
538
|
+
|
|
539
|
+
let textX = x + this.theme.cellPadding;
|
|
540
|
+
|
|
541
|
+
// Handle group indentation
|
|
542
|
+
const isAutoGroupCol = column.colId === 'ag-Grid-AutoColumn';
|
|
543
|
+
const isFirstColIfNoAutoGroup = !allVisibleColumns.some(c => c.colId === 'ag-Grid-AutoColumn') && column === allVisibleColumns[0];
|
|
544
|
+
|
|
545
|
+
if ((isAutoGroupCol || isFirstColIfNoAutoGroup) && (rowNode.group || rowNode.master || rowNode.level > 0)) {
|
|
546
|
+
const indent = rowNode.level * this.theme.groupIndentWidth;
|
|
547
|
+
textX += indent;
|
|
548
|
+
|
|
549
|
+
// Draw expand/collapse indicator
|
|
550
|
+
if (rowNode.group || rowNode.master) {
|
|
551
|
+
this.drawGroupIndicator(textX, y, rowNode.expanded);
|
|
552
|
+
textX += this.theme.groupIndicatorSize + 3;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const truncatedText = truncateText(
|
|
557
|
+
this.ctx,
|
|
558
|
+
formattedValue,
|
|
559
|
+
width - (textX - x) - this.theme.cellPadding
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
if (truncatedText) {
|
|
563
|
+
this.ctx.fillText(truncatedText, Math.floor(textX), Math.floor(y + this.rowHeight / 2));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private drawSparkline(
|
|
568
|
+
data: any[],
|
|
569
|
+
x: number,
|
|
570
|
+
y: number,
|
|
571
|
+
width: number,
|
|
572
|
+
height: number,
|
|
573
|
+
options: SparklineOptions
|
|
574
|
+
): void {
|
|
575
|
+
if (!Array.isArray(data) || data.length === 0) return;
|
|
576
|
+
|
|
577
|
+
const padding = options.padding || { top: 4, bottom: 4, left: 4, right: 4 };
|
|
578
|
+
const drawX = x + (padding.left || 0);
|
|
579
|
+
const drawY = y + (padding.top || 0);
|
|
580
|
+
const drawWidth = width - (padding.left || 0) - (padding.right || 0);
|
|
581
|
+
const drawHeight = height - (padding.top || 0) - (padding.bottom || 0);
|
|
582
|
+
|
|
583
|
+
if (drawWidth <= 0 || drawHeight <= 0) return;
|
|
584
|
+
|
|
585
|
+
const min = Math.min(...data);
|
|
586
|
+
const max = Math.max(...data);
|
|
587
|
+
const range = max - min || 1;
|
|
588
|
+
|
|
589
|
+
const type = options.type || 'line';
|
|
590
|
+
|
|
591
|
+
this.ctx.save();
|
|
592
|
+
|
|
593
|
+
if (type === 'line' || type === 'area') {
|
|
594
|
+
this.ctx.beginPath();
|
|
595
|
+
for (let i = 0; i < data.length; i++) {
|
|
596
|
+
const px = drawX + (i / (data.length - 1)) * drawWidth;
|
|
597
|
+
const py = drawY + drawHeight - ((data[i] - min) / range) * drawHeight;
|
|
598
|
+
|
|
599
|
+
if (i === 0) this.ctx.moveTo(px, py);
|
|
600
|
+
else this.ctx.lineTo(px, py);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (type === 'area') {
|
|
604
|
+
const areaOptions = options.area || {};
|
|
605
|
+
this.ctx.lineTo(drawX + drawWidth, drawY + drawHeight);
|
|
606
|
+
this.ctx.lineTo(drawX, drawY + drawHeight);
|
|
607
|
+
this.ctx.closePath();
|
|
608
|
+
this.ctx.fillStyle = areaOptions.fill || 'rgba(33, 150, 243, 0.3)';
|
|
609
|
+
this.ctx.fill();
|
|
610
|
+
|
|
611
|
+
// Stroke the top line
|
|
612
|
+
this.ctx.beginPath();
|
|
613
|
+
for (let i = 0; i < data.length; i++) {
|
|
614
|
+
const px = drawX + (i / (data.length - 1)) * drawWidth;
|
|
615
|
+
const py = drawY + drawHeight - ((data[i] - min) / range) * drawHeight;
|
|
616
|
+
if (i === 0) this.ctx.moveTo(px, py);
|
|
617
|
+
else this.ctx.lineTo(px, py);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const lineOptions = (type === 'area' ? options.area : options.line) || {};
|
|
622
|
+
this.ctx.strokeStyle = lineOptions.stroke || '#2196f3';
|
|
623
|
+
this.ctx.lineWidth = lineOptions.strokeWidth || 1.5;
|
|
624
|
+
this.ctx.lineJoin = 'round';
|
|
625
|
+
this.ctx.lineCap = 'round';
|
|
626
|
+
this.ctx.stroke();
|
|
627
|
+
} else if (type === 'column' || type === 'bar') {
|
|
628
|
+
const colOptions = options.column || {};
|
|
629
|
+
const colPadding = colOptions.padding || 0.1;
|
|
630
|
+
const colWidth = drawWidth / data.length;
|
|
631
|
+
const barWidth = colWidth * (1 - colPadding);
|
|
632
|
+
|
|
633
|
+
this.ctx.fillStyle = colOptions.fill || '#2196f3';
|
|
634
|
+
|
|
635
|
+
for (let i = 0; i < data.length; i++) {
|
|
636
|
+
const px = drawX + i * colWidth + (colWidth * colPadding) / 2;
|
|
637
|
+
const valHeight = ((data[i] - min) / range) * drawHeight;
|
|
638
|
+
const py = drawY + drawHeight - valHeight;
|
|
639
|
+
|
|
640
|
+
this.ctx.fillRect(Math.floor(px), Math.floor(py), Math.floor(barWidth), Math.ceil(valHeight));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
this.ctx.restore();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private drawGroupIndicator(x: number, y: number, expanded: boolean): void {
|
|
648
|
+
this.ctx.beginPath();
|
|
649
|
+
const centerY = Math.floor(y + this.rowHeight / 2);
|
|
650
|
+
const size = this.theme.groupIndicatorSize;
|
|
651
|
+
|
|
652
|
+
if (expanded) {
|
|
653
|
+
// Expanded: horizontal line
|
|
654
|
+
this.ctx.moveTo(Math.floor(x), centerY);
|
|
655
|
+
this.ctx.lineTo(Math.floor(x + size), centerY);
|
|
656
|
+
} else {
|
|
657
|
+
// Collapsed: plus sign
|
|
658
|
+
const halfSize = size / 2;
|
|
659
|
+
this.ctx.moveTo(Math.floor(x), centerY);
|
|
660
|
+
this.ctx.lineTo(Math.floor(x + size), centerY);
|
|
661
|
+
this.ctx.moveTo(Math.floor(x + halfSize), centerY - halfSize);
|
|
662
|
+
this.ctx.lineTo(Math.floor(x + halfSize), centerY + halfSize);
|
|
663
|
+
}
|
|
664
|
+
this.ctx.stroke();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private drawGridLines(
|
|
668
|
+
columns: Column[],
|
|
669
|
+
startRow: number,
|
|
670
|
+
endRow: number,
|
|
671
|
+
viewportWidth: number,
|
|
672
|
+
viewportHeight: number,
|
|
673
|
+
leftWidth: number,
|
|
674
|
+
rightWidth: number
|
|
675
|
+
): void {
|
|
676
|
+
// Draw horizontal row lines
|
|
677
|
+
drawRowLines(
|
|
678
|
+
this.ctx,
|
|
679
|
+
startRow,
|
|
680
|
+
endRow,
|
|
681
|
+
this.rowHeight,
|
|
682
|
+
this.scrollTop,
|
|
683
|
+
viewportWidth,
|
|
684
|
+
this.theme
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
// Draw vertical column lines
|
|
688
|
+
drawColumnLines(
|
|
689
|
+
this.ctx,
|
|
690
|
+
columns,
|
|
691
|
+
this.scrollLeft,
|
|
692
|
+
this.scrollTop,
|
|
693
|
+
viewportWidth,
|
|
694
|
+
viewportHeight,
|
|
695
|
+
leftWidth,
|
|
696
|
+
rightWidth,
|
|
697
|
+
this.theme,
|
|
698
|
+
startRow,
|
|
699
|
+
endRow,
|
|
700
|
+
this.rowHeight
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ============================================================================
|
|
705
|
+
// EVENT HANDLING
|
|
706
|
+
// ============================================================================
|
|
707
|
+
|
|
708
|
+
private handleMouseDown(event: MouseEvent): void {
|
|
709
|
+
const { rowIndex, columnIndex } = this.getHitTestResult(event);
|
|
710
|
+
const columns = this.getVisibleColumns();
|
|
711
|
+
const colId = columnIndex !== -1 ? columns[columnIndex].colId : null;
|
|
712
|
+
|
|
713
|
+
if (this.onMouseDown) {
|
|
714
|
+
this.onMouseDown(event, rowIndex, colId);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const rowNode = this.gridApi.getDisplayedRowAtIndex(rowIndex);
|
|
718
|
+
if (!rowNode) return;
|
|
719
|
+
|
|
720
|
+
// Track old selection for damage tracking
|
|
721
|
+
const oldSelectedRows = new Set<number>(
|
|
722
|
+
this.gridApi.getSelectedNodes()
|
|
723
|
+
.map(node => node.rowIndex)
|
|
724
|
+
.filter(idx => idx !== null) as number[]
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
if (event.ctrlKey || event.metaKey) {
|
|
728
|
+
rowNode.selected = !rowNode.selected;
|
|
729
|
+
} else {
|
|
730
|
+
this.gridApi.deselectAll();
|
|
731
|
+
rowNode.selected = true;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Track new selection
|
|
735
|
+
const newSelectedRows = new Set<number>(
|
|
736
|
+
this.gridApi.getSelectedNodes()
|
|
737
|
+
.map(node => node.rowIndex)
|
|
738
|
+
.filter(idx => idx !== null) as number[]
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
// Mark changed rows as dirty
|
|
742
|
+
this.damageTracker.markSelectionChanged(oldSelectedRows, newSelectedRows);
|
|
743
|
+
|
|
744
|
+
this.scheduleRender();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private handleMouseMove(event: MouseEvent): void {
|
|
748
|
+
const { rowIndex, columnIndex } = this.getHitTestResult(event);
|
|
749
|
+
const columns = this.getVisibleColumns();
|
|
750
|
+
const colId = columnIndex !== -1 ? columns[columnIndex].colId : null;
|
|
751
|
+
|
|
752
|
+
if (this.onMouseMove) {
|
|
753
|
+
this.onMouseMove(event, rowIndex, colId);
|
|
754
|
+
}
|
|
755
|
+
// TODO: Implement hover state
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private handleMouseUp(event: MouseEvent): void {
|
|
759
|
+
const { rowIndex, columnIndex } = this.getHitTestResult(event);
|
|
760
|
+
const columns = this.getVisibleColumns();
|
|
761
|
+
const colId = columnIndex !== -1 ? columns[columnIndex].colId : null;
|
|
762
|
+
|
|
763
|
+
if (this.onMouseUp) {
|
|
764
|
+
this.onMouseUp(event, rowIndex, colId);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
private handleClick(event: MouseEvent): void {
|
|
769
|
+
const { rowIndex, columnIndex } = this.getHitTestResult(event);
|
|
770
|
+
const rowNode = this.gridApi.getDisplayedRowAtIndex(rowIndex);
|
|
771
|
+
if (!rowNode) return;
|
|
772
|
+
|
|
773
|
+
// Handle expand/collapse
|
|
774
|
+
if ((rowNode.group || rowNode.master) && columnIndex !== -1) {
|
|
775
|
+
const columns = this.getVisibleColumns();
|
|
776
|
+
const clickedCol = columns[columnIndex];
|
|
777
|
+
|
|
778
|
+
const isAutoGroupCol = clickedCol.colId === 'ag-Grid-AutoColumn';
|
|
779
|
+
const isFirstColIfNoAutoGroup = !columns.some(c => c.colId === 'ag-Grid-AutoColumn') && columnIndex === 0;
|
|
780
|
+
|
|
781
|
+
if (isAutoGroupCol || isFirstColIfNoAutoGroup) {
|
|
782
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
783
|
+
const x = event.clientX - rect.left;
|
|
784
|
+
const { left: leftWidth, right: rightWidth } = getPinnedWidths(columns);
|
|
785
|
+
|
|
786
|
+
let colX = 0;
|
|
787
|
+
if (clickedCol.pinned === 'left') {
|
|
788
|
+
const leftPinned = columns.filter(c => c.pinned === 'left');
|
|
789
|
+
for (let i = 0; i < columns.indexOf(clickedCol); i++) {
|
|
790
|
+
if (columns[i].pinned === 'left') colX += columns[i].width;
|
|
791
|
+
}
|
|
792
|
+
} else if (clickedCol.pinned === 'right') {
|
|
793
|
+
colX = this.viewportWidth - columns.filter(c => c.pinned === 'right').reduce((sum, c) => sum + c.width, 0);
|
|
794
|
+
} else {
|
|
795
|
+
colX = leftWidth + this.getCenterColumnOffset(clickedCol) - this.scrollLeft;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const indent = rowNode.level * this.theme.groupIndentWidth;
|
|
799
|
+
const indicatorAreaEnd = colX + this.theme.cellPadding + indent + this.theme.groupIndicatorSize + 3;
|
|
800
|
+
|
|
801
|
+
if (x >= colX + this.theme.cellPadding + indent && x < indicatorAreaEnd) {
|
|
802
|
+
this.gridApi.setRowNodeExpanded(rowNode, !rowNode.expanded);
|
|
803
|
+
this.damageTracker.markAllDirty(); // Group expansion affects many rows
|
|
804
|
+
this.render();
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (this.onRowClick) {
|
|
811
|
+
this.onRowClick(rowIndex, event);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private handleDoubleClick(event: MouseEvent): void {
|
|
816
|
+
const { rowIndex, columnIndex } = this.getHitTestResult(event);
|
|
817
|
+
if (columnIndex === -1) return;
|
|
818
|
+
|
|
819
|
+
const rowNode = this.gridApi.getDisplayedRowAtIndex(rowIndex);
|
|
820
|
+
if (!rowNode) return;
|
|
821
|
+
|
|
822
|
+
const columns = this.getVisibleColumns();
|
|
823
|
+
const column = columns[columnIndex];
|
|
824
|
+
|
|
825
|
+
if (this.onCellDoubleClick) {
|
|
826
|
+
this.onCellDoubleClick(rowIndex, column.colId);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
getHitTestResult(event: MouseEvent): { rowIndex: number; columnIndex: number } {
|
|
831
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
832
|
+
const canvasY = event.clientY - rect.top;
|
|
833
|
+
const canvasX = event.clientX - rect.left;
|
|
834
|
+
|
|
835
|
+
// Use walker utility for row detection
|
|
836
|
+
const rowIndex = getRowAtY(canvasY, this.rowHeight, this.scrollTop);
|
|
837
|
+
|
|
838
|
+
// Use walker utility for column detection
|
|
839
|
+
const result = getColumnAtX(
|
|
840
|
+
this.getVisibleColumns(),
|
|
841
|
+
canvasX,
|
|
842
|
+
this.scrollLeft,
|
|
843
|
+
this.viewportWidth
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
return { rowIndex, columnIndex: result.index };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private getCenterColumnOffset(targetCol: Column): number {
|
|
850
|
+
const columns = this.getVisibleColumns().filter(c => !c.pinned);
|
|
851
|
+
let offset = 0;
|
|
852
|
+
for (const col of columns) {
|
|
853
|
+
if (col === targetCol) return offset;
|
|
854
|
+
offset += col.width;
|
|
855
|
+
}
|
|
856
|
+
return offset;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private getColumnDef(column: Column): ColDef<TData> | null {
|
|
860
|
+
const allDefs = this.gridApi.getColumnDefs();
|
|
861
|
+
if (!allDefs) return null;
|
|
862
|
+
|
|
863
|
+
for (const def of allDefs) {
|
|
864
|
+
if ('children' in def) {
|
|
865
|
+
const found = def.children.find(c => {
|
|
866
|
+
const cDef = c as ColDef;
|
|
867
|
+
return cDef.colId === column.colId || cDef.field?.toString() === column.colId || cDef.field?.toString() === column.field;
|
|
868
|
+
});
|
|
869
|
+
if (found) return found as ColDef<TData>;
|
|
870
|
+
} else {
|
|
871
|
+
const cDef = def as ColDef;
|
|
872
|
+
if (cDef.colId === column.colId || cDef.field?.toString() === column.colId || cDef.field?.toString() === column.field) {
|
|
873
|
+
return def as ColDef<TData>;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// ============================================================================
|
|
881
|
+
// SCROLL API
|
|
882
|
+
// ============================================================================
|
|
883
|
+
|
|
884
|
+
scrollToRow(rowIndex: number): void {
|
|
885
|
+
const container = this.canvas.parentElement;
|
|
886
|
+
if (!container) return;
|
|
887
|
+
|
|
888
|
+
const targetPosition = rowIndex * this.rowHeight;
|
|
889
|
+
container.scrollTop = targetPosition;
|
|
890
|
+
this.scrollTop = targetPosition;
|
|
891
|
+
this.damageTracker.markAllDirty();
|
|
892
|
+
this.scheduleRender();
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
scrollToTop(): void {
|
|
896
|
+
this.scrollToRow(0);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
scrollToBottom(): void {
|
|
900
|
+
const container = this.canvas.parentElement;
|
|
901
|
+
if (!container) return;
|
|
902
|
+
|
|
903
|
+
container.scrollTop = container.scrollHeight - container.clientHeight;
|
|
904
|
+
this.scrollTop = container.scrollTop;
|
|
905
|
+
this.damageTracker.markAllDirty();
|
|
906
|
+
this.scheduleRender();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// ============================================================================
|
|
910
|
+
// DAMAGE TRACKING API
|
|
911
|
+
// ============================================================================
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Mark a specific cell as dirty
|
|
915
|
+
*/
|
|
916
|
+
invalidateCell(colIndex: number, rowIndex: number): void {
|
|
917
|
+
this.damageTracker.markCellDirty(colIndex, rowIndex);
|
|
918
|
+
this.scheduleRender();
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Mark a row as dirty
|
|
923
|
+
*/
|
|
924
|
+
invalidateRow(rowIndex: number): void {
|
|
925
|
+
this.damageTracker.markRowDirty(rowIndex);
|
|
926
|
+
this.scheduleRender();
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Mark entire grid as dirty
|
|
931
|
+
*/
|
|
932
|
+
invalidateAll(): void {
|
|
933
|
+
this.damageTracker.markAllDirty();
|
|
934
|
+
this.scheduleRender();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
destroy(): void {
|
|
938
|
+
if (this.animationFrameId) {
|
|
939
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Remove event listeners
|
|
943
|
+
const container = this.canvas.parentElement;
|
|
944
|
+
if (container && this.scrollListener) {
|
|
945
|
+
container.removeEventListener('scroll', this.scrollListener);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (this.mousedownListener) this.canvas.removeEventListener('mousedown', this.mousedownListener);
|
|
949
|
+
if (this.mousemoveListener) this.canvas.removeEventListener('mousemove', this.mousemoveListener);
|
|
950
|
+
if (this.clickListener) this.canvas.removeEventListener('click', this.clickListener);
|
|
951
|
+
if (this.dblclickListener) this.canvas.removeEventListener('dblclick', this.dblclickListener);
|
|
952
|
+
if (this.mouseupListener) this.canvas.removeEventListener('mouseup', this.mouseupListener);
|
|
953
|
+
|
|
954
|
+
if (this.resizeListener) {
|
|
955
|
+
window.removeEventListener('resize', this.resizeListener);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
this.renderPending = false;
|
|
959
|
+
this.blitState.reset();
|
|
960
|
+
this.damageTracker.reset();
|
|
961
|
+
}
|
|
962
|
+
}
|