draw-table-vue 0.2.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/README.md +125 -40
- package/dist/assets/index-BoSk-1mt.css +1 -0
- package/dist/assets/index-C5vPZIAB.js +1 -0
- package/{index.html → dist/index.html} +2 -1
- package/dist/package.json +24 -0
- package/package.json +15 -1
- package/.vscode/extensions.json +0 -3
- package/docs/DESIGN.md +0 -38
- package/examples/BasicUsage.vue +0 -116
- package/pnpm-lock.yaml +0 -952
- package/src/App.vue +0 -26
- package/src/main.ts +0 -5
- package/src/style.css +0 -79
- package/src/table/components/CanvasTable.vue +0 -659
- package/src/table/core/renderer.ts +0 -948
- package/src/table/index.ts +0 -3
- package/src/table/types/index.ts +0 -77
- package/tsconfig.app.json +0 -16
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -26
- package/vite.config.ts +0 -7
- /package/{public → dist}/vite.svg +0 -0
|
@@ -1,948 +0,0 @@
|
|
|
1
|
-
import type { ColumnConfig, TableRow, TableOptions, CellInfo } from '../types';
|
|
2
|
-
|
|
3
|
-
export class CanvasRenderer {
|
|
4
|
-
private ctx: CanvasRenderingContext2D;
|
|
5
|
-
private canvas: HTMLCanvasElement;
|
|
6
|
-
private columns: ColumnConfig[] = [];
|
|
7
|
-
private data: TableRow[] = [];
|
|
8
|
-
private options: Required<TableOptions> = {
|
|
9
|
-
rowHeight: 40,
|
|
10
|
-
headerHeight: 48,
|
|
11
|
-
border: true,
|
|
12
|
-
stripe: true,
|
|
13
|
-
multiSelect: false,
|
|
14
|
-
fixedHeader: true,
|
|
15
|
-
renderExpand: () => (null as any),
|
|
16
|
-
spanMethod: () => undefined
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
private scrollX = 0;
|
|
20
|
-
private scrollY = 0;
|
|
21
|
-
private width = 0;
|
|
22
|
-
private height = 0;
|
|
23
|
-
|
|
24
|
-
// Layout info
|
|
25
|
-
private columnPositions: number[] = [];
|
|
26
|
-
private rowOffsets: number[] = [];
|
|
27
|
-
private totalWidth = 0;
|
|
28
|
-
private totalHeight = 0;
|
|
29
|
-
private fixedLeftWidth = 0;
|
|
30
|
-
private summaryRows: any[][] = [];
|
|
31
|
-
private expandedRowKeys = new Set<string | number>();
|
|
32
|
-
private selectedRowKeys = new Set<string | number>();
|
|
33
|
-
private imageCache: Map<string, HTMLImageElement> = new Map();
|
|
34
|
-
private hoverHeaderIndex: number = -1;
|
|
35
|
-
private flattenedColumns: ColumnConfig[] = [];
|
|
36
|
-
private columnLevels = 0;
|
|
37
|
-
|
|
38
|
-
constructor(canvas: HTMLCanvasElement) {
|
|
39
|
-
this.canvas = canvas;
|
|
40
|
-
const ctx = canvas.getContext('2d');
|
|
41
|
-
if (!ctx) throw new Error('Could not get canvas context');
|
|
42
|
-
this.ctx = ctx;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
public destroy() {
|
|
46
|
-
this.imageCache.forEach(img => {
|
|
47
|
-
img.onload = null;
|
|
48
|
-
img.onerror = null;
|
|
49
|
-
img.src = '';
|
|
50
|
-
});
|
|
51
|
-
this.imageCache.clear();
|
|
52
|
-
this.data = [];
|
|
53
|
-
this.columns = [];
|
|
54
|
-
this.summaryRows = [];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
public setExpandedRows(keys: (string | number)[]) {
|
|
58
|
-
this.expandedRowKeys = new Set(keys);
|
|
59
|
-
this.calculateLayout();
|
|
60
|
-
this.render();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
public setData(data: TableRow[]) {
|
|
64
|
-
this.data = data;
|
|
65
|
-
this.calculateLayout();
|
|
66
|
-
this.render();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
public setColumns(columns: ColumnConfig[]) {
|
|
70
|
-
this.columns = columns;
|
|
71
|
-
this.flattenColumns();
|
|
72
|
-
this.calculateLayout();
|
|
73
|
-
this.render();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private flattenColumns() {
|
|
77
|
-
this.flattenedColumns = [];
|
|
78
|
-
this.columnLevels = 0;
|
|
79
|
-
const flatten = (cols: ColumnConfig[], level = 0) => {
|
|
80
|
-
this.columnLevels = Math.max(this.columnLevels, level + 1);
|
|
81
|
-
cols.forEach(col => {
|
|
82
|
-
if (col.children && col.children.length > 0) {
|
|
83
|
-
flatten(col.children, level + 1);
|
|
84
|
-
} else {
|
|
85
|
-
this.flattenedColumns.push(col);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
};
|
|
89
|
-
flatten(this.columns);
|
|
90
|
-
|
|
91
|
-
// Update header height if levels > 1
|
|
92
|
-
const baseHeaderHeight = 40;
|
|
93
|
-
if (this.columnLevels > 1) {
|
|
94
|
-
this.options.headerHeight = this.columnLevels * baseHeaderHeight;
|
|
95
|
-
} else {
|
|
96
|
-
this.options.headerHeight = 48;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
public setOptions(options: Partial<TableOptions>) {
|
|
101
|
-
this.options = { ...this.options, ...options };
|
|
102
|
-
this.calculateLayout();
|
|
103
|
-
this.render();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
public resize(width: number, height: number) {
|
|
107
|
-
this.width = width;
|
|
108
|
-
this.height = height;
|
|
109
|
-
const dpr = window.devicePixelRatio || 1;
|
|
110
|
-
this.canvas.width = width * dpr;
|
|
111
|
-
this.canvas.height = height * dpr;
|
|
112
|
-
this.canvas.style.width = `${width}px`;
|
|
113
|
-
this.canvas.style.height = `${height}px`;
|
|
114
|
-
this.ctx.resetTransform();
|
|
115
|
-
this.ctx.scale(dpr, dpr);
|
|
116
|
-
this.render();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
public scrollTo(x: number, y: number) {
|
|
120
|
-
this.scrollX = x;
|
|
121
|
-
this.scrollY = y;
|
|
122
|
-
this.render();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private calculateLayout() {
|
|
126
|
-
let currentX = 0;
|
|
127
|
-
this.columnPositions = [0];
|
|
128
|
-
let fixedWidth = 0;
|
|
129
|
-
this.flattenedColumns.forEach((col) => {
|
|
130
|
-
const w = col.width || 100;
|
|
131
|
-
currentX += w;
|
|
132
|
-
this.columnPositions.push(currentX);
|
|
133
|
-
if (col.fixed === 'left' || col.fixed === true) {
|
|
134
|
-
fixedWidth = currentX;
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
this.totalWidth = currentX;
|
|
138
|
-
this.fixedLeftWidth = fixedWidth;
|
|
139
|
-
|
|
140
|
-
// Calculate row offsets for expanded rows
|
|
141
|
-
let currentY = 0;
|
|
142
|
-
this.rowOffsets = [0];
|
|
143
|
-
const { rowHeight } = this.options;
|
|
144
|
-
this.data.forEach(row => {
|
|
145
|
-
currentY += rowHeight;
|
|
146
|
-
if (this.expandedRowKeys.has(row.id)) {
|
|
147
|
-
currentY += rowHeight * 3; // Fixed expand height for demo
|
|
148
|
-
}
|
|
149
|
-
this.rowOffsets.push(currentY);
|
|
150
|
-
});
|
|
151
|
-
this.totalHeight = currentY;
|
|
152
|
-
|
|
153
|
-
this.calculateSummaries();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
private getRowRange(rowspanOffset = 20) {
|
|
157
|
-
const { headerHeight } = this.options;
|
|
158
|
-
const summaryHeight = this.summaryRows.length * this.options.rowHeight;
|
|
159
|
-
|
|
160
|
-
// Visible body height is total canvas height minus header and summary rows
|
|
161
|
-
const visibleBodyHeight = this.height - headerHeight - summaryHeight;
|
|
162
|
-
|
|
163
|
-
const startY = this.scrollY;
|
|
164
|
-
const endY = startY + visibleBodyHeight;
|
|
165
|
-
|
|
166
|
-
let startRow = 0;
|
|
167
|
-
while (startRow < this.rowOffsets.length && (this.rowOffsets[startRow + 1] ?? 0) < startY) {
|
|
168
|
-
startRow++;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
let endRow = startRow;
|
|
172
|
-
while (endRow < this.rowOffsets.length && (this.rowOffsets[endRow] ?? 0) < endY) {
|
|
173
|
-
endRow++;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
startRow: Math.max(0, startRow - rowspanOffset),
|
|
178
|
-
endRow: Math.min(this.data.length, endRow + 1)
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
private calculateSummaries() {
|
|
183
|
-
this.summaryRows = [];
|
|
184
|
-
const rowsByLabel = new Map<string, any[]>();
|
|
185
|
-
|
|
186
|
-
this.flattenedColumns.forEach((col, j) => {
|
|
187
|
-
if (!col.summary) return;
|
|
188
|
-
|
|
189
|
-
col.summary.forEach(summaryFn => {
|
|
190
|
-
try {
|
|
191
|
-
const result = summaryFn(this.data);
|
|
192
|
-
if (result && result.label) {
|
|
193
|
-
const { label, value } = result;
|
|
194
|
-
if (!rowsByLabel.has(label)) {
|
|
195
|
-
rowsByLabel.set(label, new Array(this.flattenedColumns.length).fill(''));
|
|
196
|
-
}
|
|
197
|
-
rowsByLabel.get(label)![j] = value;
|
|
198
|
-
}
|
|
199
|
-
} catch (e) {
|
|
200
|
-
console.error('Summary calculation error:', e);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Convert to array and set the first column as label
|
|
206
|
-
rowsByLabel.forEach((rowData, label) => {
|
|
207
|
-
rowData[0] = label;
|
|
208
|
-
this.summaryRows.push(rowData);
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
render() {
|
|
213
|
-
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
214
|
-
|
|
215
|
-
// 1. Draw Body (scrolling part)
|
|
216
|
-
this.drawBody();
|
|
217
|
-
|
|
218
|
-
// 2. Draw Fixed Left Body
|
|
219
|
-
this.drawFixedLeftBody();
|
|
220
|
-
|
|
221
|
-
// 3. Draw Header
|
|
222
|
-
this.drawHeader();
|
|
223
|
-
|
|
224
|
-
// 4. Draw Summary Rows (Fixed at bottom)
|
|
225
|
-
this.drawSummaryRows();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private getColumnWidth(col: ColumnConfig): number {
|
|
229
|
-
if (col.children && col.children.length > 0) {
|
|
230
|
-
const width = col.children.reduce((acc, child) => acc + this.getColumnWidth(child), 0);
|
|
231
|
-
return width;
|
|
232
|
-
}
|
|
233
|
-
return col.width || 100;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
private isColumnFixed(col: ColumnConfig): boolean {
|
|
237
|
-
if (col.fixed === 'left' || col.fixed === 'right' || col.fixed === true) return true;
|
|
238
|
-
if (col.children && col.children.length > 0) {
|
|
239
|
-
return col.children.some(child => this.isColumnFixed(child));
|
|
240
|
-
}
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private drawHeader() {
|
|
245
|
-
const { headerHeight } = this.options;
|
|
246
|
-
const baseHeaderHeight = this.columnLevels > 1 ? 40 : headerHeight;
|
|
247
|
-
|
|
248
|
-
this.ctx.save();
|
|
249
|
-
this.ctx.fillStyle = '#f5f7fa';
|
|
250
|
-
this.ctx.fillRect(0, 0, this.width, headerHeight);
|
|
251
|
-
|
|
252
|
-
const drawHeaderRecursively = (cols: ColumnConfig[], x: number, y: number, level: number, isFixedPass: boolean) => {
|
|
253
|
-
let currentX = x;
|
|
254
|
-
cols.forEach((col) => {
|
|
255
|
-
const w = this.getColumnWidth(col);
|
|
256
|
-
const isFixedCol = this.isColumnFixed(col);
|
|
257
|
-
|
|
258
|
-
// Skip based on pass
|
|
259
|
-
if (isFixedPass !== isFixedCol) {
|
|
260
|
-
currentX += w;
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const isLeaf = !col.children || col.children.length === 0;
|
|
265
|
-
const h = isLeaf ? (this.columnLevels - level) * baseHeaderHeight : baseHeaderHeight;
|
|
266
|
-
const drawX = isFixedPass ? currentX : currentX - this.scrollX;
|
|
267
|
-
|
|
268
|
-
// Only draw if within bounds or fixed
|
|
269
|
-
const shouldDraw = isFixedPass || (drawX + w > this.fixedLeftWidth && drawX < this.width);
|
|
270
|
-
|
|
271
|
-
if (shouldDraw) {
|
|
272
|
-
if (!isFixedPass) {
|
|
273
|
-
this.ctx.save();
|
|
274
|
-
this.ctx.beginPath();
|
|
275
|
-
this.ctx.rect(this.fixedLeftWidth, 0, this.width - this.fixedLeftWidth, headerHeight);
|
|
276
|
-
this.ctx.clip();
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
this.drawHeaderCell(col, drawX, w, h, y, isLeaf);
|
|
280
|
-
|
|
281
|
-
if (!isFixedPass) {
|
|
282
|
-
this.ctx.restore();
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (col.children && col.children.length > 0) {
|
|
287
|
-
drawHeaderRecursively(col.children, currentX, y + baseHeaderHeight, level + 1, isFixedPass);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
currentX += w;
|
|
291
|
-
});
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
// Draw scrolling headers first
|
|
295
|
-
drawHeaderRecursively(this.columns, 0, 0, 0, false);
|
|
296
|
-
// Draw fixed headers on top
|
|
297
|
-
drawHeaderRecursively(this.columns, 0, 0, 0, true);
|
|
298
|
-
|
|
299
|
-
this.ctx.restore();
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
public setHoverHeader(index: number) {
|
|
303
|
-
if (this.hoverHeaderIndex !== index) {
|
|
304
|
-
this.hoverHeaderIndex = index;
|
|
305
|
-
this.render();
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
public setSelectedRows(keys: (string | number)[]) {
|
|
310
|
-
this.selectedRowKeys = new Set(keys);
|
|
311
|
-
this.render();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
private drawHeaderCell(col: ColumnConfig, x: number, w: number, h: number, y: number, isLeaf: boolean) {
|
|
315
|
-
this.ctx.save();
|
|
316
|
-
|
|
317
|
-
// Clip to cell bounds to prevent text overflow
|
|
318
|
-
this.ctx.beginPath();
|
|
319
|
-
this.ctx.rect(x, y, w, h);
|
|
320
|
-
this.ctx.clip();
|
|
321
|
-
|
|
322
|
-
// Cell background
|
|
323
|
-
this.ctx.fillStyle = '#f5f7fa';
|
|
324
|
-
this.ctx.fillRect(x, y, w, h);
|
|
325
|
-
|
|
326
|
-
if (this.options.border) {
|
|
327
|
-
this.ctx.strokeStyle = '#ebeef5';
|
|
328
|
-
this.ctx.lineWidth = 1;
|
|
329
|
-
this.ctx.strokeRect(x + 0.5, y + 0.5, w, h);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if ((col.type as any) === 'selection') {
|
|
333
|
-
const allSelected = this.data.length > 0 && this.selectedRowKeys.size === this.data.length;
|
|
334
|
-
this.drawCheckboxCell(allSelected, x, y, w, h, 'center');
|
|
335
|
-
} else {
|
|
336
|
-
// Use custom renderer if provided (Canvas fallback)
|
|
337
|
-
this.ctx.fillStyle = '#303133';
|
|
338
|
-
this.ctx.font = 'bold 14px sans-serif';
|
|
339
|
-
this.ctx.textBaseline = 'middle';
|
|
340
|
-
|
|
341
|
-
const align = col.align || (isLeaf ? 'left' : 'center');
|
|
342
|
-
this.ctx.textAlign = align as any;
|
|
343
|
-
|
|
344
|
-
let textX = x + 10;
|
|
345
|
-
if (align === 'center') textX = x + w / 2;
|
|
346
|
-
if (align === 'right') textX = x + w - 24;
|
|
347
|
-
|
|
348
|
-
const title = col.title || '';
|
|
349
|
-
this.ctx.fillText(title, textX, y + h / 2);
|
|
350
|
-
|
|
351
|
-
// Draw menu icon if leaf and hovered
|
|
352
|
-
if (isLeaf && col.type !== 'selection' && col.type !== 'expand') {
|
|
353
|
-
const idx = this.flattenedColumns.indexOf(col);
|
|
354
|
-
if (this.hoverHeaderIndex === idx) {
|
|
355
|
-
this.ctx.fillStyle = '#909399';
|
|
356
|
-
this.ctx.beginPath();
|
|
357
|
-
const iconX = x + w - 18;
|
|
358
|
-
const iconY = y + h / 2;
|
|
359
|
-
this.ctx.arc(iconX, iconY - 4, 1.5, 0, Math.PI * 2);
|
|
360
|
-
this.ctx.arc(iconX, iconY, 1.5, 0, Math.PI * 2);
|
|
361
|
-
this.ctx.arc(iconX, iconY + 4, 1.5, 0, Math.PI * 2);
|
|
362
|
-
this.ctx.fill();
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
this.ctx.restore();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
public getHeaderAt(x: number, y: number): { column: ColumnConfig, rect: any, isMenu: boolean } | null {
|
|
370
|
-
const { headerHeight } = this.options;
|
|
371
|
-
if (y > headerHeight) return null;
|
|
372
|
-
|
|
373
|
-
const baseHeaderHeight = this.columnLevels > 1 ? 40 : headerHeight;
|
|
374
|
-
|
|
375
|
-
const findHeaderRecursively = (cols: ColumnConfig[], currentX: number, currentLevel: number, isFixedPass: boolean): any => {
|
|
376
|
-
let xOffset = currentX;
|
|
377
|
-
for (const col of cols) {
|
|
378
|
-
const isFixedCol = this.isColumnFixed(col);
|
|
379
|
-
|
|
380
|
-
// Skip based on pass
|
|
381
|
-
if (isFixedPass !== isFixedCol) {
|
|
382
|
-
xOffset += this.getColumnWidth(col);
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const w = this.getColumnWidth(col);
|
|
387
|
-
const isLeaf = !col.children || col.children.length === 0;
|
|
388
|
-
const h = isLeaf ? (this.columnLevels - currentLevel) * baseHeaderHeight : baseHeaderHeight;
|
|
389
|
-
const startY = currentLevel * baseHeaderHeight;
|
|
390
|
-
|
|
391
|
-
const drawX = isFixedPass ? xOffset : xOffset - this.scrollX;
|
|
392
|
-
|
|
393
|
-
if (!isFixedPass && drawX < this.fixedLeftWidth) {
|
|
394
|
-
xOffset += w;
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (x >= drawX && x < drawX + w && y >= startY && y < startY + h) {
|
|
399
|
-
const isMenu = isLeaf && col.type !== 'selection' && col.type !== 'expand' && x >= drawX + w - 24;
|
|
400
|
-
return {
|
|
401
|
-
column: col,
|
|
402
|
-
rect: { x: drawX, y: startY, width: w, height: h },
|
|
403
|
-
isMenu
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (col.children && col.children.length > 0) {
|
|
408
|
-
const result = findHeaderRecursively(col.children, xOffset, currentLevel + 1, isFixedPass);
|
|
409
|
-
if (result) return result;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
xOffset += w;
|
|
413
|
-
}
|
|
414
|
-
return null;
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
// Check fixed first
|
|
418
|
-
const fixedResult = findHeaderRecursively(this.columns, 0, 0, true);
|
|
419
|
-
if (fixedResult) return fixedResult;
|
|
420
|
-
|
|
421
|
-
// Then check scrolling
|
|
422
|
-
return findHeaderRecursively(this.columns, 0, 0, false);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
private drawBody() {
|
|
426
|
-
const { headerHeight } = this.options;
|
|
427
|
-
const { startRow, endRow } = this.getRowRange(this.height);
|
|
428
|
-
const summaryHeight = this.summaryRows.length * this.options.rowHeight;
|
|
429
|
-
|
|
430
|
-
this.ctx.save();
|
|
431
|
-
this.ctx.beginPath();
|
|
432
|
-
this.ctx.rect(this.fixedLeftWidth, headerHeight, this.width - this.fixedLeftWidth, this.height - headerHeight - summaryHeight);
|
|
433
|
-
this.ctx.clip();
|
|
434
|
-
|
|
435
|
-
const processedCells = new Set<string>();
|
|
436
|
-
|
|
437
|
-
const mergedCellsToDraw: Array<{ col: ColumnConfig, val: any, x: number, y: number, w: number, h: number, r: number, c: number, bgColor: string }> = [];
|
|
438
|
-
|
|
439
|
-
for (let i = startRow; i < endRow; i++) {
|
|
440
|
-
const row = this.data[i];
|
|
441
|
-
if (!row) continue;
|
|
442
|
-
const y = (this.rowOffsets[i] ?? 0) - this.scrollY + headerHeight;
|
|
443
|
-
const h = (this.rowOffsets[i+1] ?? 0) - (this.rowOffsets[i] ?? 0);
|
|
444
|
-
|
|
445
|
-
const rowBgColor = i % 2 === 1 && this.options.stripe ? '#fafafa' : '#fff';
|
|
446
|
-
|
|
447
|
-
this.flattenedColumns.forEach((col, j) => {
|
|
448
|
-
if (col.fixed === 'left' || col.fixed === true) return;
|
|
449
|
-
if (processedCells.has(`${i},${j}`)) return;
|
|
450
|
-
|
|
451
|
-
const x = (this.columnPositions[j] ?? 0) - this.scrollX;
|
|
452
|
-
const w = (this.columnPositions[j+1] ?? 0) - (this.columnPositions[j] ?? 0);
|
|
453
|
-
|
|
454
|
-
// Handle merged cells
|
|
455
|
-
const span = this.options.spanMethod({ row: i, column: col, rowIndex: i, columnIndex: j });
|
|
456
|
-
if (span) {
|
|
457
|
-
const { rowspan, colspan } = span;
|
|
458
|
-
if (rowspan === 0 || colspan === 0) return;
|
|
459
|
-
|
|
460
|
-
const spanW = (this.columnPositions[j + colspan] ?? this.totalWidth) - (this.columnPositions[j] ?? 0);
|
|
461
|
-
const spanH = (this.rowOffsets[i + rowspan] ?? this.totalHeight) - (this.rowOffsets[i] ?? 0);
|
|
462
|
-
|
|
463
|
-
for (let r = 0; r < rowspan; r++) {
|
|
464
|
-
for (let c = 0; c < colspan; c++) {
|
|
465
|
-
processedCells.add(`${i + r},${j + c}`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
mergedCellsToDraw.push({ col, val: row[col.key || ''], x, y, w: spanW, h: spanH, r: i, c: j, bgColor: rowBgColor });
|
|
470
|
-
} else {
|
|
471
|
-
this.ctx.fillStyle = rowBgColor;
|
|
472
|
-
this.ctx.fillRect(x, y, w, this.options.rowHeight);
|
|
473
|
-
|
|
474
|
-
this.drawCell(col, row[col.key || ''], x, y, w, this.options.rowHeight, i);
|
|
475
|
-
if (this.options.border) {
|
|
476
|
-
this.ctx.strokeStyle = '#ebeef5';
|
|
477
|
-
this.ctx.strokeRect(x + 0.5, y + 0.5, w, this.options.rowHeight);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
// Draw expand content if expanded
|
|
483
|
-
if (this.expandedRowKeys.has(row.id)) {
|
|
484
|
-
const expandY = y + this.options.rowHeight;
|
|
485
|
-
const expandH = h - this.options.rowHeight;
|
|
486
|
-
|
|
487
|
-
// Draw expand area border/background
|
|
488
|
-
this.ctx.fillStyle = '#fdfdfd';
|
|
489
|
-
this.ctx.fillRect(0, expandY, this.width, expandH);
|
|
490
|
-
if (this.options.border) {
|
|
491
|
-
this.ctx.strokeStyle = '#ebeef5';
|
|
492
|
-
this.ctx.beginPath();
|
|
493
|
-
this.ctx.moveTo(0, expandY);
|
|
494
|
-
this.ctx.lineTo(this.width, expandY);
|
|
495
|
-
this.ctx.stroke();
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
this.drawExpandPlaceholder(row, this.fixedLeftWidth - this.scrollX, expandY, this.totalWidth - this.fixedLeftWidth, expandH);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Draw merged cells at the end to be on top
|
|
503
|
-
mergedCellsToDraw.forEach(({ col, val, x, y, w, h, r, bgColor }) => {
|
|
504
|
-
this.ctx.fillStyle = bgColor;
|
|
505
|
-
this.ctx.fillRect(x, y, w, h);
|
|
506
|
-
this.drawCell(col, val, x, y, w, h, r);
|
|
507
|
-
if (this.options.border) {
|
|
508
|
-
this.ctx.strokeStyle = '#ebeef5';
|
|
509
|
-
this.ctx.strokeRect(x + 0.5, y + 0.5, w, h);
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
this.ctx.restore();
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
private drawFixedLeftBody() {
|
|
517
|
-
const { headerHeight } = this.options;
|
|
518
|
-
const { startRow, endRow } = this.getRowRange(this.height);
|
|
519
|
-
const summaryHeight = this.summaryRows.length * this.options.rowHeight;
|
|
520
|
-
|
|
521
|
-
this.ctx.save();
|
|
522
|
-
this.ctx.beginPath();
|
|
523
|
-
this.ctx.rect(0, headerHeight, this.fixedLeftWidth, this.height - headerHeight - summaryHeight);
|
|
524
|
-
this.ctx.clip();
|
|
525
|
-
|
|
526
|
-
const processedCells = new Set<string>();
|
|
527
|
-
const mergedCellsToDraw: Array<{ col: ColumnConfig, val: any, x: number, y: number, w: number, h: number, r: number, c: number, bgColor: string }> = [];
|
|
528
|
-
|
|
529
|
-
for (let i = startRow; i < endRow; i++) {
|
|
530
|
-
const row = this.data[i];
|
|
531
|
-
if (!row) continue;
|
|
532
|
-
const y = (this.rowOffsets[i] ?? 0) - this.scrollY + headerHeight;
|
|
533
|
-
|
|
534
|
-
const rowBgColor = i % 2 === 1 && this.options.stripe ? '#fafafa' : '#fff';
|
|
535
|
-
|
|
536
|
-
this.flattenedColumns.forEach((col, j) => {
|
|
537
|
-
if (!(col.fixed === 'left' || col.fixed === true)) return;
|
|
538
|
-
if (processedCells.has(`${i},${j}`)) return;
|
|
539
|
-
|
|
540
|
-
const x = (this.columnPositions[j] ?? 0);
|
|
541
|
-
const w = (this.columnPositions[j+1] ?? 0) - (this.columnPositions[j] ?? 0);
|
|
542
|
-
|
|
543
|
-
const span = this.options.spanMethod({ row: i, column: col, rowIndex: i, columnIndex: j });
|
|
544
|
-
if (span) {
|
|
545
|
-
const { rowspan, colspan } = span;
|
|
546
|
-
if (rowspan === 0 || colspan === 0) return;
|
|
547
|
-
const spanW = (this.columnPositions[j + colspan] ?? this.fixedLeftWidth) - (this.columnPositions[j] ?? 0);
|
|
548
|
-
const spanH = (this.rowOffsets[i + rowspan] ?? this.totalHeight) - (this.rowOffsets[i] ?? 0);
|
|
549
|
-
|
|
550
|
-
for (let r = 0; r < rowspan; r++) {
|
|
551
|
-
for (let c = 0; c < colspan; c++) {
|
|
552
|
-
processedCells.add(`${i + r},${j + c}`);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
mergedCellsToDraw.push({ col, val: row[col.key || ''], x, y, w: spanW, h: spanH, r: i, c: j, bgColor: rowBgColor });
|
|
557
|
-
} else {
|
|
558
|
-
this.ctx.fillStyle = rowBgColor;
|
|
559
|
-
this.ctx.fillRect(x, y, w, this.options.rowHeight);
|
|
560
|
-
|
|
561
|
-
this.drawCell(col, row[col.key || ''], x, y, w, this.options.rowHeight, i);
|
|
562
|
-
if (this.options.border) {
|
|
563
|
-
this.ctx.strokeStyle = '#ebeef5';
|
|
564
|
-
this.ctx.strokeRect(x + 0.5, y + 0.5, w, this.options.rowHeight);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Draw merged cells at the end to be on top
|
|
571
|
-
mergedCellsToDraw.forEach(({ col, val, x, y, w, h, r, bgColor }) => {
|
|
572
|
-
this.ctx.fillStyle = bgColor;
|
|
573
|
-
this.ctx.fillRect(x, y, w, h);
|
|
574
|
-
this.drawCell(col, val, x, y, w, h, r);
|
|
575
|
-
if (this.options.border) {
|
|
576
|
-
this.ctx.strokeStyle = '#ebeef5';
|
|
577
|
-
this.ctx.strokeRect(x + 0.5, y + 0.5, w, h);
|
|
578
|
-
}
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
this.ctx.restore();
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
private drawExpandPlaceholder(row: TableRow, x: number, y: number, w: number, h: number) {
|
|
585
|
-
this.ctx.save();
|
|
586
|
-
this.ctx.fillStyle = '#fef0f0';
|
|
587
|
-
this.ctx.fillRect(x, y, w, h);
|
|
588
|
-
this.ctx.fillStyle = '#f56c6c';
|
|
589
|
-
this.ctx.font = 'italic 12px sans-serif';
|
|
590
|
-
this.ctx.textAlign = 'center';
|
|
591
|
-
this.ctx.fillText(`Expanded content for row ${row.id} (Rendered via h function in overlay)`, x + w / 2, y + h / 2);
|
|
592
|
-
this.ctx.restore();
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private drawSummaryRows() {
|
|
596
|
-
if (this.summaryRows.length === 0) return;
|
|
597
|
-
const { rowHeight } = this.options;
|
|
598
|
-
const summaryHeight = this.summaryRows.length * rowHeight;
|
|
599
|
-
const startY = this.height - summaryHeight;
|
|
600
|
-
|
|
601
|
-
this.ctx.save();
|
|
602
|
-
this.ctx.fillStyle = '#fdf6ec';
|
|
603
|
-
this.ctx.fillRect(0, startY, this.width, summaryHeight);
|
|
604
|
-
|
|
605
|
-
// Calculate label colspan (merge selection, expand, id columns if they are at the start)
|
|
606
|
-
let labelColSpan = 0;
|
|
607
|
-
for (let j = 0; j < this.flattenedColumns.length; j++) {
|
|
608
|
-
const col = this.flattenedColumns[j];
|
|
609
|
-
if (col && (col.type === 'selection' || col.type === 'expand' || col.key === 'id')) {
|
|
610
|
-
labelColSpan++;
|
|
611
|
-
} else {
|
|
612
|
-
break;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
if (labelColSpan === 0) labelColSpan = 1;
|
|
616
|
-
|
|
617
|
-
this.summaryRows.forEach((row, i) => {
|
|
618
|
-
const y = startY + i * rowHeight;
|
|
619
|
-
|
|
620
|
-
// Scrolling part
|
|
621
|
-
this.ctx.save();
|
|
622
|
-
this.ctx.beginPath();
|
|
623
|
-
this.ctx.rect(this.fixedLeftWidth, y, this.width - this.fixedLeftWidth, rowHeight);
|
|
624
|
-
this.ctx.clip();
|
|
625
|
-
this.flattenedColumns.forEach((col, j) => {
|
|
626
|
-
if (col.fixed === 'left' || col.fixed === true) return;
|
|
627
|
-
if (j < labelColSpan) return; // Skip columns covered by label
|
|
628
|
-
|
|
629
|
-
const x = (this.columnPositions[j] ?? 0) - this.scrollX;
|
|
630
|
-
const w = (this.columnPositions[j+1] ?? 0) - (this.columnPositions[j] ?? 0);
|
|
631
|
-
|
|
632
|
-
this.drawTextCell(row[j], x, y, w, rowHeight, col.align);
|
|
633
|
-
|
|
634
|
-
if (this.options.border) {
|
|
635
|
-
this.ctx.strokeStyle = '#ebeef5';
|
|
636
|
-
this.ctx.strokeRect(x + 0.5, y + 0.5, w, rowHeight);
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
this.ctx.restore();
|
|
640
|
-
|
|
641
|
-
// Fixed part
|
|
642
|
-
this.flattenedColumns.forEach((col, j) => {
|
|
643
|
-
if (col.fixed === 'left' || col.fixed === true) {
|
|
644
|
-
const x = (this.columnPositions[j] ?? 0);
|
|
645
|
-
const w = (this.columnPositions[j+1] ?? 0) - (this.columnPositions[j] ?? 0);
|
|
646
|
-
|
|
647
|
-
if (j === 0) {
|
|
648
|
-
// Draw merged label
|
|
649
|
-
const labelW = (this.columnPositions[labelColSpan] ?? this.fixedLeftWidth) - (this.columnPositions[0] ?? 0);
|
|
650
|
-
this.ctx.save();
|
|
651
|
-
this.ctx.font = 'bold 13px sans-serif';
|
|
652
|
-
this.ctx.fillStyle = '#e6a23c';
|
|
653
|
-
this.drawTextCell(row[0], x, y, labelW, rowHeight, 'center');
|
|
654
|
-
this.ctx.restore();
|
|
655
|
-
|
|
656
|
-
if (this.options.border) {
|
|
657
|
-
this.ctx.strokeStyle = '#ebeef5';
|
|
658
|
-
this.ctx.strokeRect(x + 0.5, y + 0.5, labelW, rowHeight);
|
|
659
|
-
}
|
|
660
|
-
} else if (j < labelColSpan) {
|
|
661
|
-
// Skip columns covered by label
|
|
662
|
-
return;
|
|
663
|
-
} else {
|
|
664
|
-
this.drawTextCell(row[j], x, y, w, rowHeight, col.align);
|
|
665
|
-
if (this.options.border) {
|
|
666
|
-
this.ctx.strokeStyle = '#ebeef5';
|
|
667
|
-
this.ctx.strokeRect(x + 0.5, y + 0.5, w, rowHeight);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
});
|
|
672
|
-
});
|
|
673
|
-
this.ctx.restore();
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
private drawCell(col: ColumnConfig, value: any, x: number, y: number, w: number, h: number, rowIndex: number) {
|
|
677
|
-
this.ctx.save();
|
|
678
|
-
this.ctx.beginPath();
|
|
679
|
-
this.ctx.rect(x, y, w, h);
|
|
680
|
-
this.ctx.clip();
|
|
681
|
-
|
|
682
|
-
// Use custom renderer if provided
|
|
683
|
-
if (col.renderCell) {
|
|
684
|
-
// In a real Canvas app, renderCell might need to return a VNode that we render as an overlay
|
|
685
|
-
// For now, we'll draw a placeholder or the text if possible
|
|
686
|
-
this.drawTextCell(value, x, y, w, h, col.align);
|
|
687
|
-
} else {
|
|
688
|
-
switch (col.type) {
|
|
689
|
-
case 'expand':
|
|
690
|
-
this.drawExpandButton(x + (w - 16) / 2, y + (this.options.rowHeight - 16) / 2, this.expandedRowKeys.has(this.data[rowIndex]?.id ?? ''));
|
|
691
|
-
break;
|
|
692
|
-
case 'selection':
|
|
693
|
-
const isSelected = this.selectedRowKeys.has(this.data[rowIndex]?.id ?? '');
|
|
694
|
-
this.drawCheckboxCell(isSelected, x, y, w, this.options.rowHeight, 'center');
|
|
695
|
-
break;
|
|
696
|
-
case 'image':
|
|
697
|
-
this.drawImageCell(value, x, y, w, h, col.align);
|
|
698
|
-
break;
|
|
699
|
-
case 'checkbox':
|
|
700
|
-
this.drawCheckboxCell(value, x, y, w, h, col.align);
|
|
701
|
-
break;
|
|
702
|
-
case 'radio':
|
|
703
|
-
this.drawRadioCell(value, x, y, w, h, col.align);
|
|
704
|
-
break;
|
|
705
|
-
case 'switch':
|
|
706
|
-
this.drawSwitchCell(value, x, y, w, h, col.align);
|
|
707
|
-
break;
|
|
708
|
-
case 'color-picker':
|
|
709
|
-
this.drawColorPickerCell(value, x, y, w, h, col.align);
|
|
710
|
-
break;
|
|
711
|
-
case 'tags':
|
|
712
|
-
this.drawTagsCell(value, x, y, w, h);
|
|
713
|
-
break;
|
|
714
|
-
case 'text':
|
|
715
|
-
default:
|
|
716
|
-
this.drawTextCell(value, x, y, w, h, col.align);
|
|
717
|
-
break;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
this.ctx.restore();
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
private drawExpandButton(x: number, y: number, isExpanded: boolean) {
|
|
724
|
-
this.ctx.save();
|
|
725
|
-
this.ctx.translate(x + 8, y + 8);
|
|
726
|
-
if (isExpanded) {
|
|
727
|
-
this.ctx.rotate(Math.PI / 2);
|
|
728
|
-
}
|
|
729
|
-
this.ctx.strokeStyle = '#909399';
|
|
730
|
-
this.ctx.lineWidth = 2;
|
|
731
|
-
this.ctx.beginPath();
|
|
732
|
-
this.ctx.moveTo(-3, -5);
|
|
733
|
-
this.ctx.lineTo(3, 0);
|
|
734
|
-
this.ctx.lineTo(-3, 5);
|
|
735
|
-
this.ctx.stroke();
|
|
736
|
-
this.ctx.restore();
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
private drawTextCell(value: any, x: number, y: number, w: number, h: number, align?: string) {
|
|
740
|
-
this.ctx.fillStyle = '#606266';
|
|
741
|
-
this.ctx.font = '14px sans-serif';
|
|
742
|
-
this.ctx.textAlign = (align as any) || 'left';
|
|
743
|
-
this.ctx.textBaseline = 'middle';
|
|
744
|
-
|
|
745
|
-
let textX = x + 10;
|
|
746
|
-
if (align === 'center') textX = x + w / 2;
|
|
747
|
-
if (align === 'right') textX = x + w - 10;
|
|
748
|
-
|
|
749
|
-
const cellValue = value === undefined || value === null ? '' : String(value);
|
|
750
|
-
this.ctx.fillText(cellValue, textX, y + h / 2);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
private drawImageCell(value: any, x: number, y: number, w: number, h: number, align?: string) {
|
|
754
|
-
if (!value) return;
|
|
755
|
-
const src = String(value);
|
|
756
|
-
const size = Math.min(w, h) - 10;
|
|
757
|
-
let imgX = x + 5;
|
|
758
|
-
if (align === 'center') imgX = x + (w - size) / 2;
|
|
759
|
-
if (align === 'right') imgX = x + w - size - 5;
|
|
760
|
-
|
|
761
|
-
let img = this.imageCache.get(src);
|
|
762
|
-
if (!img) {
|
|
763
|
-
img = new Image();
|
|
764
|
-
img.src = src;
|
|
765
|
-
this.imageCache.set(src, img);
|
|
766
|
-
img.onload = () => this.render();
|
|
767
|
-
img.onerror = () => {
|
|
768
|
-
console.error(`Failed to load image: ${src}`);
|
|
769
|
-
this.imageCache.delete(src);
|
|
770
|
-
};
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (img.complete && img.naturalWidth > 0) {
|
|
774
|
-
this.ctx.drawImage(img, imgX, y + (h - size) / 2, size, size);
|
|
775
|
-
} else {
|
|
776
|
-
// Placeholder
|
|
777
|
-
this.ctx.fillStyle = '#f5f7fa';
|
|
778
|
-
this.ctx.fillRect(imgX, y + (h - size) / 2, size, size);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
private drawCheckboxCell(value: any, x: number, y: number, w: number, h: number, align?: string) {
|
|
783
|
-
const size = 16;
|
|
784
|
-
let rectX = x + 10;
|
|
785
|
-
if (align === 'center') rectX = x + (w - size) / 2;
|
|
786
|
-
if (align === 'right') rectX = x + w - size - 10;
|
|
787
|
-
|
|
788
|
-
const rectY = y + (h - size) / 2;
|
|
789
|
-
|
|
790
|
-
this.ctx.strokeStyle = value ? '#409eff' : '#dcdfe6';
|
|
791
|
-
this.ctx.fillStyle = value ? '#409eff' : '#fff';
|
|
792
|
-
this.ctx.lineWidth = 1;
|
|
793
|
-
this.ctx.strokeRect(rectX + 0.5, rectY + 0.5, size, size);
|
|
794
|
-
if (value) {
|
|
795
|
-
this.ctx.fillRect(rectX + 1, rectY + 1, size - 1, size - 1);
|
|
796
|
-
// Draw checkmark
|
|
797
|
-
this.ctx.strokeStyle = '#fff';
|
|
798
|
-
this.ctx.beginPath();
|
|
799
|
-
this.ctx.moveTo(rectX + 4, rectY + 8);
|
|
800
|
-
this.ctx.lineTo(rectX + 7, rectY + 11);
|
|
801
|
-
this.ctx.lineTo(rectX + 12, rectY + 5);
|
|
802
|
-
this.ctx.stroke();
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
private drawRadioCell(value: any, x: number, y: number, w: number, h: number, align?: string) {
|
|
807
|
-
const size = 16;
|
|
808
|
-
let rectX = x + 10;
|
|
809
|
-
if (align === 'center') rectX = x + (w - size) / 2;
|
|
810
|
-
if (align === 'right') rectX = x + w - size - 10;
|
|
811
|
-
const rectY = y + (h - size) / 2;
|
|
812
|
-
|
|
813
|
-
const centerX = rectX + size / 2;
|
|
814
|
-
const centerY = rectY + size / 2;
|
|
815
|
-
|
|
816
|
-
this.ctx.strokeStyle = value ? '#409eff' : '#dcdfe6';
|
|
817
|
-
this.ctx.fillStyle = '#fff';
|
|
818
|
-
this.ctx.beginPath();
|
|
819
|
-
this.ctx.arc(centerX, centerY, size / 2, 0, Math.PI * 2);
|
|
820
|
-
this.ctx.stroke();
|
|
821
|
-
this.ctx.fill();
|
|
822
|
-
|
|
823
|
-
if (value) {
|
|
824
|
-
this.ctx.fillStyle = '#409eff';
|
|
825
|
-
this.ctx.beginPath();
|
|
826
|
-
this.ctx.arc(centerX, centerY, size / 4, 0, Math.PI * 2);
|
|
827
|
-
this.ctx.fill();
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
private drawSwitchCell(value: any, x: number, y: number, w: number, h: number, align?: string) {
|
|
832
|
-
const swWidth = 32;
|
|
833
|
-
const swHeight = 16;
|
|
834
|
-
let rectX = x + 10;
|
|
835
|
-
if (align === 'center') rectX = x + (w - swWidth) / 2;
|
|
836
|
-
if (align === 'right') rectX = x + w - swWidth - 10;
|
|
837
|
-
const rectY = y + (h - swHeight) / 2;
|
|
838
|
-
|
|
839
|
-
this.ctx.fillStyle = value ? '#13ce66' : '#ff4949';
|
|
840
|
-
this.ctx.beginPath();
|
|
841
|
-
this.ctx.roundRect?.(rectX, rectY, swWidth, swHeight, swHeight / 2);
|
|
842
|
-
this.ctx.fill();
|
|
843
|
-
|
|
844
|
-
this.ctx.fillStyle = '#fff';
|
|
845
|
-
this.ctx.beginPath();
|
|
846
|
-
const knobX = value ? rectX + swWidth - swHeight + 2 : rectX + 2;
|
|
847
|
-
this.ctx.arc(knobX + (swHeight - 4) / 2, rectY + swHeight / 2, (swHeight - 4) / 2, 0, Math.PI * 2);
|
|
848
|
-
this.ctx.fill();
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
private drawColorPickerCell(value: any, x: number, y: number, w: number, h: number, align?: string) {
|
|
852
|
-
const size = 20;
|
|
853
|
-
let rectX = x + 10;
|
|
854
|
-
if (align === 'center') rectX = x + (w - size) / 2;
|
|
855
|
-
if (align === 'right') rectX = x + w - size - 10;
|
|
856
|
-
const rectY = y + (h - size) / 2;
|
|
857
|
-
|
|
858
|
-
this.ctx.fillStyle = value || '#000';
|
|
859
|
-
this.ctx.fillRect(rectX, rectY, size, size);
|
|
860
|
-
this.ctx.strokeStyle = '#dcdfe6';
|
|
861
|
-
this.ctx.strokeRect(rectX + 0.5, rectY + 0.5, size, size);
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
private drawTagsCell(value: any[], x: number, y: number, w: number, h: number) {
|
|
865
|
-
if (!Array.isArray(value)) return;
|
|
866
|
-
let currentX = x + 5;
|
|
867
|
-
const tagHeight = 20;
|
|
868
|
-
const tagPadding = 5;
|
|
869
|
-
|
|
870
|
-
const fontSize = 12
|
|
871
|
-
|
|
872
|
-
value.forEach(tag => {
|
|
873
|
-
const text = String(tag);
|
|
874
|
-
this.ctx.font = `${fontSize}px sans-serif`;
|
|
875
|
-
const textWidth = this.ctx.measureText(text).width;
|
|
876
|
-
const tagWidth = textWidth + tagPadding * 2;
|
|
877
|
-
|
|
878
|
-
if (currentX + tagWidth > x + w - 5) return; // Basic overflow handling
|
|
879
|
-
|
|
880
|
-
const tagY = y + (h - tagHeight) / 2;
|
|
881
|
-
this.ctx.fillStyle = '#ecf5ff';
|
|
882
|
-
this.ctx.strokeStyle = '#d9ecff';
|
|
883
|
-
this.ctx.beginPath();
|
|
884
|
-
this.ctx.roundRect?.(currentX, tagY, tagWidth, tagHeight, 4);
|
|
885
|
-
this.ctx.fill();
|
|
886
|
-
this.ctx.stroke();
|
|
887
|
-
|
|
888
|
-
this.ctx.fillStyle = '#409eff';
|
|
889
|
-
this.ctx.textAlign = 'center';
|
|
890
|
-
this.ctx.fillText(text, currentX + tagWidth / 2, tagY + tagHeight - fontSize / 2);
|
|
891
|
-
|
|
892
|
-
currentX += tagWidth + 5;
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
public getCellAt(x: number, y: number): CellInfo | null {
|
|
897
|
-
const { headerHeight } = this.options;
|
|
898
|
-
if (y < headerHeight) return null;
|
|
899
|
-
|
|
900
|
-
const { startRow, endRow } = this.getRowRange(this.height);
|
|
901
|
-
for (let i = startRow; i < endRow; i++) {
|
|
902
|
-
const row = this.data[i];
|
|
903
|
-
if (!row) continue;
|
|
904
|
-
const cellY = (this.rowOffsets[i] ?? 0) - this.scrollY + headerHeight;
|
|
905
|
-
const cellH = (this.rowOffsets[i+1] ?? 0) - (this.rowOffsets[i] ?? 0);
|
|
906
|
-
|
|
907
|
-
if (y >= cellY && y < cellY + cellH) {
|
|
908
|
-
// Check columns
|
|
909
|
-
for (let j = 0; j < this.flattenedColumns.length; j++) {
|
|
910
|
-
const column = this.flattenedColumns[j]!;
|
|
911
|
-
const w = (this.columnPositions[j+1] ?? 0) - (this.columnPositions[j] ?? 0);
|
|
912
|
-
|
|
913
|
-
let cellX = 0;
|
|
914
|
-
if (column.fixed === 'left' || column.fixed === true) {
|
|
915
|
-
cellX = this.columnPositions[j] ?? 0;
|
|
916
|
-
} else {
|
|
917
|
-
cellX = (this.columnPositions[j] ?? 0) - this.scrollX;
|
|
918
|
-
if (cellX < this.fixedLeftWidth) continue;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
if (x >= cellX && x < cellX + w) {
|
|
922
|
-
const isExpandBtn = column.type === 'expand';
|
|
923
|
-
const isSelection = column.type === 'selection';
|
|
924
|
-
|
|
925
|
-
return {
|
|
926
|
-
row: i,
|
|
927
|
-
col: j,
|
|
928
|
-
rect: { x: cellX, y: cellY, width: w, height: cellH },
|
|
929
|
-
column,
|
|
930
|
-
data: row,
|
|
931
|
-
isExpandBtn,
|
|
932
|
-
isSelection
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
return null;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
public getRowOffsets() { return this.rowOffsets; }
|
|
942
|
-
public getScrollX() { return this.scrollX; }
|
|
943
|
-
public getScrollY() { return this.scrollY; }
|
|
944
|
-
public getFixedLeftWidth() { return this.fixedLeftWidth; }
|
|
945
|
-
public getTotalWidth() { return this.totalWidth; }
|
|
946
|
-
public getTotalHeight() { return this.totalHeight; }
|
|
947
|
-
}
|
|
948
|
-
|