@pagent-libs/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/canvas/cell-renderer.d.ts +45 -0
- package/dist/canvas/cell-renderer.d.ts.map +1 -0
- package/dist/canvas/grid-renderer.d.ts +29 -0
- package/dist/canvas/grid-renderer.d.ts.map +1 -0
- package/dist/canvas/header-renderer.d.ts +58 -0
- package/dist/canvas/header-renderer.d.ts.map +1 -0
- package/dist/canvas/hit-testing.d.ts +81 -0
- package/dist/canvas/hit-testing.d.ts.map +1 -0
- package/dist/canvas/index.d.ts +9 -0
- package/dist/canvas/index.d.ts.map +1 -0
- package/dist/canvas/renderer.d.ts +140 -0
- package/dist/canvas/renderer.d.ts.map +1 -0
- package/dist/canvas/selection-renderer.d.ts +55 -0
- package/dist/canvas/selection-renderer.d.ts.map +1 -0
- package/dist/canvas/text-renderer.d.ts +49 -0
- package/dist/canvas/text-renderer.d.ts.map +1 -0
- package/dist/canvas/types.d.ts +200 -0
- package/dist/canvas/types.d.ts.map +1 -0
- package/dist/collaboration/firebase-provider.d.ts +13 -0
- package/dist/collaboration/firebase-provider.d.ts.map +1 -0
- package/dist/collaboration/index.d.ts +3 -0
- package/dist/collaboration/index.d.ts.map +1 -0
- package/dist/collaboration/types.d.ts +34 -0
- package/dist/collaboration/types.d.ts.map +1 -0
- package/dist/event-emitter.d.ts +13 -0
- package/dist/event-emitter.d.ts.map +1 -0
- package/dist/export/csv.d.ts +5 -0
- package/dist/export/csv.d.ts.map +1 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/features/filter.d.ts +58 -0
- package/dist/features/filter.d.ts.map +1 -0
- package/dist/features/freeze.d.ts +86 -0
- package/dist/features/freeze.d.ts.map +1 -0
- package/dist/features/index.d.ts +4 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/sort.d.ts +15 -0
- package/dist/features/sort.d.ts.map +1 -0
- package/dist/format-pool.d.ts +17 -0
- package/dist/format-pool.d.ts.map +1 -0
- package/dist/formula-graph.d.ts +12 -0
- package/dist/formula-graph.d.ts.map +1 -0
- package/dist/formula-parser/cell-reference.d.ts +7 -0
- package/dist/formula-parser/cell-reference.d.ts.map +1 -0
- package/dist/formula-parser/formula-adjust.d.ts +13 -0
- package/dist/formula-parser/formula-adjust.d.ts.map +1 -0
- package/dist/formula-parser/formula-ranges.d.ts +22 -0
- package/dist/formula-parser/formula-ranges.d.ts.map +1 -0
- package/dist/formula-parser/index.d.ts +6 -0
- package/dist/formula-parser/index.d.ts.map +1 -0
- package/dist/formula-parser/parser.d.ts +18 -0
- package/dist/formula-parser/parser.d.ts.map +1 -0
- package/dist/formula-parser/types.d.ts +33 -0
- package/dist/formula-parser/types.d.ts.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +5823 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +5885 -0
- package/dist/index.js.map +1 -0
- package/dist/sheet.d.ts +119 -0
- package/dist/sheet.d.ts.map +1 -0
- package/dist/style-pool.d.ts +17 -0
- package/dist/style-pool.d.ts.map +1 -0
- package/dist/types.d.ts +260 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/cell-key.d.ts +7 -0
- package/dist/utils/cell-key.d.ts.map +1 -0
- package/dist/utils/format-utils.d.ts +75 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/range.d.ts +13 -0
- package/dist/utils/range.d.ts.map +1 -0
- package/dist/workbook.d.ts +155 -0
- package/dist/workbook.d.ts.map +1 -0
- package/package.json +46 -0
- package/src/canvas/cell-renderer.ts +181 -0
- package/src/canvas/grid-renderer.ts +238 -0
- package/src/canvas/header-renderer.ts +402 -0
- package/src/canvas/hit-testing.ts +537 -0
- package/src/canvas/index.ts +16 -0
- package/src/canvas/renderer.ts +1056 -0
- package/src/canvas/selection-renderer.ts +604 -0
- package/src/canvas/text-renderer.ts +321 -0
- package/src/canvas/types.ts +289 -0
- package/src/collaboration/firebase-provider.ts +48 -0
- package/src/collaboration/index.ts +5 -0
- package/src/collaboration/types.ts +38 -0
- package/src/event-emitter.ts +73 -0
- package/src/export/csv.ts +101 -0
- package/src/export/index.ts +4 -0
- package/src/features/filter.ts +231 -0
- package/src/features/freeze.ts +271 -0
- package/src/features/index.ts +5 -0
- package/src/features/sort.ts +282 -0
- package/src/format-pool.ts +61 -0
- package/src/formula-graph.ts +84 -0
- package/src/formula-parser/cell-reference.ts +99 -0
- package/src/formula-parser/formula-adjust.ts +129 -0
- package/src/formula-parser/formula-ranges.ts +159 -0
- package/src/formula-parser/index.ts +8 -0
- package/src/formula-parser/parser.ts +438 -0
- package/src/formula-parser/types.ts +39 -0
- package/src/index.ts +25 -0
- package/src/sheet.ts +502 -0
- package/src/style-pool.ts +62 -0
- package/src/types.ts +291 -0
- package/src/utils/cell-key.ts +19 -0
- package/src/utils/format-utils.ts +515 -0
- package/src/utils/range.ts +53 -0
- package/src/workbook.ts +1031 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
// Selection Renderer - Handles selection highlighting and active cell
|
|
2
|
+
|
|
3
|
+
import type { CanvasTheme, Viewport, CellPosition, Rect, FormulaRangeHighlight } from './types';
|
|
4
|
+
import type { Selection, Range } from '../types';
|
|
5
|
+
import type { FreezeRegion } from '../features/freeze';
|
|
6
|
+
import { getCellRegion } from '../features/freeze';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Renders selection highlighting and active cell border
|
|
10
|
+
*/
|
|
11
|
+
export class SelectionRenderer {
|
|
12
|
+
private theme: CanvasTheme;
|
|
13
|
+
|
|
14
|
+
constructor(theme: CanvasTheme) {
|
|
15
|
+
this.theme = theme;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Main render method for all selection elements
|
|
20
|
+
* @param frozenRows - Number of frozen rows (optional, for freeze pane support)
|
|
21
|
+
* @param frozenCols - Number of frozen columns (optional, for freeze pane support)
|
|
22
|
+
* @param currentRegion - Current freeze region being rendered (optional)
|
|
23
|
+
*/
|
|
24
|
+
render(
|
|
25
|
+
ctx: CanvasRenderingContext2D,
|
|
26
|
+
selection: Selection | null,
|
|
27
|
+
activeCell: CellPosition | null,
|
|
28
|
+
viewport: Viewport,
|
|
29
|
+
rowHeights: Map<number, number>,
|
|
30
|
+
colWidths: Map<number, number>,
|
|
31
|
+
defaultRowHeight: number,
|
|
32
|
+
defaultColWidth: number,
|
|
33
|
+
headerWidth: number,
|
|
34
|
+
headerHeight: number,
|
|
35
|
+
formulaRanges?: FormulaRangeHighlight[],
|
|
36
|
+
hiddenRows?: Set<number>,
|
|
37
|
+
hiddenCols?: Set<number>,
|
|
38
|
+
frozenRows?: number,
|
|
39
|
+
frozenCols?: number,
|
|
40
|
+
currentRegion?: FreezeRegion
|
|
41
|
+
): void {
|
|
42
|
+
const hidRows = hiddenRows ?? new Set<number>();
|
|
43
|
+
const hidCols = hiddenCols ?? new Set<number>();
|
|
44
|
+
const numFrozenRows = frozenRows ?? 0;
|
|
45
|
+
const numFrozenCols = frozenCols ?? 0;
|
|
46
|
+
|
|
47
|
+
// Render formula reference highlights first (behind selection)
|
|
48
|
+
if (formulaRanges && formulaRanges.length > 0) {
|
|
49
|
+
for (const formulaRange of formulaRanges) {
|
|
50
|
+
// Check if this formula range should be rendered in the current region
|
|
51
|
+
if (currentRegion && !this.shouldRenderRangeInRegion(
|
|
52
|
+
{ startRow: formulaRange.startRow, endRow: formulaRange.endRow,
|
|
53
|
+
startCol: formulaRange.startCol, endCol: formulaRange.endCol },
|
|
54
|
+
currentRegion, numFrozenRows, numFrozenCols
|
|
55
|
+
)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.renderFormulaRange(
|
|
60
|
+
ctx,
|
|
61
|
+
formulaRange,
|
|
62
|
+
viewport,
|
|
63
|
+
rowHeights,
|
|
64
|
+
colWidths,
|
|
65
|
+
defaultRowHeight,
|
|
66
|
+
defaultColWidth,
|
|
67
|
+
headerWidth,
|
|
68
|
+
headerHeight,
|
|
69
|
+
hidRows,
|
|
70
|
+
hidCols,
|
|
71
|
+
numFrozenRows,
|
|
72
|
+
numFrozenCols
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Render selection ranges (highlight)
|
|
78
|
+
if (selection) {
|
|
79
|
+
for (const range of selection.ranges) {
|
|
80
|
+
// Check if this selection range should be rendered in the current region
|
|
81
|
+
if (currentRegion && !this.shouldRenderRangeInRegion(
|
|
82
|
+
range, currentRegion, numFrozenRows, numFrozenCols
|
|
83
|
+
)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.renderSelectionRange(
|
|
88
|
+
ctx,
|
|
89
|
+
range,
|
|
90
|
+
viewport,
|
|
91
|
+
rowHeights,
|
|
92
|
+
colWidths,
|
|
93
|
+
defaultRowHeight,
|
|
94
|
+
defaultColWidth,
|
|
95
|
+
headerWidth,
|
|
96
|
+
headerHeight,
|
|
97
|
+
hidRows,
|
|
98
|
+
hidCols,
|
|
99
|
+
numFrozenRows,
|
|
100
|
+
numFrozenCols
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Render active cell border (only if not hidden)
|
|
106
|
+
if (activeCell && !hidRows.has(activeCell.row) && !hidCols.has(activeCell.col)) {
|
|
107
|
+
// Check if active cell should be rendered in the current region
|
|
108
|
+
const activeCellRegion = getCellRegion(activeCell.row, activeCell.col, numFrozenRows, numFrozenCols);
|
|
109
|
+
if (!currentRegion || activeCellRegion === currentRegion) {
|
|
110
|
+
this.renderActiveCell(
|
|
111
|
+
ctx,
|
|
112
|
+
activeCell,
|
|
113
|
+
viewport,
|
|
114
|
+
rowHeights,
|
|
115
|
+
colWidths,
|
|
116
|
+
defaultRowHeight,
|
|
117
|
+
defaultColWidth,
|
|
118
|
+
headerWidth,
|
|
119
|
+
headerHeight,
|
|
120
|
+
hidRows,
|
|
121
|
+
hidCols,
|
|
122
|
+
numFrozenRows,
|
|
123
|
+
numFrozenCols
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Render fill handle if there's a selection (only in the region containing the end of selection)
|
|
128
|
+
if (selection && selection.ranges.length > 0) {
|
|
129
|
+
const lastRange = selection.ranges[selection.ranges.length - 1];
|
|
130
|
+
const fillHandleRegion = getCellRegion(lastRange.endRow, lastRange.endCol, numFrozenRows, numFrozenCols);
|
|
131
|
+
|
|
132
|
+
if (!currentRegion || fillHandleRegion === currentRegion) {
|
|
133
|
+
this.renderFillHandle(
|
|
134
|
+
ctx,
|
|
135
|
+
lastRange,
|
|
136
|
+
viewport,
|
|
137
|
+
rowHeights,
|
|
138
|
+
colWidths,
|
|
139
|
+
defaultRowHeight,
|
|
140
|
+
defaultColWidth,
|
|
141
|
+
headerWidth,
|
|
142
|
+
headerHeight,
|
|
143
|
+
hidRows,
|
|
144
|
+
hidCols,
|
|
145
|
+
numFrozenRows,
|
|
146
|
+
numFrozenCols
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Determine if a range should be rendered in the current freeze region.
|
|
155
|
+
* A range should be rendered if any of its cells overlap with the current region.
|
|
156
|
+
*/
|
|
157
|
+
private shouldRenderRangeInRegion(
|
|
158
|
+
range: Range,
|
|
159
|
+
currentRegion: FreezeRegion,
|
|
160
|
+
frozenRows: number,
|
|
161
|
+
frozenCols: number
|
|
162
|
+
): boolean {
|
|
163
|
+
// Check each corner of the range to see if any fall in the current region
|
|
164
|
+
const corners = [
|
|
165
|
+
{ row: range.startRow, col: range.startCol },
|
|
166
|
+
{ row: range.startRow, col: range.endCol },
|
|
167
|
+
{ row: range.endRow, col: range.startCol },
|
|
168
|
+
{ row: range.endRow, col: range.endCol },
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
for (const corner of corners) {
|
|
172
|
+
if (getCellRegion(corner.row, corner.col, frozenRows, frozenCols) === currentRegion) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Also check if the range spans across the current region
|
|
178
|
+
// (e.g., a range from frozen to non-frozen area)
|
|
179
|
+
const rangeStartsFrozenRow = range.startRow < frozenRows;
|
|
180
|
+
const rangeEndsFrozenRow = range.endRow < frozenRows;
|
|
181
|
+
const rangeStartsFrozenCol = range.startCol < frozenCols;
|
|
182
|
+
const rangeEndsFrozenCol = range.endCol < frozenCols;
|
|
183
|
+
|
|
184
|
+
switch (currentRegion) {
|
|
185
|
+
case 'top-left':
|
|
186
|
+
// Any range that has cells in frozen rows AND frozen cols
|
|
187
|
+
return rangeStartsFrozenRow && rangeStartsFrozenCol;
|
|
188
|
+
case 'top':
|
|
189
|
+
// Range that has cells in frozen rows AND non-frozen cols
|
|
190
|
+
return rangeStartsFrozenRow && !rangeEndsFrozenCol;
|
|
191
|
+
case 'left':
|
|
192
|
+
// Range that has cells in non-frozen rows AND frozen cols
|
|
193
|
+
return !rangeEndsFrozenRow && rangeStartsFrozenCol;
|
|
194
|
+
case 'main':
|
|
195
|
+
// Range that has cells in non-frozen rows AND non-frozen cols
|
|
196
|
+
return !rangeEndsFrozenRow && !rangeEndsFrozenCol;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get color for formula range by index
|
|
202
|
+
*/
|
|
203
|
+
private getFormulaRangeColor(colorIndex: number): { border: string; fill: string } {
|
|
204
|
+
const colors = this.theme.formulaReferenceColors;
|
|
205
|
+
if (colorIndex < colors.length) {
|
|
206
|
+
return colors[colorIndex];
|
|
207
|
+
}
|
|
208
|
+
// Generate a color for indices beyond the palette
|
|
209
|
+
const hue = (colorIndex * 137.508) % 360;
|
|
210
|
+
const saturation = 60 + (colorIndex % 20);
|
|
211
|
+
const lightness = 50 + (colorIndex % 10);
|
|
212
|
+
return {
|
|
213
|
+
border: `hsl(${hue}, ${saturation}%, ${lightness}%)`,
|
|
214
|
+
fill: `hsla(${hue}, ${saturation}%, ${lightness}%, 0.15)`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Render a formula range highlight (cell references in formulas)
|
|
220
|
+
*/
|
|
221
|
+
private renderFormulaRange(
|
|
222
|
+
ctx: CanvasRenderingContext2D,
|
|
223
|
+
formulaRange: FormulaRangeHighlight,
|
|
224
|
+
viewport: Viewport,
|
|
225
|
+
rowHeights: Map<number, number>,
|
|
226
|
+
colWidths: Map<number, number>,
|
|
227
|
+
defaultRowHeight: number,
|
|
228
|
+
defaultColWidth: number,
|
|
229
|
+
headerWidth: number,
|
|
230
|
+
headerHeight: number,
|
|
231
|
+
hiddenRows?: Set<number>,
|
|
232
|
+
hiddenCols?: Set<number>,
|
|
233
|
+
frozenRows?: number,
|
|
234
|
+
frozenCols?: number
|
|
235
|
+
): void {
|
|
236
|
+
const range: Range = {
|
|
237
|
+
startRow: formulaRange.startRow,
|
|
238
|
+
endRow: formulaRange.endRow,
|
|
239
|
+
startCol: formulaRange.startCol,
|
|
240
|
+
endCol: formulaRange.endCol,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const bounds = this.getRangeBounds(
|
|
244
|
+
range,
|
|
245
|
+
viewport,
|
|
246
|
+
rowHeights,
|
|
247
|
+
colWidths,
|
|
248
|
+
defaultRowHeight,
|
|
249
|
+
defaultColWidth,
|
|
250
|
+
headerWidth,
|
|
251
|
+
headerHeight,
|
|
252
|
+
hiddenRows,
|
|
253
|
+
hiddenCols,
|
|
254
|
+
frozenRows,
|
|
255
|
+
frozenCols
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (!bounds) return;
|
|
259
|
+
|
|
260
|
+
const color = this.getFormulaRangeColor(formulaRange.colorIndex);
|
|
261
|
+
|
|
262
|
+
// Formula range fill
|
|
263
|
+
ctx.fillStyle = color.fill;
|
|
264
|
+
ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
|
|
265
|
+
|
|
266
|
+
// Formula range border
|
|
267
|
+
ctx.strokeStyle = color.border;
|
|
268
|
+
ctx.lineWidth = 2;
|
|
269
|
+
ctx.strokeRect(
|
|
270
|
+
bounds.x + 1,
|
|
271
|
+
bounds.y + 1,
|
|
272
|
+
bounds.width - 2,
|
|
273
|
+
bounds.height - 2
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Calculate bounds for a range in canvas coordinates
|
|
279
|
+
*/
|
|
280
|
+
private getRangeBounds(
|
|
281
|
+
range: Range,
|
|
282
|
+
viewport: Viewport,
|
|
283
|
+
rowHeights: Map<number, number>,
|
|
284
|
+
colWidths: Map<number, number>,
|
|
285
|
+
defaultRowHeight: number,
|
|
286
|
+
defaultColWidth: number,
|
|
287
|
+
headerWidth: number,
|
|
288
|
+
headerHeight: number,
|
|
289
|
+
hiddenRows?: Set<number>,
|
|
290
|
+
hiddenCols?: Set<number>,
|
|
291
|
+
frozenRows?: number,
|
|
292
|
+
frozenCols?: number
|
|
293
|
+
): Rect | null {
|
|
294
|
+
const { scrollTop, scrollLeft } = viewport;
|
|
295
|
+
const hidRows = hiddenRows ?? new Set<number>();
|
|
296
|
+
const hidCols = hiddenCols ?? new Set<number>();
|
|
297
|
+
const numFrozenRows = frozenRows ?? 0;
|
|
298
|
+
const numFrozenCols = frozenCols ?? 0;
|
|
299
|
+
|
|
300
|
+
// Determine if this range is in frozen area
|
|
301
|
+
const isRowFrozen = range.startRow < numFrozenRows;
|
|
302
|
+
const isColFrozen = range.startCol < numFrozenCols;
|
|
303
|
+
|
|
304
|
+
// Calculate x position (skip hidden columns)
|
|
305
|
+
// If column is frozen, accumulate from 0; otherwise accumulate from frozenCols
|
|
306
|
+
let x = headerWidth;
|
|
307
|
+
const startColAccumulation = isColFrozen ? 0 : numFrozenCols;
|
|
308
|
+
for (let c = startColAccumulation; c < range.startCol; c++) {
|
|
309
|
+
if (!hidCols.has(c)) {
|
|
310
|
+
x += colWidths.get(c) ?? defaultColWidth;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Only apply scroll if not in frozen columns
|
|
314
|
+
if (!isColFrozen) {
|
|
315
|
+
x -= scrollLeft;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Calculate y position (skip hidden rows)
|
|
319
|
+
// If row is frozen, accumulate from 0; otherwise accumulate from frozenRows
|
|
320
|
+
let y = headerHeight;
|
|
321
|
+
const startRowAccumulation = isRowFrozen ? 0 : numFrozenRows;
|
|
322
|
+
for (let r = startRowAccumulation; r < range.startRow; r++) {
|
|
323
|
+
if (!hidRows.has(r)) {
|
|
324
|
+
y += rowHeights.get(r) ?? defaultRowHeight;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Only apply scroll if not in frozen rows
|
|
328
|
+
if (!isRowFrozen) {
|
|
329
|
+
y -= scrollTop;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Calculate width (skip hidden columns)
|
|
333
|
+
let width = 0;
|
|
334
|
+
for (let c = range.startCol; c <= range.endCol; c++) {
|
|
335
|
+
if (!hidCols.has(c)) {
|
|
336
|
+
width += colWidths.get(c) ?? defaultColWidth;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Calculate height (skip hidden rows)
|
|
341
|
+
let height = 0;
|
|
342
|
+
for (let r = range.startRow; r <= range.endRow; r++) {
|
|
343
|
+
if (!hidRows.has(r)) {
|
|
344
|
+
height += rowHeights.get(r) ?? defaultRowHeight;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Return null if entire range is hidden
|
|
349
|
+
if (width === 0 || height === 0) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { x, y, width, height };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Render a selection range (highlight fill and border)
|
|
358
|
+
*/
|
|
359
|
+
private renderSelectionRange(
|
|
360
|
+
ctx: CanvasRenderingContext2D,
|
|
361
|
+
range: Range,
|
|
362
|
+
viewport: Viewport,
|
|
363
|
+
rowHeights: Map<number, number>,
|
|
364
|
+
colWidths: Map<number, number>,
|
|
365
|
+
defaultRowHeight: number,
|
|
366
|
+
defaultColWidth: number,
|
|
367
|
+
headerWidth: number,
|
|
368
|
+
headerHeight: number,
|
|
369
|
+
hiddenRows?: Set<number>,
|
|
370
|
+
hiddenCols?: Set<number>,
|
|
371
|
+
frozenRows?: number,
|
|
372
|
+
frozenCols?: number
|
|
373
|
+
): void {
|
|
374
|
+
const bounds = this.getRangeBounds(
|
|
375
|
+
range,
|
|
376
|
+
viewport,
|
|
377
|
+
rowHeights,
|
|
378
|
+
colWidths,
|
|
379
|
+
defaultRowHeight,
|
|
380
|
+
defaultColWidth,
|
|
381
|
+
headerWidth,
|
|
382
|
+
headerHeight,
|
|
383
|
+
hiddenRows,
|
|
384
|
+
hiddenCols,
|
|
385
|
+
frozenRows,
|
|
386
|
+
frozenCols
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
if (!bounds) return;
|
|
390
|
+
|
|
391
|
+
// Selection fill
|
|
392
|
+
ctx.fillStyle = this.theme.selectionFillColor;
|
|
393
|
+
ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
|
|
394
|
+
|
|
395
|
+
// Selection border
|
|
396
|
+
ctx.strokeStyle = this.theme.selectionBorderColor;
|
|
397
|
+
ctx.lineWidth = this.theme.selectionBorderWidth;
|
|
398
|
+
ctx.strokeRect(
|
|
399
|
+
bounds.x + 0.5,
|
|
400
|
+
bounds.y + 0.5,
|
|
401
|
+
bounds.width - 1,
|
|
402
|
+
bounds.height - 1
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Render the active cell border
|
|
408
|
+
*/
|
|
409
|
+
private renderActiveCell(
|
|
410
|
+
ctx: CanvasRenderingContext2D,
|
|
411
|
+
cell: CellPosition,
|
|
412
|
+
viewport: Viewport,
|
|
413
|
+
rowHeights: Map<number, number>,
|
|
414
|
+
colWidths: Map<number, number>,
|
|
415
|
+
defaultRowHeight: number,
|
|
416
|
+
defaultColWidth: number,
|
|
417
|
+
headerWidth: number,
|
|
418
|
+
headerHeight: number,
|
|
419
|
+
hiddenRows?: Set<number>,
|
|
420
|
+
hiddenCols?: Set<number>,
|
|
421
|
+
frozenRows?: number,
|
|
422
|
+
frozenCols?: number
|
|
423
|
+
): void {
|
|
424
|
+
const range: Range = {
|
|
425
|
+
startRow: cell.row,
|
|
426
|
+
endRow: cell.row,
|
|
427
|
+
startCol: cell.col,
|
|
428
|
+
endCol: cell.col,
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const bounds = this.getRangeBounds(
|
|
432
|
+
range,
|
|
433
|
+
viewport,
|
|
434
|
+
rowHeights,
|
|
435
|
+
colWidths,
|
|
436
|
+
defaultRowHeight,
|
|
437
|
+
defaultColWidth,
|
|
438
|
+
headerWidth,
|
|
439
|
+
headerHeight,
|
|
440
|
+
hiddenRows,
|
|
441
|
+
hiddenCols,
|
|
442
|
+
frozenRows,
|
|
443
|
+
frozenCols
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
if (!bounds) return;
|
|
447
|
+
|
|
448
|
+
// Active cell border only - cell content is already rendered by CellRenderer
|
|
449
|
+
ctx.strokeStyle = this.theme.activeCellBorderColor;
|
|
450
|
+
ctx.lineWidth = this.theme.activeCellBorderWidth;
|
|
451
|
+
ctx.strokeRect(
|
|
452
|
+
bounds.x + 1,
|
|
453
|
+
bounds.y + 1,
|
|
454
|
+
bounds.width - 2,
|
|
455
|
+
bounds.height - 2
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Render the fill handle (small square at bottom-right of selection)
|
|
461
|
+
*/
|
|
462
|
+
private renderFillHandle(
|
|
463
|
+
ctx: CanvasRenderingContext2D,
|
|
464
|
+
range: Range,
|
|
465
|
+
viewport: Viewport,
|
|
466
|
+
rowHeights: Map<number, number>,
|
|
467
|
+
colWidths: Map<number, number>,
|
|
468
|
+
defaultRowHeight: number,
|
|
469
|
+
defaultColWidth: number,
|
|
470
|
+
headerWidth: number,
|
|
471
|
+
headerHeight: number,
|
|
472
|
+
hiddenRows?: Set<number>,
|
|
473
|
+
hiddenCols?: Set<number>,
|
|
474
|
+
frozenRows?: number,
|
|
475
|
+
frozenCols?: number
|
|
476
|
+
): void {
|
|
477
|
+
const bounds = this.getRangeBounds(
|
|
478
|
+
range,
|
|
479
|
+
viewport,
|
|
480
|
+
rowHeights,
|
|
481
|
+
colWidths,
|
|
482
|
+
defaultRowHeight,
|
|
483
|
+
defaultColWidth,
|
|
484
|
+
headerWidth,
|
|
485
|
+
headerHeight,
|
|
486
|
+
hiddenRows,
|
|
487
|
+
hiddenCols,
|
|
488
|
+
frozenRows,
|
|
489
|
+
frozenCols
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
if (!bounds) return;
|
|
493
|
+
|
|
494
|
+
const handleSize = this.theme.fillHandleSize;
|
|
495
|
+
const handleX = bounds.x + bounds.width - handleSize / 2;
|
|
496
|
+
const handleY = bounds.y + bounds.height - handleSize / 2;
|
|
497
|
+
|
|
498
|
+
// Fill handle square
|
|
499
|
+
ctx.fillStyle = this.theme.fillHandleColor;
|
|
500
|
+
ctx.fillRect(
|
|
501
|
+
handleX - handleSize / 2,
|
|
502
|
+
handleY - handleSize / 2,
|
|
503
|
+
handleSize,
|
|
504
|
+
handleSize
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
// White border around fill handle
|
|
508
|
+
ctx.strokeStyle = '#ffffff';
|
|
509
|
+
ctx.lineWidth = 1;
|
|
510
|
+
ctx.strokeRect(
|
|
511
|
+
handleX - handleSize / 2,
|
|
512
|
+
handleY - handleSize / 2,
|
|
513
|
+
handleSize,
|
|
514
|
+
handleSize
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Render copy/paste marching ants border
|
|
520
|
+
*/
|
|
521
|
+
renderMarchingAnts(
|
|
522
|
+
ctx: CanvasRenderingContext2D,
|
|
523
|
+
range: Range,
|
|
524
|
+
viewport: Viewport,
|
|
525
|
+
rowHeights: Map<number, number>,
|
|
526
|
+
colWidths: Map<number, number>,
|
|
527
|
+
defaultRowHeight: number,
|
|
528
|
+
defaultColWidth: number,
|
|
529
|
+
headerWidth: number,
|
|
530
|
+
headerHeight: number,
|
|
531
|
+
offset: number
|
|
532
|
+
): void {
|
|
533
|
+
const bounds = this.getRangeBounds(
|
|
534
|
+
range,
|
|
535
|
+
viewport,
|
|
536
|
+
rowHeights,
|
|
537
|
+
colWidths,
|
|
538
|
+
defaultRowHeight,
|
|
539
|
+
defaultColWidth,
|
|
540
|
+
headerWidth,
|
|
541
|
+
headerHeight
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
if (!bounds) return;
|
|
545
|
+
|
|
546
|
+
ctx.strokeStyle = this.theme.selectionBorderColor;
|
|
547
|
+
ctx.lineWidth = 1;
|
|
548
|
+
ctx.setLineDash([4, 4]);
|
|
549
|
+
ctx.lineDashOffset = offset;
|
|
550
|
+
|
|
551
|
+
ctx.strokeRect(
|
|
552
|
+
bounds.x + 0.5,
|
|
553
|
+
bounds.y + 0.5,
|
|
554
|
+
bounds.width - 1,
|
|
555
|
+
bounds.height - 1
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
ctx.setLineDash([]);
|
|
559
|
+
ctx.lineDashOffset = 0;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Render fill preview during fill handle drag
|
|
564
|
+
*/
|
|
565
|
+
renderFillPreview(
|
|
566
|
+
ctx: CanvasRenderingContext2D,
|
|
567
|
+
range: Range,
|
|
568
|
+
viewport: Viewport,
|
|
569
|
+
rowHeights: Map<number, number>,
|
|
570
|
+
colWidths: Map<number, number>,
|
|
571
|
+
defaultRowHeight: number,
|
|
572
|
+
defaultColWidth: number,
|
|
573
|
+
headerWidth: number,
|
|
574
|
+
headerHeight: number
|
|
575
|
+
): void {
|
|
576
|
+
const bounds = this.getRangeBounds(
|
|
577
|
+
range,
|
|
578
|
+
viewport,
|
|
579
|
+
rowHeights,
|
|
580
|
+
colWidths,
|
|
581
|
+
defaultRowHeight,
|
|
582
|
+
defaultColWidth,
|
|
583
|
+
headerWidth,
|
|
584
|
+
headerHeight
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
if (!bounds) return;
|
|
588
|
+
|
|
589
|
+
// Dashed border for fill preview
|
|
590
|
+
ctx.strokeStyle = this.theme.selectionBorderColor;
|
|
591
|
+
ctx.lineWidth = 1;
|
|
592
|
+
ctx.setLineDash([3, 3]);
|
|
593
|
+
|
|
594
|
+
ctx.strokeRect(
|
|
595
|
+
bounds.x + 0.5,
|
|
596
|
+
bounds.y + 0.5,
|
|
597
|
+
bounds.width - 1,
|
|
598
|
+
bounds.height - 1
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
ctx.setLineDash([]);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|