argent-grid 0.1.0 → 0.2.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/ci.yml +69 -0
- package/.github/workflows/pages.yml +6 -12
- package/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/tsconfig.json +24 -0
- package/AGENTS.md +2 -2
- package/README.md +51 -34
- package/angular.json +66 -0
- package/biome.json +66 -0
- package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
- package/docs/AG-GRID-COMPARISON.md +725 -0
- package/docs/CELL-RENDERER-GUIDE.md +241 -0
- package/docs/CONTEXT-MENU-GUIDE.md +371 -0
- package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
- package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
- package/docs/PERFORMANCE-REVIEW.md +571 -0
- package/docs/RESEARCH-STATUS.md +234 -0
- package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
- package/docs/STORYBOOK-REFACTOR.md +215 -0
- package/docs/STORYBOOK-STATUS.md +156 -0
- package/docs/TEST-COVERAGE-REPORT.md +276 -0
- package/docs/THEME-API-GUIDE.md +445 -0
- package/docs/THEME-API-PLAN.md +364 -0
- package/e2e/advanced.spec.ts +109 -0
- package/e2e/argentgrid.spec.ts +65 -0
- package/e2e/benchmark.spec.ts +52 -0
- package/e2e/screenshots.spec.ts +52 -0
- package/e2e/theming.spec.ts +35 -0
- package/e2e/visual.spec.ts +91 -0
- package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
- package/package.json +20 -6
- package/plan.md +50 -18
- package/playwright.config.ts +38 -0
- package/setup-vitest.ts +10 -13
- package/src/lib/argent-grid.module.ts +10 -12
- package/src/lib/components/argent-grid.component.css +327 -76
- package/src/lib/components/argent-grid.component.html +186 -64
- package/src/lib/components/argent-grid.component.spec.ts +120 -160
- package/src/lib/components/argent-grid.component.ts +642 -189
- package/src/lib/components/argent-grid.selection.spec.ts +132 -0
- package/src/lib/components/set-filter/set-filter.component.ts +302 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
- package/src/lib/directives/click-outside.directive.ts +19 -0
- package/src/lib/rendering/canvas-renderer.spec.ts +366 -0
- package/src/lib/rendering/canvas-renderer.ts +418 -305
- package/src/lib/rendering/live-data-handler.ts +110 -0
- package/src/lib/rendering/live-data-optimizations.ts +133 -0
- package/src/lib/rendering/render/blit.spec.ts +16 -27
- package/src/lib/rendering/render/blit.ts +48 -36
- package/src/lib/rendering/render/cells.spec.ts +132 -0
- package/src/lib/rendering/render/cells.ts +46 -24
- package/src/lib/rendering/render/column-utils.ts +73 -0
- package/src/lib/rendering/render/hit-test.ts +55 -0
- package/src/lib/rendering/render/index.ts +79 -76
- package/src/lib/rendering/render/lines.ts +43 -43
- package/src/lib/rendering/render/primitives.ts +161 -0
- package/src/lib/rendering/render/theme.spec.ts +8 -12
- package/src/lib/rendering/render/theme.ts +7 -10
- package/src/lib/rendering/render/types.ts +2 -2
- package/src/lib/rendering/render/walk.spec.ts +35 -38
- package/src/lib/rendering/render/walk.ts +60 -50
- package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
- package/src/lib/rendering/utils/damage-tracker.ts +6 -18
- package/src/lib/rendering/utils/index.ts +1 -1
- package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
- package/src/lib/services/grid.service.spec.ts +1165 -201
- package/src/lib/services/grid.service.ts +819 -187
- package/src/lib/themes/parts/color-schemes.ts +132 -0
- package/src/lib/themes/parts/icon-sets.ts +258 -0
- package/src/lib/themes/theme-builder.ts +347 -0
- package/src/lib/themes/theme-quartz.ts +72 -0
- package/src/lib/themes/types.ts +238 -0
- package/src/lib/types/ag-grid-types.ts +73 -14
- package/src/public-api.ts +39 -9
- package/src/stories/Advanced.stories.ts +188 -0
- package/src/stories/ArgentGrid.stories.ts +277 -0
- package/src/stories/Benchmark.stories.ts +74 -0
- package/src/stories/CellRenderers.stories.ts +221 -0
- package/src/stories/Filtering.stories.ts +252 -0
- package/src/stories/Grouping.stories.ts +217 -0
- package/src/stories/Theming.stories.ts +124 -0
- package/src/stories/benchmark-wrapper.component.ts +315 -0
- package/tsconfig.storybook.json +10 -0
- package/vitest.config.ts +9 -9
- package/demo-app/README.md +0 -70
- package/demo-app/angular.json +0 -78
- package/demo-app/e2e/benchmark.spec.ts +0 -53
- package/demo-app/e2e/demo-page.spec.ts +0 -77
- package/demo-app/e2e/grid-features.spec.ts +0 -269
- package/demo-app/package-lock.json +0 -14023
- package/demo-app/package.json +0 -36
- package/demo-app/playwright-test-menu.js +0 -19
- package/demo-app/playwright.config.ts +0 -23
- package/demo-app/src/app/app.component.ts +0 -10
- package/demo-app/src/app/app.config.ts +0 -13
- package/demo-app/src/app/app.routes.ts +0 -7
- package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
- package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
- package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
- package/demo-app/src/index.html +0 -19
- package/demo-app/src/main.ts +0 -6
- package/demo-app/tsconfig.json +0 -31
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Data Handler for Canvas Renderer
|
|
3
|
+
*
|
|
4
|
+
* Manages high-frequency data updates and buffering.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { GridApi } from '../types/ag-grid-types';
|
|
8
|
+
|
|
9
|
+
export class LiveDataHandler<TData = any> {
|
|
10
|
+
private updateBuffer: TData[] = [];
|
|
11
|
+
private updateBufferTimer: number | null = null;
|
|
12
|
+
private batchInterval = 100;
|
|
13
|
+
private rowIndexById: Map<string, number> = new Map();
|
|
14
|
+
private dirtyRows: Set<number> = new Set();
|
|
15
|
+
|
|
16
|
+
constructor(private gridApi: GridApi<TData>) {}
|
|
17
|
+
|
|
18
|
+
setBatchInterval(intervalMs: number): void {
|
|
19
|
+
this.batchInterval = Math.max(16, intervalMs);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
addRowData(data: TData, immediate = false, onFlush: () => void): void {
|
|
23
|
+
this.updateBuffer.push(data);
|
|
24
|
+
|
|
25
|
+
// Index row by ID if available
|
|
26
|
+
const dataWithId = data as any;
|
|
27
|
+
if (dataWithId.id) {
|
|
28
|
+
const index = this.gridApi.getRowData().length + this.updateBuffer.length - 1;
|
|
29
|
+
this.rowIndexById.set(dataWithId.id, index);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (immediate) {
|
|
33
|
+
this.flushUpdateBuffer(onFlush);
|
|
34
|
+
} else if (!this.updateBufferTimer) {
|
|
35
|
+
this.updateBufferTimer = window.setTimeout(() => {
|
|
36
|
+
this.flushUpdateBuffer(onFlush);
|
|
37
|
+
}, this.batchInterval);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
flushUpdateBuffer(onFlush: () => void): void {
|
|
42
|
+
if (this.updateBuffer.length === 0) return;
|
|
43
|
+
|
|
44
|
+
if (this.updateBufferTimer) {
|
|
45
|
+
clearTimeout(this.updateBufferTimer);
|
|
46
|
+
this.updateBufferTimer = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const currentCount = this.gridApi.getDisplayedRowCount();
|
|
50
|
+
|
|
51
|
+
// Apply transaction
|
|
52
|
+
this.gridApi.applyTransaction({ add: this.updateBuffer });
|
|
53
|
+
|
|
54
|
+
// Mark new rows as dirty
|
|
55
|
+
for (let i = 0; i < this.updateBuffer.length; i++) {
|
|
56
|
+
this.dirtyRows.add(currentCount + i);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.updateBuffer = [];
|
|
60
|
+
onFlush();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
markRowDirty(rowIndex: number): void {
|
|
64
|
+
this.dirtyRows.add(rowIndex);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getDirtyRows(): Set<number> {
|
|
68
|
+
return this.dirtyRows;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clearDirtyRows(): void {
|
|
72
|
+
this.dirtyRows.clear();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
updateRowById(id: string, updates: Partial<TData>): boolean {
|
|
76
|
+
const index = this.rowIndexById.get(id);
|
|
77
|
+
const rowData = this.gridApi.getRowData();
|
|
78
|
+
if (index === undefined || index >= rowData.length) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.gridApi.applyTransaction({ update: [{ ...rowData[index], ...updates }] });
|
|
83
|
+
this.markRowDirty(index);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
removeRowById(id: string): boolean {
|
|
88
|
+
const index = this.rowIndexById.get(id);
|
|
89
|
+
if (index === undefined) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const rowData = this.gridApi.getRowData();
|
|
94
|
+
this.gridApi.applyTransaction({ remove: [rowData[index]] });
|
|
95
|
+
this.rowIndexById.delete(id);
|
|
96
|
+
this.rebuildRowIndex();
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
rebuildRowIndex(): void {
|
|
101
|
+
this.rowIndexById.clear();
|
|
102
|
+
const rowData = this.gridApi.getRowData();
|
|
103
|
+
rowData.forEach((row, index) => {
|
|
104
|
+
const rowWithId = row as any;
|
|
105
|
+
if (rowWithId.id) {
|
|
106
|
+
this.rowIndexById.set(rowWithId.id, index);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Data Optimizations for CanvasRenderer
|
|
3
|
+
*
|
|
4
|
+
* Performance optimizations for high-frequency data updates (10+ entries/sec):
|
|
5
|
+
* - Update batching (90% fewer renders)
|
|
6
|
+
* - Dirty row tracking (90% less rendering work)
|
|
7
|
+
* - Row ID indexing (O(1) updates instead of O(n))
|
|
8
|
+
*
|
|
9
|
+
* @see CanvasRenderer - Main renderer class that uses these optimizations
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mixin for live data optimizations
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* ```typescript
|
|
17
|
+
* class OptimizedRenderer extends LiveDataOptimizations(CanvasRenderer) {
|
|
18
|
+
* // Has all live data optimization methods
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function LiveDataOptimizations<TBase extends new (...args: any[]) => any>(Base: TBase) {
|
|
23
|
+
return class extends Base {
|
|
24
|
+
// Update batching
|
|
25
|
+
updateBuffer: any[] = [];
|
|
26
|
+
updateBufferTimer: number | null = null;
|
|
27
|
+
batchInterval = 100; // ms
|
|
28
|
+
|
|
29
|
+
// Dirty row tracking
|
|
30
|
+
dirtyRows: Set<number> = new Set();
|
|
31
|
+
|
|
32
|
+
// Row index by ID
|
|
33
|
+
rowIndexById: Map<string, number> = new Map();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Set update batching interval
|
|
37
|
+
*/
|
|
38
|
+
setBatchInterval(intervalMs: number): void {
|
|
39
|
+
this.batchInterval = Math.max(16, intervalMs);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Add row data with batching
|
|
44
|
+
*/
|
|
45
|
+
addRowData(data: any, immediate = false): void {
|
|
46
|
+
this.updateBuffer.push(data);
|
|
47
|
+
|
|
48
|
+
// Index by ID
|
|
49
|
+
if (data.id) {
|
|
50
|
+
const index = (this as any).rowData.length + this.updateBuffer.length - 1;
|
|
51
|
+
this.rowIndexById.set(data.id, index);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (immediate) {
|
|
55
|
+
this.flushUpdateBuffer();
|
|
56
|
+
} else if (!this.updateBufferTimer) {
|
|
57
|
+
this.updateBufferTimer = window.setTimeout(() => {
|
|
58
|
+
this.flushUpdateBuffer();
|
|
59
|
+
}, this.batchInterval);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Flush update buffer
|
|
65
|
+
*/
|
|
66
|
+
flushUpdateBuffer(): void {
|
|
67
|
+
if (this.updateBuffer.length === 0) return;
|
|
68
|
+
|
|
69
|
+
if (this.updateBufferTimer) {
|
|
70
|
+
clearTimeout(this.updateBufferTimer);
|
|
71
|
+
this.updateBufferTimer = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const startIndex = (this as any).rowData.length;
|
|
75
|
+
(this as any).rowData.push(...this.updateBuffer);
|
|
76
|
+
this.updateBuffer = [];
|
|
77
|
+
|
|
78
|
+
// Mark new rows as dirty
|
|
79
|
+
for (let i = 0; i < (this as any).rowData.length - startIndex; i++) {
|
|
80
|
+
this.dirtyRows.add(startIndex + i);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
(this as any).totalRowCount = (this as any).rowData.length;
|
|
84
|
+
(this as any).renderFrame();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Mark row as dirty
|
|
89
|
+
*/
|
|
90
|
+
markRowDirty(rowIndex: number): void {
|
|
91
|
+
this.dirtyRows.add(rowIndex);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Update row by ID (O(1))
|
|
96
|
+
*/
|
|
97
|
+
updateRowById(id: string, updates: any): boolean {
|
|
98
|
+
const index = this.rowIndexById.get(id);
|
|
99
|
+
if (index === undefined || index >= (this as any).rowData.length) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
Object.assign((this as any).rowData[index], updates);
|
|
104
|
+
this.markRowDirty(index);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Remove row by ID
|
|
110
|
+
*/
|
|
111
|
+
removeRowById(id: string): boolean {
|
|
112
|
+
const index = this.rowIndexById.get(id);
|
|
113
|
+
if (index === undefined) return false;
|
|
114
|
+
|
|
115
|
+
(this as any).rowData.splice(index, 1);
|
|
116
|
+
this.rowIndexById.delete(id);
|
|
117
|
+
this.rebuildRowIndex();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Rebuild row index
|
|
123
|
+
*/
|
|
124
|
+
rebuildRowIndex(): void {
|
|
125
|
+
this.rowIndexById.clear();
|
|
126
|
+
(this as any).rowData.forEach((row: any, index: number) => {
|
|
127
|
+
if (row.id) {
|
|
128
|
+
this.rowIndexById.set(row.id, index);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -5,18 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
calculateBlit,
|
|
8
|
+
BlitState,
|
|
10
9
|
blitLastFrame,
|
|
10
|
+
calculateBlit,
|
|
11
11
|
createBufferPair,
|
|
12
|
-
swapBuffers,
|
|
13
12
|
displayBuffer,
|
|
14
|
-
resizeBufferPair,
|
|
15
|
-
BlitState,
|
|
16
|
-
MIN_BLIT_DELTA,
|
|
17
13
|
MAX_BLIT_DELTA_RATIO,
|
|
14
|
+
MIN_BLIT_DELTA,
|
|
15
|
+
resizeBufferPair,
|
|
16
|
+
shouldBlit,
|
|
17
|
+
swapBuffers,
|
|
18
18
|
} from './blit';
|
|
19
|
-
import { Rectangle, BufferPair } from './types';
|
|
20
19
|
|
|
21
20
|
// Mock canvas and context
|
|
22
21
|
const mockContext = {
|
|
@@ -114,7 +113,7 @@ describe('Blitting Optimization', () => {
|
|
|
114
113
|
it('should calculate vertical scroll blit (down)', () => {
|
|
115
114
|
const result = calculateBlit(
|
|
116
115
|
{ x: 0, y: 100 }, // Current
|
|
117
|
-
{ x: 0, y: 0 },
|
|
116
|
+
{ x: 0, y: 0 }, // Last
|
|
118
117
|
viewportSize,
|
|
119
118
|
pinnedWidths
|
|
120
119
|
);
|
|
@@ -136,7 +135,7 @@ describe('Blitting Optimization', () => {
|
|
|
136
135
|
|
|
137
136
|
it('should calculate vertical scroll blit (up)', () => {
|
|
138
137
|
const result = calculateBlit(
|
|
139
|
-
{ x: 0, y: 0 },
|
|
138
|
+
{ x: 0, y: 0 }, // Current
|
|
140
139
|
{ x: 0, y: 100 }, // Last
|
|
141
140
|
viewportSize,
|
|
142
141
|
pinnedWidths
|
|
@@ -154,7 +153,7 @@ describe('Blitting Optimization', () => {
|
|
|
154
153
|
it('should calculate horizontal scroll blit (right)', () => {
|
|
155
154
|
const result = calculateBlit(
|
|
156
155
|
{ x: 100, y: 0 }, // Current
|
|
157
|
-
{ x: 0, y: 0 },
|
|
156
|
+
{ x: 0, y: 0 }, // Last
|
|
158
157
|
viewportSize,
|
|
159
158
|
pinnedWidths
|
|
160
159
|
);
|
|
@@ -171,7 +170,7 @@ describe('Blitting Optimization', () => {
|
|
|
171
170
|
|
|
172
171
|
it('should calculate horizontal scroll blit (left)', () => {
|
|
173
172
|
const result = calculateBlit(
|
|
174
|
-
{ x: 0, y: 0 },
|
|
173
|
+
{ x: 0, y: 0 }, // Current
|
|
175
174
|
{ x: 100, y: 0 }, // Last
|
|
176
175
|
viewportSize,
|
|
177
176
|
pinnedWidths
|
|
@@ -185,12 +184,7 @@ describe('Blitting Optimization', () => {
|
|
|
185
184
|
});
|
|
186
185
|
|
|
187
186
|
it('should return full redraw for diagonal scroll', () => {
|
|
188
|
-
const result = calculateBlit(
|
|
189
|
-
{ x: 50, y: 50 },
|
|
190
|
-
{ x: 0, y: 0 },
|
|
191
|
-
viewportSize,
|
|
192
|
-
pinnedWidths
|
|
193
|
-
);
|
|
187
|
+
const result = calculateBlit({ x: 50, y: 50 }, { x: 0, y: 0 }, viewportSize, pinnedWidths);
|
|
194
188
|
|
|
195
189
|
expect(result.canBlit).toBe(false);
|
|
196
190
|
expect(result.dirtyRegions[0].width).toBe(800);
|
|
@@ -198,12 +192,7 @@ describe('Blitting Optimization', () => {
|
|
|
198
192
|
});
|
|
199
193
|
|
|
200
194
|
it('should account for pinned widths', () => {
|
|
201
|
-
const result = calculateBlit(
|
|
202
|
-
{ x: 100, y: 0 },
|
|
203
|
-
{ x: 0, y: 0 },
|
|
204
|
-
viewportSize,
|
|
205
|
-
pinnedWidths
|
|
206
|
-
);
|
|
195
|
+
const result = calculateBlit({ x: 100, y: 0 }, { x: 0, y: 0 }, viewportSize, pinnedWidths);
|
|
207
196
|
|
|
208
197
|
// Center region is between pinned columns
|
|
209
198
|
expect(result.sourceRect.x).toBe(100); // left pinned width
|
|
@@ -293,7 +282,7 @@ describe('Blitting Optimization', () => {
|
|
|
293
282
|
|
|
294
283
|
it('should swap contexts too', () => {
|
|
295
284
|
const buffers = createBufferPair(800, 600);
|
|
296
|
-
const
|
|
285
|
+
const _originalFrontCtx = buffers.frontCtx;
|
|
297
286
|
|
|
298
287
|
swapBuffers(buffers);
|
|
299
288
|
|
|
@@ -377,8 +366,8 @@ describe('Blitting Optimization', () => {
|
|
|
377
366
|
|
|
378
367
|
const lastCanvas = state.getLastCanvas();
|
|
379
368
|
expect(lastCanvas).not.toBeNull();
|
|
380
|
-
expect(lastCanvas
|
|
381
|
-
expect(lastCanvas
|
|
369
|
+
expect(lastCanvas?.width).toBe(800);
|
|
370
|
+
expect(lastCanvas?.height).toBe(600);
|
|
382
371
|
});
|
|
383
372
|
|
|
384
373
|
it('should report hasLastFrame correctly', () => {
|
|
@@ -450,4 +439,4 @@ describe('Blitting Optimization', () => {
|
|
|
450
439
|
expect(result.canBlit).toBe(true);
|
|
451
440
|
});
|
|
452
441
|
});
|
|
453
|
-
});
|
|
442
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Based on Glide Data Grid's blitting architecture.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { BlitResult, BufferPair, Rectangle } from './types';
|
|
9
9
|
|
|
10
10
|
// ============================================================================
|
|
11
11
|
// BLIT THRESHOLDS
|
|
@@ -99,7 +99,7 @@ export function calculateBlit(
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
const
|
|
102
|
+
const _dirtyRegions: Rectangle[] = [];
|
|
103
103
|
|
|
104
104
|
// Vertical scroll (most common)
|
|
105
105
|
if (Math.abs(deltaY) >= MIN_BLIT_DELTA && Math.abs(deltaX) < MIN_BLIT_DELTA) {
|
|
@@ -127,19 +127,27 @@ export function calculateBlit(
|
|
|
127
127
|
},
|
|
128
128
|
dirtyRegions: [
|
|
129
129
|
// Top strip that needs redraw
|
|
130
|
-
...(deltaY > 0
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
...(deltaY > 0
|
|
131
|
+
? [
|
|
132
|
+
{
|
|
133
|
+
x: leftPinnedWidth,
|
|
134
|
+
y: 0,
|
|
135
|
+
width: centerWidth,
|
|
136
|
+
height: absDelta,
|
|
137
|
+
},
|
|
138
|
+
]
|
|
139
|
+
: []),
|
|
136
140
|
// Bottom strip that needs redraw
|
|
137
|
-
...(deltaY < 0
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
...(deltaY < 0
|
|
142
|
+
? [
|
|
143
|
+
{
|
|
144
|
+
x: leftPinnedWidth,
|
|
145
|
+
y: viewportSize.height - absDelta,
|
|
146
|
+
width: centerWidth,
|
|
147
|
+
height: absDelta,
|
|
148
|
+
},
|
|
149
|
+
]
|
|
150
|
+
: []),
|
|
143
151
|
],
|
|
144
152
|
deltaX,
|
|
145
153
|
deltaY,
|
|
@@ -171,19 +179,27 @@ export function calculateBlit(
|
|
|
171
179
|
},
|
|
172
180
|
dirtyRegions: [
|
|
173
181
|
// Left strip that needs redraw
|
|
174
|
-
...(deltaX > 0
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
182
|
+
...(deltaX > 0
|
|
183
|
+
? [
|
|
184
|
+
{
|
|
185
|
+
x: leftPinnedWidth,
|
|
186
|
+
y: 0,
|
|
187
|
+
width: absDelta,
|
|
188
|
+
height: viewportSize.height,
|
|
189
|
+
},
|
|
190
|
+
]
|
|
191
|
+
: []),
|
|
180
192
|
// Right strip that needs redraw
|
|
181
|
-
...(deltaX < 0
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
...(deltaX < 0
|
|
194
|
+
? [
|
|
195
|
+
{
|
|
196
|
+
x: viewportSize.width - rightPinnedWidth - absDelta,
|
|
197
|
+
y: 0,
|
|
198
|
+
width: absDelta,
|
|
199
|
+
height: viewportSize.height,
|
|
200
|
+
},
|
|
201
|
+
]
|
|
202
|
+
: []),
|
|
187
203
|
],
|
|
188
204
|
deltaX,
|
|
189
205
|
deltaY,
|
|
@@ -248,15 +264,11 @@ export function blitLastFrame(
|
|
|
248
264
|
/**
|
|
249
265
|
* Create a buffer pair for double buffering
|
|
250
266
|
*/
|
|
251
|
-
export function createBufferPair(
|
|
252
|
-
width: number,
|
|
253
|
-
height: number,
|
|
254
|
-
dpr: number = 1
|
|
255
|
-
): BufferPair {
|
|
267
|
+
export function createBufferPair(width: number, height: number, dpr: number = 1): BufferPair {
|
|
256
268
|
const front = document.createElement('canvas');
|
|
257
269
|
const back = document.createElement('canvas');
|
|
258
270
|
|
|
259
|
-
[front, back].forEach(canvas => {
|
|
271
|
+
[front, back].forEach((canvas) => {
|
|
260
272
|
canvas.width = Math.floor(width * dpr);
|
|
261
273
|
canvas.height = Math.floor(height * dpr);
|
|
262
274
|
canvas.style.width = `${width}px`;
|
|
@@ -307,7 +319,7 @@ export function resizeBufferPair(
|
|
|
307
319
|
height: number,
|
|
308
320
|
dpr: number = 1
|
|
309
321
|
): void {
|
|
310
|
-
[buffers.front, buffers.back].forEach(canvas => {
|
|
322
|
+
[buffers.front, buffers.back].forEach((canvas) => {
|
|
311
323
|
canvas.width = Math.floor(width * dpr);
|
|
312
324
|
canvas.height = Math.floor(height * dpr);
|
|
313
325
|
canvas.style.width = `${width}px`;
|
|
@@ -356,14 +368,14 @@ export class BlitState {
|
|
|
356
368
|
if (!this.lastCanvas) {
|
|
357
369
|
this.lastCanvas = document.createElement('canvas');
|
|
358
370
|
}
|
|
359
|
-
|
|
371
|
+
|
|
360
372
|
// Only resize if dimensions changed, as setting width/height clears the canvas
|
|
361
373
|
// and is an expensive operation.
|
|
362
374
|
if (this.lastCanvas.width !== canvas.width || this.lastCanvas.height !== canvas.height) {
|
|
363
375
|
this.lastCanvas.width = canvas.width;
|
|
364
376
|
this.lastCanvas.height = canvas.height;
|
|
365
377
|
}
|
|
366
|
-
|
|
378
|
+
|
|
367
379
|
const ctx = this.lastCanvas.getContext('2d')!;
|
|
368
380
|
ctx.drawImage(canvas, 0, 0);
|
|
369
381
|
}
|
|
@@ -390,4 +402,4 @@ export class BlitState {
|
|
|
390
402
|
this.lastScrollY = 0;
|
|
391
403
|
this.lastCanvas = null;
|
|
392
404
|
}
|
|
393
|
-
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ColDef, GridApi, IRowNode } from '../../types/ag-grid-types';
|
|
3
|
+
import { getFormattedValue, getValueByPath, stripHtmlTags } from './cells';
|
|
4
|
+
|
|
5
|
+
describe('cells.ts', () => {
|
|
6
|
+
describe('stripHtmlTags', () => {
|
|
7
|
+
it('should strip HTML tags from string', () => {
|
|
8
|
+
expect(stripHtmlTags('<span>active</span>')).toBe('active');
|
|
9
|
+
expect(stripHtmlTags('<div class="test">content</div>')).toBe('content');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should handle complex HTML', () => {
|
|
13
|
+
const html =
|
|
14
|
+
'<span style="color: green; padding: 4px 8px; background: green20; border-radius: 4px;">active</span>';
|
|
15
|
+
expect(stripHtmlTags(html)).toBe('active');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should handle empty string', () => {
|
|
19
|
+
expect(stripHtmlTags('')).toBe('');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should handle null/undefined', () => {
|
|
23
|
+
expect(stripHtmlTags(null as any)).toBe('');
|
|
24
|
+
expect(stripHtmlTags(undefined as any)).toBe('');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should handle plain text (no HTML)', () => {
|
|
28
|
+
expect(stripHtmlTags('plain text')).toBe('plain text');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should handle multiple tags', () => {
|
|
32
|
+
expect(stripHtmlTags('<strong><em>bold italic</em></strong>')).toBe('bold italic');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('getValueByPath', () => {
|
|
37
|
+
it('should get value from simple path', () => {
|
|
38
|
+
const obj = { name: 'John', age: 30 };
|
|
39
|
+
expect(getValueByPath(obj, 'name')).toBe('John');
|
|
40
|
+
expect(getValueByPath(obj, 'age')).toBe(30);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should get value from nested path', () => {
|
|
44
|
+
const obj = { user: { name: 'John', address: { city: 'NYC' } } };
|
|
45
|
+
expect(getValueByPath(obj, 'user.name')).toBe('John');
|
|
46
|
+
expect(getValueByPath(obj, 'user.address.city')).toBe('NYC');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return undefined for missing path', () => {
|
|
50
|
+
const obj = { name: 'John' };
|
|
51
|
+
expect(getValueByPath(obj, 'age')).toBe(undefined);
|
|
52
|
+
expect(getValueByPath(obj, 'address.city')).toBe(undefined);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should handle null/undefined objects', () => {
|
|
56
|
+
expect(getValueByPath(null, 'name')).toBe(undefined);
|
|
57
|
+
expect(getValueByPath(undefined, 'name')).toBe(undefined);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('getFormattedValue', () => {
|
|
62
|
+
const mockApi = {} as GridApi;
|
|
63
|
+
const mockRowNode = { data: { name: 'John', age: 30 } } as IRowNode;
|
|
64
|
+
|
|
65
|
+
it('should return empty string for null/undefined', () => {
|
|
66
|
+
expect(getFormattedValue(null, null, null as any, mockRowNode, mockApi)).toBe('');
|
|
67
|
+
expect(getFormattedValue(undefined, null, null as any, mockRowNode, mockApi)).toBe('');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should use valueFormatter if provided', () => {
|
|
71
|
+
const colDef = {
|
|
72
|
+
valueFormatter: vi.fn((params) => `$${params.value}`),
|
|
73
|
+
} as ColDef;
|
|
74
|
+
|
|
75
|
+
const result = getFormattedValue(100, colDef, { salary: 100 }, mockRowNode, mockApi);
|
|
76
|
+
expect(result).toBe('$100');
|
|
77
|
+
expect(colDef.valueFormatter).toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should use cellRenderer and strip HTML tags', () => {
|
|
81
|
+
const colDef = {
|
|
82
|
+
cellRenderer: vi.fn((params) => `<span style="color: green">${params.value}</span>`),
|
|
83
|
+
} as ColDef;
|
|
84
|
+
|
|
85
|
+
const result = getFormattedValue(
|
|
86
|
+
'active',
|
|
87
|
+
colDef,
|
|
88
|
+
{ status: 'active' },
|
|
89
|
+
mockRowNode,
|
|
90
|
+
mockApi
|
|
91
|
+
);
|
|
92
|
+
expect(result).toBe('active'); // HTML stripped
|
|
93
|
+
expect(colDef.cellRenderer).toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should handle cellRenderer returning plain text', () => {
|
|
97
|
+
const colDef = {
|
|
98
|
+
cellRenderer: vi.fn((params) => params.value.toUpperCase()),
|
|
99
|
+
} as ColDef;
|
|
100
|
+
|
|
101
|
+
const result = getFormattedValue('hello', colDef, { text: 'hello' }, mockRowNode, mockApi);
|
|
102
|
+
expect(result).toBe('HELLO');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should convert value to string if no formatter/renderer', () => {
|
|
106
|
+
const result = getFormattedValue(123, null, null as any, mockRowNode, mockApi);
|
|
107
|
+
expect(result).toBe('123');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle cellRenderer errors gracefully', () => {
|
|
111
|
+
const colDef = {
|
|
112
|
+
cellRenderer: vi.fn(() => {
|
|
113
|
+
throw new Error('Renderer error');
|
|
114
|
+
}),
|
|
115
|
+
} as ColDef;
|
|
116
|
+
|
|
117
|
+
const result = getFormattedValue('test', colDef, null as any, mockRowNode, mockApi);
|
|
118
|
+
expect(result).toBe('test'); // Falls back to value
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should handle valueFormatter errors gracefully', () => {
|
|
122
|
+
const colDef = {
|
|
123
|
+
valueFormatter: vi.fn(() => {
|
|
124
|
+
throw new Error('Formatter error');
|
|
125
|
+
}),
|
|
126
|
+
} as ColDef;
|
|
127
|
+
|
|
128
|
+
const result = getFormattedValue('test', colDef, null as any, mockRowNode, mockApi);
|
|
129
|
+
expect(result).toBe('test'); // Falls back to value
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|