argent-grid 0.1.0 → 0.3.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 +70 -27
- 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/cell-renderers.spec.ts +152 -0
- package/e2e/debug-streaming.spec.ts +31 -0
- package/e2e/dnd.spec.ts +73 -0
- package/e2e/screenshots.spec.ts +52 -0
- package/e2e/theming.spec.ts +35 -0
- package/e2e/visual.spec.ts +112 -0
- package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
- package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -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/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
- package/package.json +21 -7
- package/plan.md +56 -28
- 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 +281 -321
- package/src/lib/components/argent-grid.component.html +295 -207
- package/src/lib/components/argent-grid.component.spec.ts +120 -160
- package/src/lib/components/argent-grid.component.ts +1193 -290
- package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
- package/src/lib/components/argent-grid.selection.spec.ts +132 -0
- package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
- package/src/lib/components/set-filter/set-filter.component.ts +307 -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 +513 -0
- package/src/lib/rendering/canvas-renderer.ts +456 -452
- 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 +167 -28
- package/src/lib/rendering/render/column-utils.ts +95 -0
- package/src/lib/rendering/render/hit-test.ts +50 -0
- package/src/lib/rendering/render/index.ts +88 -76
- package/src/lib/rendering/render/lines.ts +53 -47
- package/src/lib/rendering/render/primitives.ts +423 -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 +3 -2
- package/src/lib/rendering/render/walk.spec.ts +35 -38
- package/src/lib/rendering/render/walk.ts +94 -64
- 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 +1241 -201
- package/src/lib/services/grid.service.ts +1204 -235
- 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 +573 -14
- package/src/public-api.ts +39 -9
- package/src/stories/Advanced.stories.ts +249 -0
- package/src/stories/ArgentGrid.stories.ts +301 -0
- package/src/stories/Benchmark.stories.ts +76 -0
- package/src/stories/CellRenderers.stories.ts +395 -0
- package/src/stories/Filtering.stories.ts +292 -0
- package/src/stories/Grouping.stories.ts +290 -0
- package/src/stories/Streaming.stories.ts +57 -0
- package/src/stories/Theming.stories.ts +137 -0
- package/src/stories/Tooltips.stories.ts +381 -0
- package/src/stories/benchmark-wrapper.component.ts +355 -0
- package/src/stories/story-utils.ts +88 -0
- package/src/stories/streaming-wrapper.component.ts +441 -0
- package/tsconfig.json +1 -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
|
@@ -1,36 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { Column, GridApi, IRowNode } from '../types/ag-grid-types';
|
|
2
|
+
import { LiveDataHandler } from './live-data-handler';
|
|
3
3
|
// Import new rendering modules from the index
|
|
4
4
|
import {
|
|
5
|
-
// Types
|
|
6
|
-
GridTheme,
|
|
7
5
|
ColumnPrepResult,
|
|
8
6
|
// Theme
|
|
9
7
|
DEFAULT_THEME,
|
|
8
|
+
drawCell,
|
|
9
|
+
drawColumnLines,
|
|
10
|
+
drawRangeSelectionBorder,
|
|
11
|
+
// Lines
|
|
12
|
+
drawRowLines,
|
|
13
|
+
// Types
|
|
14
|
+
GridTheme,
|
|
15
|
+
getCellValue,
|
|
16
|
+
getCenterColumnOffset,
|
|
17
|
+
getColumnAtX,
|
|
18
|
+
getColumnDef,
|
|
10
19
|
getFontFromTheme,
|
|
11
|
-
|
|
12
|
-
// Walker
|
|
13
|
-
walkColumns,
|
|
14
|
-
walkRows,
|
|
15
|
-
walkCells,
|
|
16
|
-
getVisibleRowRange,
|
|
20
|
+
getFormattedValue,
|
|
17
21
|
getPinnedWidths,
|
|
18
|
-
|
|
22
|
+
getPositionedColumns,
|
|
19
23
|
getRowAtY,
|
|
20
|
-
// Blitting
|
|
21
|
-
BlitState,
|
|
22
|
-
calculateBlit,
|
|
23
|
-
shouldBlit,
|
|
24
|
-
// Cells
|
|
25
|
-
truncateText,
|
|
26
|
-
prepColumn,
|
|
27
|
-
getFormattedValue,
|
|
28
24
|
getValueByPath,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
getVisibleRowRange,
|
|
26
|
+
isColumnVisible,
|
|
27
|
+
mergeTheme,
|
|
28
|
+
PositionedColumn,
|
|
29
|
+
performHitTest,
|
|
30
|
+
prepColumn,
|
|
31
|
+
walkRows,
|
|
34
32
|
} from './render';
|
|
35
33
|
import { DamageTracker } from './utils/damage-tracker';
|
|
36
34
|
|
|
@@ -52,32 +50,39 @@ export class CanvasRenderer<TData = any> {
|
|
|
52
50
|
private canvas: HTMLCanvasElement;
|
|
53
51
|
private ctx: CanvasRenderingContext2D;
|
|
54
52
|
private gridApi: GridApi<TData>;
|
|
55
|
-
private rowHeight: number;
|
|
56
53
|
private scrollTop = 0;
|
|
57
54
|
private scrollLeft = 0;
|
|
58
55
|
|
|
59
|
-
get currentScrollTop(): number {
|
|
60
|
-
|
|
56
|
+
get currentScrollTop(): number {
|
|
57
|
+
return this.scrollTop;
|
|
58
|
+
}
|
|
59
|
+
get currentScrollLeft(): number {
|
|
60
|
+
return this.scrollLeft;
|
|
61
|
+
}
|
|
61
62
|
|
|
62
63
|
private animationFrameId: number | null = null;
|
|
63
|
-
|
|
64
|
+
// When a render is already in-flight and another is requested, coalesce it here
|
|
65
|
+
// so it fires immediately after the current frame completes rather than being dropped.
|
|
66
|
+
private nextRenderPending = false;
|
|
64
67
|
private rowBuffer = 5;
|
|
65
|
-
private totalRowCount = 0;
|
|
66
68
|
private viewportHeight = 0;
|
|
67
69
|
private viewportWidth = 0;
|
|
70
|
+
private scrollbarWidth = 0;
|
|
68
71
|
|
|
69
72
|
// Theme system
|
|
70
73
|
private theme: GridTheme;
|
|
71
74
|
|
|
72
75
|
// Performance tracking
|
|
73
76
|
private lastRenderDuration = 0;
|
|
74
|
-
get lastFrameTime(): number {
|
|
77
|
+
get lastFrameTime(): number {
|
|
78
|
+
return this.lastRenderDuration;
|
|
79
|
+
}
|
|
75
80
|
|
|
76
81
|
// Damage tracking
|
|
77
82
|
private damageTracker = new DamageTracker();
|
|
78
83
|
|
|
79
|
-
//
|
|
80
|
-
private
|
|
84
|
+
// Live data handling
|
|
85
|
+
private liveDataHandler: LiveDataHandler<TData>;
|
|
81
86
|
|
|
82
87
|
// Column prep results cache
|
|
83
88
|
private columnPreps: Map<string, ColumnPrepResult<TData>> = new Map();
|
|
@@ -87,6 +92,7 @@ export class CanvasRenderer<TData = any> {
|
|
|
87
92
|
private resizeListener?: () => void;
|
|
88
93
|
private mousedownListener?: (e: MouseEvent) => void;
|
|
89
94
|
private mousemoveListener?: (e: MouseEvent) => void;
|
|
95
|
+
private mouseleaveListener?: (e: MouseEvent) => void;
|
|
90
96
|
private clickListener?: (e: MouseEvent) => void;
|
|
91
97
|
private dblclickListener?: (e: MouseEvent) => void;
|
|
92
98
|
private mouseupListener?: (e: MouseEvent) => void;
|
|
@@ -107,8 +113,8 @@ export class CanvasRenderer<TData = any> {
|
|
|
107
113
|
this.canvas = canvas;
|
|
108
114
|
this.ctx = canvas.getContext('2d')!;
|
|
109
115
|
this.gridApi = gridApi;
|
|
110
|
-
this.rowHeight = rowHeight;
|
|
111
116
|
this.theme = mergeTheme(DEFAULT_THEME, { rowHeight }, theme || {});
|
|
117
|
+
this.liveDataHandler = new LiveDataHandler(gridApi);
|
|
112
118
|
|
|
113
119
|
this.setupEventListeners();
|
|
114
120
|
this.resize();
|
|
@@ -118,7 +124,7 @@ export class CanvasRenderer<TData = any> {
|
|
|
118
124
|
* Update the theme
|
|
119
125
|
*/
|
|
120
126
|
setTheme(theme: Partial<GridTheme>): void {
|
|
121
|
-
this.theme = mergeTheme(DEFAULT_THEME, { rowHeight: this.rowHeight }, theme);
|
|
127
|
+
this.theme = mergeTheme(DEFAULT_THEME, { rowHeight: this.theme.rowHeight }, theme);
|
|
122
128
|
this.damageTracker.markAllDirty();
|
|
123
129
|
this.scheduleRender();
|
|
124
130
|
}
|
|
@@ -130,21 +136,140 @@ export class CanvasRenderer<TData = any> {
|
|
|
130
136
|
return this.theme;
|
|
131
137
|
}
|
|
132
138
|
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// LIVE DATA OPTIMIZATIONS
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set update batching interval for live data scenarios
|
|
145
|
+
*
|
|
146
|
+
* Performance optimization: Batches multiple data updates into a single render,
|
|
147
|
+
* reducing render calls by 90% for high-frequency data feeds (10+ entries/sec).
|
|
148
|
+
*
|
|
149
|
+
* @param intervalMs - Batch interval in milliseconds (default: 100ms = ~10fps)
|
|
150
|
+
*/
|
|
151
|
+
setBatchInterval(intervalMs: number): void {
|
|
152
|
+
this.liveDataHandler.setBatchInterval(intervalMs);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
addRowData(data: TData, immediate = false): void {
|
|
156
|
+
this.liveDataHandler.addRowData(data, immediate, () => this.renderFrame());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
flushUpdateBuffer(): void {
|
|
160
|
+
this.liveDataHandler.flushUpdateBuffer(() => this.renderFrame());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
markRowDirty(rowIndex: number): void {
|
|
164
|
+
this.liveDataHandler.markRowDirty(rowIndex);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
updateRowById(id: string, updates: Partial<TData>): boolean {
|
|
168
|
+
return this.liveDataHandler.updateRowById(id, updates);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
removeRowById(id: string): boolean {
|
|
172
|
+
return this.liveDataHandler.removeRowById(id);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Render a single frame (public for testing)
|
|
177
|
+
*/
|
|
178
|
+
renderFrame(): void {
|
|
179
|
+
this.doRender();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get row index at Y coordinate (O(1) lookup)
|
|
184
|
+
*
|
|
185
|
+
* Performance optimization: Uses direct mathematical calculation instead of
|
|
186
|
+
* iterating through rows. This provides O(1) constant-time hit testing,
|
|
187
|
+
* essential for responsive mouse interactions even with 1M+ rows.
|
|
188
|
+
*
|
|
189
|
+
* Formula: rowIndex = floor((y + scrollTop) / rowHeight)
|
|
190
|
+
*
|
|
191
|
+
* @param y - Y coordinate in canvas space
|
|
192
|
+
* @returns Row index at Y coordinate
|
|
193
|
+
*
|
|
194
|
+
* @performance O(1) - Constant time, regardless of total row count
|
|
195
|
+
*/
|
|
196
|
+
getRowAtY(y: number): number {
|
|
197
|
+
return getRowAtY(y, this.theme.rowHeight, this.scrollTop);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get column at X coordinate (public for testing)
|
|
202
|
+
*
|
|
203
|
+
* @param x - X coordinate in canvas space
|
|
204
|
+
* @returns Column at X coordinate or null if not found
|
|
205
|
+
*/
|
|
206
|
+
getColumnAtX(x: number): Column | null {
|
|
207
|
+
const columns = this.getVisibleColumns();
|
|
208
|
+
const result = getColumnAtX(columns, x, this.scrollLeft, this.viewportWidth);
|
|
209
|
+
return result?.column || null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Throttle function calls to limit execution rate
|
|
214
|
+
*
|
|
215
|
+
* Performance optimization: Mouse move events can fire hundreds of times per second,
|
|
216
|
+
* causing excessive event handler calls and potential performance issues. This throttle
|
|
217
|
+
* function limits the execution rate to once per `limit` milliseconds (typically 16ms
|
|
218
|
+
* for ~60fps), reducing event handler calls by 50-80%.
|
|
219
|
+
*
|
|
220
|
+
* @param fn - Function to throttle
|
|
221
|
+
* @param limit - Minimum time between calls in milliseconds (16ms = ~60fps)
|
|
222
|
+
* @returns Throttled function
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* // Throttle mousemove to 60fps
|
|
226
|
+
* this.mousemoveListener = this.throttle(this.handleMouseMove.bind(this), 16);
|
|
227
|
+
*/
|
|
228
|
+
private throttle<T extends (...args: any[]) => any>(fn: T, limit: number): T {
|
|
229
|
+
let inThrottle = false;
|
|
230
|
+
return ((...args: any[]) => {
|
|
231
|
+
if (!inThrottle) {
|
|
232
|
+
fn.apply(this, args);
|
|
233
|
+
inThrottle = true;
|
|
234
|
+
setTimeout(() => (inThrottle = false), limit);
|
|
235
|
+
}
|
|
236
|
+
}) as T;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Setup event listeners for user interactions
|
|
241
|
+
*
|
|
242
|
+
* Performance optimizations:
|
|
243
|
+
* 1. Mouse move throttling - Limits mousemove events to ~60fps (16ms intervals),
|
|
244
|
+
* reducing event handler calls by 50-80% without affecting user experience.
|
|
245
|
+
* 2. Passive scroll listener - Allows browser to optimize scroll performance
|
|
246
|
+
* by indicating we won't call preventDefault().
|
|
247
|
+
*
|
|
248
|
+
* @see throttle() - Mouse move throttling implementation
|
|
249
|
+
*/
|
|
133
250
|
private setupEventListeners(): void {
|
|
134
251
|
const container = this.canvas.parentElement;
|
|
135
252
|
if (container) {
|
|
253
|
+
// Use passive listener for better scroll performance
|
|
136
254
|
this.scrollListener = this.handleScroll.bind(this);
|
|
137
255
|
container.addEventListener('scroll', this.scrollListener, { passive: true });
|
|
138
256
|
}
|
|
139
257
|
|
|
140
258
|
this.mousedownListener = this.handleMouseDown.bind(this);
|
|
141
|
-
|
|
259
|
+
// Throttle mousemove to ~60fps (16ms) to reduce excessive event handler calls
|
|
260
|
+
// Mousemove can fire hundreds of times per second; throttling reduces this to 60fps
|
|
261
|
+
// without affecting user experience, improving performance by 50-80%
|
|
262
|
+
this.mousemoveListener = this.throttle(this.handleMouseMove.bind(this), 16);
|
|
142
263
|
this.clickListener = this.handleClick.bind(this);
|
|
143
264
|
this.dblclickListener = this.handleDoubleClick.bind(this);
|
|
144
265
|
this.mouseupListener = this.handleMouseUp.bind(this);
|
|
145
266
|
|
|
146
267
|
this.canvas.addEventListener('mousedown', this.mousedownListener);
|
|
147
268
|
this.canvas.addEventListener('mousemove', this.mousemoveListener);
|
|
269
|
+
this.mouseleaveListener = () => {
|
|
270
|
+
this.canvas.style.cursor = '';
|
|
271
|
+
};
|
|
272
|
+
this.canvas.addEventListener('mouseleave', this.mouseleaveListener);
|
|
148
273
|
this.canvas.addEventListener('click', this.clickListener);
|
|
149
274
|
this.canvas.addEventListener('dblclick', this.dblclickListener);
|
|
150
275
|
this.canvas.addEventListener('mouseup', this.mouseupListener);
|
|
@@ -161,44 +286,23 @@ export class CanvasRenderer<TData = any> {
|
|
|
161
286
|
const container = this.canvas.parentElement;
|
|
162
287
|
if (!container) return;
|
|
163
288
|
|
|
164
|
-
const oldScrollTop = this.scrollTop;
|
|
165
|
-
const oldScrollLeft = this.scrollLeft;
|
|
166
|
-
|
|
167
289
|
this.scrollTop = container.scrollTop;
|
|
168
290
|
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
291
|
this.damageTracker.markAllDirty();
|
|
196
|
-
this.
|
|
292
|
+
this.scheduleRender();
|
|
197
293
|
}
|
|
198
294
|
|
|
199
|
-
setViewportDimensions(width: number, height: number): void {
|
|
295
|
+
setViewportDimensions(width: number, height: number, scrollbarWidth: number = 0): void {
|
|
296
|
+
if (
|
|
297
|
+
Math.abs(this.viewportWidth - width) < 1 &&
|
|
298
|
+
Math.abs(this.viewportHeight - height) < 1 &&
|
|
299
|
+
this.scrollbarWidth === scrollbarWidth
|
|
300
|
+
) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
200
303
|
this.viewportWidth = width;
|
|
201
304
|
this.viewportHeight = height;
|
|
305
|
+
this.scrollbarWidth = scrollbarWidth;
|
|
202
306
|
this.damageTracker.markAllDirty();
|
|
203
307
|
this.updateCanvasSize();
|
|
204
308
|
}
|
|
@@ -209,19 +313,13 @@ export class CanvasRenderer<TData = any> {
|
|
|
209
313
|
const width = this.viewportWidth || this.canvas.clientWidth;
|
|
210
314
|
const height = this.viewportHeight || this.canvas.clientHeight || 600;
|
|
211
315
|
|
|
316
|
+
// Set pixel buffer dimensions only. CSS sizing is handled by the stylesheet
|
|
317
|
+
// (width: 100%; height: 100%) so we never touch canvas.style.width/height here.
|
|
318
|
+
// Modifying canvas style dimensions would change layout, re-fire the
|
|
319
|
+
// ResizeObserver and create an infinite grow loop.
|
|
212
320
|
this.canvas.width = width * dpr;
|
|
213
321
|
this.canvas.height = height * dpr;
|
|
214
|
-
this.
|
|
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();
|
|
322
|
+
this.ctx?.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
225
323
|
this.scheduleRender();
|
|
226
324
|
}
|
|
227
325
|
|
|
@@ -230,10 +328,7 @@ export class CanvasRenderer<TData = any> {
|
|
|
230
328
|
if (!container) return;
|
|
231
329
|
|
|
232
330
|
const rect = container.getBoundingClientRect();
|
|
233
|
-
this.
|
|
234
|
-
this.viewportHeight = rect.height;
|
|
235
|
-
|
|
236
|
-
this.updateCanvasSize();
|
|
331
|
+
this.setViewportDimensions(rect.width, rect.height);
|
|
237
332
|
}
|
|
238
333
|
|
|
239
334
|
render(): void {
|
|
@@ -241,14 +336,28 @@ export class CanvasRenderer<TData = any> {
|
|
|
241
336
|
this.scheduleRender();
|
|
242
337
|
}
|
|
243
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Schedule a render on the next animation frame.
|
|
341
|
+
* Coalesces: if a frame is already in-flight, marks a follow-up so the next
|
|
342
|
+
* frame fires immediately after (no renders dropped, no pile-up).
|
|
343
|
+
* No-op if nothing is dirty.
|
|
344
|
+
*/
|
|
244
345
|
private scheduleRender(): void {
|
|
245
|
-
if (this.
|
|
346
|
+
if (!this.damageTracker.hasDamage()) return;
|
|
347
|
+
|
|
348
|
+
if (this.animationFrameId !== null) {
|
|
349
|
+
this.nextRenderPending = true;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
246
352
|
|
|
247
|
-
this.renderPending = true;
|
|
248
353
|
this.animationFrameId = requestAnimationFrame(() => {
|
|
249
354
|
this.doRender();
|
|
250
|
-
this.renderPending = false;
|
|
251
355
|
this.animationFrameId = null;
|
|
356
|
+
|
|
357
|
+
if (this.nextRenderPending) {
|
|
358
|
+
this.nextRenderPending = false;
|
|
359
|
+
this.scheduleRender();
|
|
360
|
+
}
|
|
252
361
|
});
|
|
253
362
|
}
|
|
254
363
|
|
|
@@ -257,23 +366,28 @@ export class CanvasRenderer<TData = any> {
|
|
|
257
366
|
}
|
|
258
367
|
|
|
259
368
|
private getVisibleColumns(): Column[] {
|
|
260
|
-
return this.gridApi.getAllColumns().filter(col => col
|
|
369
|
+
return this.gridApi.getAllColumns().filter((col) => isColumnVisible(col));
|
|
261
370
|
}
|
|
262
371
|
|
|
372
|
+
/** Build the per-column prep cache once per frame before rendering visible rows. */
|
|
263
373
|
private prepareColumns(): void {
|
|
264
|
-
const columns = this.getVisibleColumns();
|
|
265
374
|
this.columnPreps.clear();
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
375
|
+
for (const column of this.getVisibleColumns()) {
|
|
376
|
+
this.columnPreps.set(
|
|
377
|
+
column.colId,
|
|
378
|
+
prepColumn(this.ctx, column, getColumnDef(column, this.gridApi), this.theme)
|
|
379
|
+
);
|
|
270
380
|
}
|
|
271
381
|
}
|
|
272
382
|
|
|
273
383
|
private doRender(): void {
|
|
384
|
+
// Skip paint entirely if nothing has been marked dirty.
|
|
385
|
+
if (!this.damageTracker.hasDamage()) return;
|
|
386
|
+
|
|
274
387
|
const startTime = performance.now();
|
|
275
388
|
const width = this.viewportWidth || this.canvas.clientWidth;
|
|
276
389
|
const height = this.viewportHeight || this.canvas.clientHeight;
|
|
390
|
+
const availableWidth = width - this.scrollbarWidth;
|
|
277
391
|
|
|
278
392
|
// Clear canvas
|
|
279
393
|
this.ctx.clearRect(0, 0, width, height);
|
|
@@ -283,16 +397,33 @@ export class CanvasRenderer<TData = any> {
|
|
|
283
397
|
const { left: leftWidth, right: rightWidth } = getPinnedWidths(allVisibleColumns);
|
|
284
398
|
|
|
285
399
|
// Calculate visible row range
|
|
286
|
-
const totalRows = this.
|
|
400
|
+
const totalRows = this.gridApi.getDisplayedRowCount();
|
|
401
|
+
|
|
402
|
+
if (totalRows === 0) {
|
|
403
|
+
this.damageTracker.clear();
|
|
404
|
+
this.lastRenderDuration = performance.now() - startTime;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
287
408
|
const { startRow, endRow } = getVisibleRowRange(
|
|
288
409
|
this.scrollTop,
|
|
289
410
|
height,
|
|
290
|
-
this.rowHeight,
|
|
411
|
+
this.theme.rowHeight,
|
|
291
412
|
totalRows,
|
|
292
413
|
this.rowBuffer,
|
|
293
414
|
this.gridApi
|
|
294
415
|
);
|
|
295
416
|
|
|
417
|
+
// Log state periodically (not every frame to avoid flood)
|
|
418
|
+
if (Math.random() < 0.01) {
|
|
419
|
+
console.log('[ArgentGrid] doRender', {
|
|
420
|
+
viewport: { width, height },
|
|
421
|
+
rows: { total: totalRows, start: startRow, end: endRow },
|
|
422
|
+
scroll: { top: this.scrollTop, left: this.scrollLeft },
|
|
423
|
+
columns: allVisibleColumns.length,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
296
427
|
// Prepare columns (sets font, caches colDef)
|
|
297
428
|
this.prepareColumns();
|
|
298
429
|
|
|
@@ -300,33 +431,43 @@ export class CanvasRenderer<TData = any> {
|
|
|
300
431
|
this.ctx.font = getFontFromTheme(this.theme);
|
|
301
432
|
this.ctx.textBaseline = 'middle';
|
|
302
433
|
|
|
303
|
-
|
|
304
|
-
|
|
434
|
+
const positionedColumns = getPositionedColumns(
|
|
435
|
+
allVisibleColumns,
|
|
436
|
+
this.scrollLeft,
|
|
437
|
+
width,
|
|
438
|
+
leftWidth,
|
|
439
|
+
rightWidth,
|
|
440
|
+
availableWidth
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// Render all visible rows
|
|
444
|
+
walkRows(
|
|
445
|
+
startRow,
|
|
446
|
+
endRow,
|
|
447
|
+
this.scrollTop,
|
|
448
|
+
this.theme.rowHeight,
|
|
305
449
|
(rowIndex) => this.gridApi.getDisplayedRowAtIndex(rowIndex),
|
|
306
|
-
(rowIndex, y,
|
|
450
|
+
(rowIndex, y, _rowHeight, rowNode) => {
|
|
307
451
|
if (!rowNode) return;
|
|
308
|
-
this.renderRow(rowIndex, y, rowNode,
|
|
452
|
+
this.renderRow(rowIndex, y, rowNode, positionedColumns);
|
|
309
453
|
},
|
|
310
454
|
this.gridApi
|
|
311
455
|
);
|
|
312
456
|
|
|
313
457
|
// Draw grid lines
|
|
314
|
-
this.drawGridLines(
|
|
458
|
+
this.drawGridLines(positionedColumns, startRow, endRow, width, height, leftWidth, rightWidth);
|
|
315
459
|
|
|
316
460
|
// Draw range selections
|
|
317
|
-
this.drawRangeSelections(
|
|
318
|
-
|
|
319
|
-
// Store current frame for blitting
|
|
320
|
-
this.blitState.setLastCanvas(this.canvas);
|
|
461
|
+
this.drawRangeSelections(positionedColumns, leftWidth, rightWidth, width);
|
|
321
462
|
|
|
322
463
|
// Clear damage
|
|
323
464
|
this.damageTracker.clear();
|
|
324
|
-
|
|
465
|
+
|
|
325
466
|
this.lastRenderDuration = performance.now() - startTime;
|
|
326
467
|
}
|
|
327
468
|
|
|
328
469
|
private drawRangeSelections(
|
|
329
|
-
|
|
470
|
+
positionedColumns: PositionedColumn[],
|
|
330
471
|
leftPinnedWidth: number,
|
|
331
472
|
rightPinnedWidth: number,
|
|
332
473
|
viewportWidth: number
|
|
@@ -336,88 +477,53 @@ export class CanvasRenderer<TData = any> {
|
|
|
336
477
|
|
|
337
478
|
for (const range of ranges) {
|
|
338
479
|
// 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;
|
|
480
|
+
const startY = range.startRow * this.theme.rowHeight - this.scrollTop;
|
|
481
|
+
const endY = (range.endRow + 1) * this.theme.rowHeight - this.scrollTop;
|
|
347
482
|
|
|
348
483
|
let minX = Infinity;
|
|
349
484
|
let maxX = -Infinity;
|
|
350
485
|
|
|
351
486
|
// Calculate the total bounding box of all columns in the range
|
|
352
|
-
range.columns.forEach(col => {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
487
|
+
range.columns.forEach((col) => {
|
|
488
|
+
const pc = positionedColumns.find((p) => p.column.colId === col.colId);
|
|
489
|
+
if (pc) {
|
|
490
|
+
minX = Math.min(minX, pc.x);
|
|
491
|
+
maxX = Math.max(maxX, pc.x + pc.width);
|
|
492
|
+
}
|
|
356
493
|
});
|
|
357
494
|
|
|
358
495
|
if (minX === Infinity) continue;
|
|
359
496
|
|
|
360
|
-
drawRangeSelectionBorder(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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;
|
|
497
|
+
drawRangeSelectionBorder(
|
|
498
|
+
this.ctx,
|
|
499
|
+
{
|
|
500
|
+
x: minX,
|
|
501
|
+
y: startY,
|
|
502
|
+
width: maxX - minX,
|
|
503
|
+
height: endY - startY,
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
color: '#2196f3', // Strong blue border (Material Blue)
|
|
507
|
+
fillColor: 'rgba(33, 150, 243, 0.25)', // 25% blue tint
|
|
508
|
+
lineWidth: 2,
|
|
400
509
|
}
|
|
401
|
-
|
|
510
|
+
);
|
|
402
511
|
}
|
|
403
|
-
return 0;
|
|
404
512
|
}
|
|
405
513
|
|
|
406
514
|
private renderRow(
|
|
407
515
|
rowIndex: number,
|
|
408
516
|
y: number,
|
|
409
517
|
rowNode: IRowNode<TData>,
|
|
410
|
-
|
|
411
|
-
viewportWidth: number,
|
|
412
|
-
leftWidth: number,
|
|
413
|
-
rightWidth: number
|
|
518
|
+
positionedColumns: PositionedColumn[]
|
|
414
519
|
): void {
|
|
415
520
|
if (rowNode.detail) {
|
|
416
|
-
this.renderDetailRow(rowIndex, y, rowNode, viewportWidth);
|
|
521
|
+
this.renderDetailRow(rowIndex, y, rowNode, this.viewportWidth);
|
|
417
522
|
return;
|
|
418
523
|
}
|
|
419
524
|
|
|
420
525
|
const isEvenRow = rowIndex % 2 === 0;
|
|
526
|
+
const rowHeight = rowNode.rowHeight || this.theme.rowHeight;
|
|
421
527
|
|
|
422
528
|
// Draw row background
|
|
423
529
|
let bgColor = isEvenRow ? this.theme.bgCellEven : this.theme.bgCell;
|
|
@@ -426,41 +532,23 @@ export class CanvasRenderer<TData = any> {
|
|
|
426
532
|
}
|
|
427
533
|
|
|
428
534
|
this.ctx.fillStyle = bgColor;
|
|
429
|
-
|
|
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
|
-
}
|
|
535
|
+
// Fill background for the entire available width
|
|
536
|
+
this.ctx.fillRect(0, Math.floor(y), this.viewportWidth - this.scrollbarWidth, rowHeight);
|
|
450
537
|
|
|
451
|
-
// Render
|
|
452
|
-
|
|
453
|
-
|
|
538
|
+
// Render columns using pre-calculated positions
|
|
539
|
+
for (const pc of positionedColumns) {
|
|
540
|
+
this.renderCell(pc.column, pc.x, y, pc.width, rowNode, positionedColumns);
|
|
541
|
+
}
|
|
454
542
|
}
|
|
455
543
|
|
|
456
544
|
private renderDetailRow(
|
|
457
|
-
|
|
545
|
+
_rowIndex: number,
|
|
458
546
|
y: number,
|
|
459
547
|
rowNode: IRowNode<TData>,
|
|
460
548
|
viewportWidth: number
|
|
461
549
|
): void {
|
|
462
550
|
const rowHeight = rowNode.rowHeight || 200;
|
|
463
|
-
|
|
551
|
+
|
|
464
552
|
// Draw detail background
|
|
465
553
|
this.ctx.fillStyle = '#f0f0f0';
|
|
466
554
|
this.ctx.fillRect(0, Math.floor(y), viewportWidth, rowHeight);
|
|
@@ -473,199 +561,53 @@ export class CanvasRenderer<TData = any> {
|
|
|
473
561
|
Math.floor(this.theme.cellPadding * 4),
|
|
474
562
|
Math.floor(y + rowHeight / 2)
|
|
475
563
|
);
|
|
476
|
-
|
|
564
|
+
|
|
477
565
|
// Reset font
|
|
478
566
|
this.ctx.font = getFontFromTheme(this.theme);
|
|
479
567
|
}
|
|
480
568
|
|
|
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
569
|
private renderCell(
|
|
510
570
|
column: Column,
|
|
511
571
|
x: number,
|
|
512
572
|
y: number,
|
|
513
573
|
width: number,
|
|
514
574
|
rowNode: IRowNode<TData>,
|
|
515
|
-
|
|
575
|
+
positionedColumns: PositionedColumn[]
|
|
516
576
|
): void {
|
|
517
577
|
const prep = this.columnPreps.get(column.colId);
|
|
518
578
|
if (!prep) return;
|
|
519
579
|
|
|
520
|
-
const
|
|
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
|
-
|
|
580
|
+
const value = getCellValue(column, prep.colDef, rowNode, this.gridApi);
|
|
527
581
|
const formattedValue = getFormattedValue(
|
|
528
|
-
|
|
582
|
+
value,
|
|
529
583
|
prep.colDef,
|
|
530
584
|
rowNode.data,
|
|
531
585
|
rowNode,
|
|
532
586
|
this.gridApi
|
|
533
587
|
);
|
|
534
588
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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,
|
|
589
|
+
drawCell(this.ctx, prep, {
|
|
590
|
+
ctx: this.ctx,
|
|
591
|
+
theme: this.theme,
|
|
592
|
+
column,
|
|
593
|
+
colDef: prep.colDef,
|
|
594
|
+
rowNode,
|
|
595
|
+
rowIndex: rowNode.displayedRowIndex,
|
|
596
|
+
x,
|
|
597
|
+
y,
|
|
598
|
+
width,
|
|
599
|
+
height: rowNode.rowHeight || this.theme.rowHeight,
|
|
600
|
+
value,
|
|
558
601
|
formattedValue,
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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();
|
|
602
|
+
isSelected: rowNode.selected,
|
|
603
|
+
isHovered: false, // TODO: Implement hover
|
|
604
|
+
isEvenRow: rowNode.displayedRowIndex % 2 === 0,
|
|
605
|
+
api: this.gridApi,
|
|
606
|
+
});
|
|
665
607
|
}
|
|
666
608
|
|
|
667
609
|
private drawGridLines(
|
|
668
|
-
|
|
610
|
+
positionedColumns: PositionedColumn[],
|
|
669
611
|
startRow: number,
|
|
670
612
|
endRow: number,
|
|
671
613
|
viewportWidth: number,
|
|
@@ -678,16 +620,17 @@ export class CanvasRenderer<TData = any> {
|
|
|
678
620
|
this.ctx,
|
|
679
621
|
startRow,
|
|
680
622
|
endRow,
|
|
681
|
-
this.rowHeight,
|
|
623
|
+
this.theme.rowHeight,
|
|
682
624
|
this.scrollTop,
|
|
683
|
-
viewportWidth,
|
|
684
|
-
this.theme
|
|
625
|
+
viewportWidth - this.scrollbarWidth,
|
|
626
|
+
this.theme,
|
|
627
|
+
this.gridApi
|
|
685
628
|
);
|
|
686
629
|
|
|
687
630
|
// Draw vertical column lines
|
|
688
631
|
drawColumnLines(
|
|
689
632
|
this.ctx,
|
|
690
|
-
|
|
633
|
+
this.getVisibleColumns(),
|
|
691
634
|
this.scrollLeft,
|
|
692
635
|
this.scrollTop,
|
|
693
636
|
viewportWidth,
|
|
@@ -697,7 +640,9 @@ export class CanvasRenderer<TData = any> {
|
|
|
697
640
|
this.theme,
|
|
698
641
|
startRow,
|
|
699
642
|
endRow,
|
|
700
|
-
this.rowHeight
|
|
643
|
+
this.theme.rowHeight,
|
|
644
|
+
this.gridApi,
|
|
645
|
+
viewportWidth - this.scrollbarWidth
|
|
701
646
|
);
|
|
702
647
|
}
|
|
703
648
|
|
|
@@ -706,7 +651,17 @@ export class CanvasRenderer<TData = any> {
|
|
|
706
651
|
// ============================================================================
|
|
707
652
|
|
|
708
653
|
private handleMouseDown(event: MouseEvent): void {
|
|
709
|
-
const
|
|
654
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
655
|
+
const { rowIndex, columnIndex } = performHitTest(
|
|
656
|
+
event.clientX - rect.left,
|
|
657
|
+
event.clientY - rect.top,
|
|
658
|
+
this.theme.rowHeight,
|
|
659
|
+
this.scrollTop,
|
|
660
|
+
this.scrollLeft,
|
|
661
|
+
this.viewportWidth,
|
|
662
|
+
this.getVisibleColumns(),
|
|
663
|
+
this.viewportWidth - this.scrollbarWidth
|
|
664
|
+
);
|
|
710
665
|
const columns = this.getVisibleColumns();
|
|
711
666
|
const colId = columnIndex !== -1 ? columns[columnIndex].colId : null;
|
|
712
667
|
|
|
@@ -716,39 +671,28 @@ export class CanvasRenderer<TData = any> {
|
|
|
716
671
|
|
|
717
672
|
const rowNode = this.gridApi.getDisplayedRowAtIndex(rowIndex);
|
|
718
673
|
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();
|
|
674
|
+
// Selection logic moved to handleClick to prevent double-toggling with onRowClick/DOM events
|
|
745
675
|
}
|
|
746
676
|
|
|
747
677
|
private handleMouseMove(event: MouseEvent): void {
|
|
748
|
-
const
|
|
678
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
679
|
+
const { rowIndex, columnIndex } = performHitTest(
|
|
680
|
+
event.clientX - rect.left,
|
|
681
|
+
event.clientY - rect.top,
|
|
682
|
+
this.theme.rowHeight,
|
|
683
|
+
this.scrollTop,
|
|
684
|
+
this.scrollLeft,
|
|
685
|
+
this.viewportWidth,
|
|
686
|
+
this.getVisibleColumns(),
|
|
687
|
+
this.viewportWidth - this.scrollbarWidth
|
|
688
|
+
);
|
|
749
689
|
const columns = this.getVisibleColumns();
|
|
750
690
|
const colId = columnIndex !== -1 ? columns[columnIndex].colId : null;
|
|
751
691
|
|
|
692
|
+
// Update cursor: pointer for button cells, default otherwise
|
|
693
|
+
const hoveredColDef = colId ? this.columnPreps.get(colId)?.colDef : null;
|
|
694
|
+
this.canvas.style.cursor = hoveredColDef?.buttonOptions ? 'pointer' : '';
|
|
695
|
+
|
|
752
696
|
if (this.onMouseMove) {
|
|
753
697
|
this.onMouseMove(event, rowIndex, colId);
|
|
754
698
|
}
|
|
@@ -756,7 +700,17 @@ export class CanvasRenderer<TData = any> {
|
|
|
756
700
|
}
|
|
757
701
|
|
|
758
702
|
private handleMouseUp(event: MouseEvent): void {
|
|
759
|
-
const
|
|
703
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
704
|
+
const { rowIndex, columnIndex } = performHitTest(
|
|
705
|
+
event.clientX - rect.left,
|
|
706
|
+
event.clientY - rect.top,
|
|
707
|
+
this.theme.rowHeight,
|
|
708
|
+
this.scrollTop,
|
|
709
|
+
this.scrollLeft,
|
|
710
|
+
this.viewportWidth,
|
|
711
|
+
this.getVisibleColumns(),
|
|
712
|
+
this.viewportWidth - this.scrollbarWidth
|
|
713
|
+
);
|
|
760
714
|
const columns = this.getVisibleColumns();
|
|
761
715
|
const colId = columnIndex !== -1 ? columns[columnIndex].colId : null;
|
|
762
716
|
|
|
@@ -766,39 +720,86 @@ export class CanvasRenderer<TData = any> {
|
|
|
766
720
|
}
|
|
767
721
|
|
|
768
722
|
private handleClick(event: MouseEvent): void {
|
|
769
|
-
const
|
|
723
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
724
|
+
const { rowIndex, columnIndex } = performHitTest(
|
|
725
|
+
event.clientX - rect.left,
|
|
726
|
+
event.clientY - rect.top,
|
|
727
|
+
this.theme.rowHeight,
|
|
728
|
+
this.scrollTop,
|
|
729
|
+
this.scrollLeft,
|
|
730
|
+
this.viewportWidth,
|
|
731
|
+
this.getVisibleColumns(),
|
|
732
|
+
this.viewportWidth - this.scrollbarWidth
|
|
733
|
+
);
|
|
770
734
|
const rowNode = this.gridApi.getDisplayedRowAtIndex(rowIndex);
|
|
771
735
|
if (!rowNode) return;
|
|
772
736
|
|
|
737
|
+
// Handle selection column or explicit checkbox renderer
|
|
738
|
+
const columns = this.getVisibleColumns();
|
|
739
|
+
const clickedCol = columnIndex !== -1 ? columns[columnIndex] : null;
|
|
740
|
+
const clickedColDef = clickedCol ? this.columnPreps.get(clickedCol.colId)?.colDef : null;
|
|
741
|
+
|
|
742
|
+
if (
|
|
743
|
+
clickedCol?.colId === 'ag-Grid-SelectionColumn' ||
|
|
744
|
+
clickedColDef?.cellRenderer === 'checkbox'
|
|
745
|
+
) {
|
|
746
|
+
rowNode.setSelected(!rowNode.selected);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Handle button cell — fire onClick and stop propagation to row click
|
|
751
|
+
if (clickedCol) {
|
|
752
|
+
const colDef = this.columnPreps.get(clickedCol.colId)?.colDef;
|
|
753
|
+
if (colDef?.buttonOptions?.onClick) {
|
|
754
|
+
colDef.buttonOptions.onClick({
|
|
755
|
+
value: clickedCol.field ? getValueByPath(rowNode.data, clickedCol.field) : undefined,
|
|
756
|
+
data: rowNode.data,
|
|
757
|
+
node: rowNode,
|
|
758
|
+
colDef,
|
|
759
|
+
api: this.gridApi,
|
|
760
|
+
event,
|
|
761
|
+
});
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
773
766
|
// Handle expand/collapse
|
|
774
767
|
if ((rowNode.group || rowNode.master) && columnIndex !== -1) {
|
|
775
768
|
const columns = this.getVisibleColumns();
|
|
776
769
|
const clickedCol = columns[columnIndex];
|
|
777
770
|
|
|
778
771
|
const isAutoGroupCol = clickedCol.colId === 'ag-Grid-AutoColumn';
|
|
779
|
-
const isFirstColIfNoAutoGroup =
|
|
772
|
+
const isFirstColIfNoAutoGroup =
|
|
773
|
+
!columns.some((c) => c.colId === 'ag-Grid-AutoColumn') && columnIndex === 0;
|
|
780
774
|
|
|
781
775
|
if (isAutoGroupCol || isFirstColIfNoAutoGroup) {
|
|
782
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
783
776
|
const x = event.clientX - rect.left;
|
|
784
|
-
const { left: leftWidth
|
|
777
|
+
const { left: leftWidth } = getPinnedWidths(columns);
|
|
785
778
|
|
|
786
779
|
let colX = 0;
|
|
787
780
|
if (clickedCol.pinned === 'left') {
|
|
788
|
-
const leftPinned = columns.filter(c => c.pinned === 'left');
|
|
789
781
|
for (let i = 0; i < columns.indexOf(clickedCol); i++) {
|
|
790
782
|
if (columns[i].pinned === 'left') colX += columns[i].width;
|
|
791
783
|
}
|
|
792
784
|
} else if (clickedCol.pinned === 'right') {
|
|
793
|
-
colX =
|
|
785
|
+
colX =
|
|
786
|
+
this.viewportWidth -
|
|
787
|
+
columns.filter((c) => c.pinned === 'right').reduce((sum, c) => sum + c.width, 0);
|
|
794
788
|
} else {
|
|
795
|
-
colX = leftWidth +
|
|
789
|
+
colX = leftWidth + getCenterColumnOffset(clickedCol, columns) - this.scrollLeft;
|
|
796
790
|
}
|
|
797
791
|
|
|
798
792
|
const indent = rowNode.level * this.theme.groupIndentWidth;
|
|
799
|
-
|
|
793
|
+
let textX = colX + this.theme.cellPadding;
|
|
794
|
+
|
|
795
|
+
// Account for dedicated selection column if clicked directly on it
|
|
796
|
+
if (clickedCol.colId === 'ag-Grid-SelectionColumn') {
|
|
797
|
+
textX += clickedCol.width;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const indicatorAreaEnd = textX + indent + this.theme.groupIndicatorSize + 3;
|
|
800
801
|
|
|
801
|
-
if (x >=
|
|
802
|
+
if (x >= textX + indent && x < indicatorAreaEnd) {
|
|
802
803
|
this.gridApi.setRowNodeExpanded(rowNode, !rowNode.expanded);
|
|
803
804
|
this.damageTracker.markAllDirty(); // Group expansion affects many rows
|
|
804
805
|
this.render();
|
|
@@ -813,7 +814,17 @@ export class CanvasRenderer<TData = any> {
|
|
|
813
814
|
}
|
|
814
815
|
|
|
815
816
|
private handleDoubleClick(event: MouseEvent): void {
|
|
816
|
-
const
|
|
817
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
818
|
+
const { rowIndex, columnIndex } = performHitTest(
|
|
819
|
+
event.clientX - rect.left,
|
|
820
|
+
event.clientY - rect.top,
|
|
821
|
+
this.theme.rowHeight,
|
|
822
|
+
this.scrollTop,
|
|
823
|
+
this.scrollLeft,
|
|
824
|
+
this.viewportWidth,
|
|
825
|
+
this.getVisibleColumns(),
|
|
826
|
+
this.viewportWidth - this.scrollbarWidth
|
|
827
|
+
);
|
|
817
828
|
if (columnIndex === -1) return;
|
|
818
829
|
|
|
819
830
|
const rowNode = this.gridApi.getDisplayedRowAtIndex(rowIndex);
|
|
@@ -829,52 +840,15 @@ export class CanvasRenderer<TData = any> {
|
|
|
829
840
|
|
|
830
841
|
getHitTestResult(event: MouseEvent): { rowIndex: number; columnIndex: number } {
|
|
831
842
|
const rect = this.canvas.getBoundingClientRect();
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
// Use walker utility for column detection
|
|
839
|
-
const result = getColumnAtX(
|
|
840
|
-
this.getVisibleColumns(),
|
|
841
|
-
canvasX,
|
|
843
|
+
return performHitTest(
|
|
844
|
+
event.clientX - rect.left,
|
|
845
|
+
event.clientY - rect.top,
|
|
846
|
+
this.theme.rowHeight,
|
|
847
|
+
this.scrollTop,
|
|
842
848
|
this.scrollLeft,
|
|
843
|
-
this.viewportWidth
|
|
849
|
+
this.viewportWidth,
|
|
850
|
+
this.getVisibleColumns()
|
|
844
851
|
);
|
|
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
852
|
}
|
|
879
853
|
|
|
880
854
|
// ============================================================================
|
|
@@ -885,7 +859,7 @@ export class CanvasRenderer<TData = any> {
|
|
|
885
859
|
const container = this.canvas.parentElement;
|
|
886
860
|
if (!container) return;
|
|
887
861
|
|
|
888
|
-
const targetPosition = rowIndex * this.rowHeight;
|
|
862
|
+
const targetPosition = rowIndex * this.theme.rowHeight;
|
|
889
863
|
container.scrollTop = targetPosition;
|
|
890
864
|
this.scrollTop = targetPosition;
|
|
891
865
|
this.damageTracker.markAllDirty();
|
|
@@ -934,29 +908,59 @@ export class CanvasRenderer<TData = any> {
|
|
|
934
908
|
this.scheduleRender();
|
|
935
909
|
}
|
|
936
910
|
|
|
911
|
+
/**
|
|
912
|
+
* Get column at x position
|
|
913
|
+
*/
|
|
914
|
+
getColumnAtPosition(x: number): number {
|
|
915
|
+
const columns = this.gridApi.getAllColumns();
|
|
916
|
+
let currentX = 0;
|
|
917
|
+
for (let i = 0; i < columns.length; i++) {
|
|
918
|
+
const col = columns[i];
|
|
919
|
+
const width = col.width || 150;
|
|
920
|
+
if (x >= currentX && x < currentX + width) {
|
|
921
|
+
return i;
|
|
922
|
+
}
|
|
923
|
+
currentX += width;
|
|
924
|
+
}
|
|
925
|
+
return -1;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Get row at y position
|
|
930
|
+
*/
|
|
931
|
+
getRowAtPosition(y: number): number {
|
|
932
|
+
const scrollTop = this.scrollTop || 0;
|
|
933
|
+
const rowY = y + scrollTop;
|
|
934
|
+
return Math.floor(rowY / this.theme.rowHeight);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
937
|
destroy(): void {
|
|
938
938
|
if (this.animationFrameId) {
|
|
939
939
|
cancelAnimationFrame(this.animationFrameId);
|
|
940
940
|
}
|
|
941
|
-
|
|
941
|
+
|
|
942
942
|
// Remove event listeners
|
|
943
943
|
const container = this.canvas.parentElement;
|
|
944
944
|
if (container && this.scrollListener) {
|
|
945
945
|
container.removeEventListener('scroll', this.scrollListener);
|
|
946
946
|
}
|
|
947
|
-
|
|
948
|
-
if (this.mousedownListener)
|
|
949
|
-
|
|
947
|
+
|
|
948
|
+
if (this.mousedownListener)
|
|
949
|
+
this.canvas.removeEventListener('mousedown', this.mousedownListener);
|
|
950
|
+
if (this.mousemoveListener)
|
|
951
|
+
this.canvas.removeEventListener('mousemove', this.mousemoveListener);
|
|
952
|
+
if (this.mouseleaveListener)
|
|
953
|
+
this.canvas.removeEventListener('mouseleave', this.mouseleaveListener);
|
|
950
954
|
if (this.clickListener) this.canvas.removeEventListener('click', this.clickListener);
|
|
951
955
|
if (this.dblclickListener) this.canvas.removeEventListener('dblclick', this.dblclickListener);
|
|
952
956
|
if (this.mouseupListener) this.canvas.removeEventListener('mouseup', this.mouseupListener);
|
|
953
|
-
|
|
957
|
+
|
|
954
958
|
if (this.resizeListener) {
|
|
955
959
|
window.removeEventListener('resize', this.resizeListener);
|
|
956
960
|
}
|
|
957
961
|
|
|
958
|
-
this.
|
|
959
|
-
this.
|
|
962
|
+
this.nextRenderPending = false;
|
|
963
|
+
this.animationFrameId = null;
|
|
960
964
|
this.damageTracker.reset();
|
|
961
965
|
}
|
|
962
966
|
}
|