@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,73 @@
|
|
|
1
|
+
// Event emitter for workbook events
|
|
2
|
+
|
|
3
|
+
import type { EventData, EventHandler, EventType } from './types';
|
|
4
|
+
|
|
5
|
+
export class EventEmitter {
|
|
6
|
+
private handlers: Map<EventType, Set<EventHandler>> = new Map();
|
|
7
|
+
private batchQueue: EventData[] = [];
|
|
8
|
+
private isBatching = false;
|
|
9
|
+
|
|
10
|
+
on(event: EventType, handler: EventHandler): () => void {
|
|
11
|
+
if (!this.handlers.has(event)) {
|
|
12
|
+
this.handlers.set(event, new Set());
|
|
13
|
+
}
|
|
14
|
+
this.handlers.get(event)!.add(handler);
|
|
15
|
+
|
|
16
|
+
// Return unsubscribe function
|
|
17
|
+
return () => {
|
|
18
|
+
this.handlers.get(event)?.delete(handler);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
off(event: EventType, handler: EventHandler): void {
|
|
23
|
+
this.handlers.get(event)?.delete(handler);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
emit(event: EventType, payload: unknown): void {
|
|
27
|
+
const data: EventData = { type: event, payload };
|
|
28
|
+
|
|
29
|
+
if (this.isBatching) {
|
|
30
|
+
this.batchQueue.push(data);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.dispatch(data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private dispatch(data: EventData): void {
|
|
38
|
+
const handlers = this.handlers.get(data.type);
|
|
39
|
+
if (handlers) {
|
|
40
|
+
for (const handler of handlers) {
|
|
41
|
+
try {
|
|
42
|
+
handler(data);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error in event handler:', error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
batch(operations: () => void): void {
|
|
51
|
+
this.isBatching = true;
|
|
52
|
+
this.batchQueue = [];
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
operations();
|
|
56
|
+
} finally {
|
|
57
|
+
this.isBatching = false;
|
|
58
|
+
const events = [...this.batchQueue];
|
|
59
|
+
this.batchQueue = [];
|
|
60
|
+
|
|
61
|
+
// Dispatch all batched events
|
|
62
|
+
for (const event of events) {
|
|
63
|
+
this.dispatch(event);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
clear(): void {
|
|
69
|
+
this.handlers.clear();
|
|
70
|
+
this.batchQueue = [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Sheet } from '../types';
|
|
2
|
+
import type { WorkbookImpl } from '../workbook';
|
|
3
|
+
|
|
4
|
+
export function exportToCSV(workbook: WorkbookImpl, sheetId?: string): string {
|
|
5
|
+
const sheet = workbook.getSheet(sheetId);
|
|
6
|
+
const rows: string[][] = [];
|
|
7
|
+
|
|
8
|
+
// Find the maximum row and column with data
|
|
9
|
+
let maxRow = 0;
|
|
10
|
+
let maxCol = 0;
|
|
11
|
+
|
|
12
|
+
for (const [key] of sheet.cells) {
|
|
13
|
+
const [row, col] = key.split(':').map(Number);
|
|
14
|
+
maxRow = Math.max(maxRow, row);
|
|
15
|
+
maxCol = Math.max(maxCol, col);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Generate CSV rows
|
|
19
|
+
for (let row = 0; row <= maxRow; row++) {
|
|
20
|
+
const csvRow: string[] = [];
|
|
21
|
+
for (let col = 0; col <= maxCol; col++) {
|
|
22
|
+
const cell = sheet.getCell(row, col);
|
|
23
|
+
let value = '';
|
|
24
|
+
|
|
25
|
+
if (cell) {
|
|
26
|
+
if (cell.formula) {
|
|
27
|
+
// For formulas, export the formula itself (or could export calculated value)
|
|
28
|
+
value = cell.formula;
|
|
29
|
+
} else {
|
|
30
|
+
value = cell.value?.toString() || '';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Escape CSV value
|
|
35
|
+
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
|
36
|
+
value = `"${value.replace(/"/g, '""')}"`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
csvRow.push(value);
|
|
40
|
+
}
|
|
41
|
+
rows.push(csvRow);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return rows.map((row) => row.join(',')).join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function importFromCSV(csv: string, sheet: Sheet): void {
|
|
48
|
+
const lines = csv.split('\n');
|
|
49
|
+
let row = 0;
|
|
50
|
+
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
if (!line.trim()) {
|
|
53
|
+
row++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Simple CSV parsing (doesn't handle all edge cases)
|
|
58
|
+
const values = parseCSVLine(line);
|
|
59
|
+
values.forEach((value, col) => {
|
|
60
|
+
if (value.trim()) {
|
|
61
|
+
sheet.setCellValue(row, col, value);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
row++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseCSVLine(line: string): string[] {
|
|
70
|
+
const values: string[] = [];
|
|
71
|
+
let current = '';
|
|
72
|
+
let inQuotes = false;
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < line.length; i++) {
|
|
75
|
+
const char = line[i];
|
|
76
|
+
const nextChar = line[i + 1];
|
|
77
|
+
|
|
78
|
+
if (char === '"') {
|
|
79
|
+
if (inQuotes && nextChar === '"') {
|
|
80
|
+
// Escaped quote
|
|
81
|
+
current += '"';
|
|
82
|
+
i++; // Skip next quote
|
|
83
|
+
} else {
|
|
84
|
+
// Toggle quote state
|
|
85
|
+
inQuotes = !inQuotes;
|
|
86
|
+
}
|
|
87
|
+
} else if (char === ',' && !inQuotes) {
|
|
88
|
+
// End of value
|
|
89
|
+
values.push(current);
|
|
90
|
+
current = '';
|
|
91
|
+
} else {
|
|
92
|
+
current += char;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add last value
|
|
97
|
+
values.push(current);
|
|
98
|
+
|
|
99
|
+
return values;
|
|
100
|
+
}
|
|
101
|
+
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Filter functionality for spreadsheet sheets
|
|
2
|
+
|
|
3
|
+
import type { ColumnFilter, FilterCriteria, Sheet } from '../types';
|
|
4
|
+
import { getCellKey } from '../utils/cell-key';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Manages filtering operations for a spreadsheet sheet
|
|
8
|
+
*/
|
|
9
|
+
export class FilterManager {
|
|
10
|
+
/**
|
|
11
|
+
* Apply filters to get visible row indices
|
|
12
|
+
* @param sheet The sheet to filter
|
|
13
|
+
* @param filters Map of column filters
|
|
14
|
+
* @param dataRange Optional range to filter, defaults to data rows (starting from row 1, assuming row 0 is header)
|
|
15
|
+
* @returns Set of visible row indices
|
|
16
|
+
*/
|
|
17
|
+
static getFilteredRows(
|
|
18
|
+
sheet: Sheet,
|
|
19
|
+
filters: Map<number, ColumnFilter>,
|
|
20
|
+
dataRange?: { startRow: number; endRow: number }
|
|
21
|
+
): Set<number> {
|
|
22
|
+
if (filters.size === 0) {
|
|
23
|
+
// No filters, all rows visible (including header row 0)
|
|
24
|
+
const visibleRows = new Set<number>();
|
|
25
|
+
const startRow = dataRange?.startRow ?? 0;
|
|
26
|
+
const endRow = dataRange?.endRow ?? sheet.rowCount - 1;
|
|
27
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
28
|
+
visibleRows.add(row);
|
|
29
|
+
}
|
|
30
|
+
return visibleRows;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Determine data range to filter (default to row 1+, assuming row 0 is header)
|
|
34
|
+
const startRow = dataRange?.startRow ?? Math.max(1, this.detectDataStartRow(sheet));
|
|
35
|
+
const endRow = dataRange?.endRow ?? this.detectDataEndRow(sheet);
|
|
36
|
+
|
|
37
|
+
const visibleRows = new Set<number>();
|
|
38
|
+
|
|
39
|
+
// Always include the header row (row 0) if we're starting from row 0 or earlier
|
|
40
|
+
if (dataRange?.startRow === undefined || dataRange.startRow <= 0) {
|
|
41
|
+
visibleRows.add(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check each data row against all filters
|
|
45
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
46
|
+
let isVisible = true;
|
|
47
|
+
|
|
48
|
+
for (const [, filter] of filters) {
|
|
49
|
+
const cell = sheet.cells.get(getCellKey(row, filter.column));
|
|
50
|
+
const cellValue = cell?.value ?? null;
|
|
51
|
+
|
|
52
|
+
if (!this.matchesFilter(cellValue, filter.criteria)) {
|
|
53
|
+
isVisible = false;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (isVisible) {
|
|
59
|
+
visibleRows.add(row);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return visibleRows;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a cell value matches a filter criteria
|
|
68
|
+
* @param cellValue The cell value to check
|
|
69
|
+
* @param criteria The filter criteria
|
|
70
|
+
* @returns true if the value matches the filter
|
|
71
|
+
*/
|
|
72
|
+
private static matchesFilter(cellValue: unknown, criteria: FilterCriteria): boolean {
|
|
73
|
+
// Handle null/undefined values
|
|
74
|
+
if (cellValue === null || cellValue === undefined) {
|
|
75
|
+
// Null values only match if criteria allows empty values
|
|
76
|
+
return criteria.type === 'equals' && criteria.value === '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Convert cell value to string for text operations
|
|
80
|
+
const strValue = String(cellValue) as string;
|
|
81
|
+
|
|
82
|
+
switch (criteria.type) {
|
|
83
|
+
case 'equals':
|
|
84
|
+
return strValue === String(criteria.value);
|
|
85
|
+
|
|
86
|
+
case 'notEquals':
|
|
87
|
+
return strValue !== String(criteria.value);
|
|
88
|
+
|
|
89
|
+
case 'contains':
|
|
90
|
+
return strValue.toLowerCase().includes(criteria.value.toLowerCase());
|
|
91
|
+
|
|
92
|
+
case 'notContains':
|
|
93
|
+
return !strValue.toLowerCase().includes(criteria.value.toLowerCase());
|
|
94
|
+
|
|
95
|
+
case 'startsWith':
|
|
96
|
+
return strValue.toLowerCase().startsWith(criteria.value.toLowerCase());
|
|
97
|
+
|
|
98
|
+
case 'endsWith':
|
|
99
|
+
return strValue.toLowerCase().endsWith(criteria.value.toLowerCase());
|
|
100
|
+
|
|
101
|
+
case 'greaterThan': {
|
|
102
|
+
const numValue = Number(cellValue);
|
|
103
|
+
return !isNaN(numValue) && numValue > criteria.value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'lessThan': {
|
|
107
|
+
const numValue = Number(cellValue);
|
|
108
|
+
return !isNaN(numValue) && numValue < criteria.value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
case 'greaterThanOrEqual': {
|
|
112
|
+
const numValue = Number(cellValue);
|
|
113
|
+
return !isNaN(numValue) && numValue >= criteria.value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'lessThanOrEqual': {
|
|
117
|
+
const numValue = Number(cellValue);
|
|
118
|
+
return !isNaN(numValue) && numValue <= criteria.value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case 'between': {
|
|
122
|
+
const numValue = Number(cellValue);
|
|
123
|
+
return !isNaN(numValue) && numValue >= criteria.min && numValue <= criteria.max;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case 'custom':
|
|
127
|
+
return criteria.values.has(cellValue as string | number);
|
|
128
|
+
|
|
129
|
+
default:
|
|
130
|
+
return true; // Unknown criteria type, show the row
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get unique values in a column for filter dropdowns
|
|
136
|
+
* @param sheet The sheet to analyze
|
|
137
|
+
* @param column Column index
|
|
138
|
+
* @param dataRange Optional range to analyze, defaults to data rows (starting from row 1, assuming row 0 is header)
|
|
139
|
+
* @returns Set of unique values in the column
|
|
140
|
+
*/
|
|
141
|
+
static getUniqueColumnValues(
|
|
142
|
+
sheet: Sheet,
|
|
143
|
+
column: number,
|
|
144
|
+
dataRange?: { startRow: number; endRow: number }
|
|
145
|
+
): Set<string | number> {
|
|
146
|
+
const uniqueValues = new Set<string | number>();
|
|
147
|
+
|
|
148
|
+
// Default to data rows only (skip header row 0), unless explicitly specified
|
|
149
|
+
const startRow = dataRange?.startRow ?? Math.max(1, this.detectDataStartRow(sheet));
|
|
150
|
+
const endRow = dataRange?.endRow ?? this.detectDataEndRow(sheet);
|
|
151
|
+
|
|
152
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
153
|
+
const cell = sheet.cells.get(getCellKey(row, column));
|
|
154
|
+
if (cell?.value !== null && cell?.value !== undefined) {
|
|
155
|
+
uniqueValues.add(cell.value as string | number);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return uniqueValues;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Detect the first row that contains data (skip empty rows at the top)
|
|
164
|
+
*/
|
|
165
|
+
private static detectDataStartRow(sheet: Sheet): number {
|
|
166
|
+
// Start from row 0, find first non-empty row
|
|
167
|
+
for (let row = 0; row < sheet.rowCount; row++) {
|
|
168
|
+
for (let col = 0; col < sheet.colCount; col++) {
|
|
169
|
+
const cell = sheet.cells.get(getCellKey(row, col));
|
|
170
|
+
if (cell && (cell.value !== null || cell.formula)) {
|
|
171
|
+
return row;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Detect the last row that contains data (skip empty rows at the bottom)
|
|
180
|
+
*/
|
|
181
|
+
private static detectDataEndRow(sheet: Sheet): number {
|
|
182
|
+
// Start from the bottom, find last non-empty row
|
|
183
|
+
for (let row = sheet.rowCount - 1; row >= 0; row--) {
|
|
184
|
+
for (let col = 0; col < sheet.colCount; col++) {
|
|
185
|
+
const cell = sheet.cells.get(getCellKey(row, col));
|
|
186
|
+
if (cell && (cell.value !== null || cell.formula)) {
|
|
187
|
+
return row;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return Math.max(0, sheet.rowCount - 1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check if a column has an active filter
|
|
196
|
+
* @param column Column index
|
|
197
|
+
* @param filters Current filters map
|
|
198
|
+
* @returns true if column has a filter
|
|
199
|
+
*/
|
|
200
|
+
static hasFilter(column: number, filters: Map<number, ColumnFilter>): boolean {
|
|
201
|
+
return filters.has(column);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get filter type based on column data
|
|
206
|
+
* @param sheet The sheet to analyze
|
|
207
|
+
* @param column Column index
|
|
208
|
+
* @returns 'text', 'number', or 'date' based on data analysis
|
|
209
|
+
*/
|
|
210
|
+
static detectColumnType(sheet: Sheet, column: number): 'text' | 'number' | 'date' {
|
|
211
|
+
const uniqueValues = this.getUniqueColumnValues(sheet, column);
|
|
212
|
+
let hasNumbers = false;
|
|
213
|
+
let hasDates = false;
|
|
214
|
+
|
|
215
|
+
for (const value of uniqueValues) {
|
|
216
|
+
if (typeof value === 'number') {
|
|
217
|
+
hasNumbers = true;
|
|
218
|
+
} else if (typeof value === 'string') {
|
|
219
|
+
// Check if string looks like a date
|
|
220
|
+
const date = new Date(value);
|
|
221
|
+
if (!isNaN(date.getTime()) && value.match(/\d{1,2}[-/]\d{1,2}[-/]\d{4}|\d{4}[-/]\d{1,2}[-/]\d{1,2}/)) {
|
|
222
|
+
hasDates = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (hasDates) return 'date';
|
|
228
|
+
if (hasNumbers) return 'number';
|
|
229
|
+
return 'text';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Freeze Panes Utilities
|
|
2
|
+
// Handles calculation and management of frozen rows/columns
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for freeze panes
|
|
6
|
+
*/
|
|
7
|
+
export interface FreezeConfig {
|
|
8
|
+
frozenRows: number;
|
|
9
|
+
frozenCols: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cached dimensions for frozen areas
|
|
14
|
+
*/
|
|
15
|
+
export interface FreezeDimensions {
|
|
16
|
+
/** Total width of all frozen columns */
|
|
17
|
+
frozenWidth: number;
|
|
18
|
+
/** Total height of all frozen rows */
|
|
19
|
+
frozenHeight: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The 4 regions created by freeze panes
|
|
24
|
+
*/
|
|
25
|
+
export type FreezeRegion =
|
|
26
|
+
| 'top-left' // Frozen rows AND cols - never scrolls
|
|
27
|
+
| 'top' // Frozen rows only - scrolls horizontally
|
|
28
|
+
| 'left' // Frozen cols only - scrolls vertically
|
|
29
|
+
| 'main'; // Regular scrollable area
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Calculate the total dimensions of frozen areas
|
|
33
|
+
*/
|
|
34
|
+
export function calculateFreezeDimensions(
|
|
35
|
+
frozenRows: number,
|
|
36
|
+
frozenCols: number,
|
|
37
|
+
rowHeights: Map<number, number>,
|
|
38
|
+
colWidths: Map<number, number>,
|
|
39
|
+
defaultRowHeight: number,
|
|
40
|
+
defaultColWidth: number,
|
|
41
|
+
hiddenRows?: Set<number>,
|
|
42
|
+
hiddenCols?: Set<number>
|
|
43
|
+
): FreezeDimensions {
|
|
44
|
+
const hidRows = hiddenRows ?? new Set<number>();
|
|
45
|
+
const hidCols = hiddenCols ?? new Set<number>();
|
|
46
|
+
|
|
47
|
+
// Calculate frozen width (sum of frozen column widths, excluding hidden)
|
|
48
|
+
let frozenWidth = 0;
|
|
49
|
+
for (let c = 0; c < frozenCols; c++) {
|
|
50
|
+
if (!hidCols.has(c)) {
|
|
51
|
+
frozenWidth += colWidths.get(c) ?? defaultColWidth;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Calculate frozen height (sum of frozen row heights, excluding hidden)
|
|
56
|
+
let frozenHeight = 0;
|
|
57
|
+
for (let r = 0; r < frozenRows; r++) {
|
|
58
|
+
if (!hidRows.has(r)) {
|
|
59
|
+
frozenHeight += rowHeights.get(r) ?? defaultRowHeight;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { frozenWidth, frozenHeight };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Determine which freeze region a canvas point falls into
|
|
68
|
+
*
|
|
69
|
+
* @param x - Canvas x coordinate (relative to canvas origin, after headers)
|
|
70
|
+
* @param y - Canvas y coordinate (relative to canvas origin, after headers)
|
|
71
|
+
* @param frozenWidth - Total width of frozen columns
|
|
72
|
+
* @param frozenHeight - Total height of frozen rows
|
|
73
|
+
* @param headerWidth - Width of row headers
|
|
74
|
+
* @param headerHeight - Height of column headers
|
|
75
|
+
*/
|
|
76
|
+
export function getRegionAtPoint(
|
|
77
|
+
x: number,
|
|
78
|
+
y: number,
|
|
79
|
+
frozenWidth: number,
|
|
80
|
+
frozenHeight: number,
|
|
81
|
+
headerWidth: number,
|
|
82
|
+
headerHeight: number
|
|
83
|
+
): FreezeRegion {
|
|
84
|
+
const inFrozenCols = x <= headerWidth + frozenWidth;
|
|
85
|
+
const inFrozenRows = y <= headerHeight + frozenHeight;
|
|
86
|
+
|
|
87
|
+
if (inFrozenRows && inFrozenCols) {
|
|
88
|
+
return 'top-left';
|
|
89
|
+
} else if (inFrozenRows) {
|
|
90
|
+
return 'top';
|
|
91
|
+
} else if (inFrozenCols) {
|
|
92
|
+
return 'left';
|
|
93
|
+
} else {
|
|
94
|
+
return 'main';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the effective scroll offset for a given region
|
|
100
|
+
*/
|
|
101
|
+
export function getScrollForRegion(
|
|
102
|
+
region: FreezeRegion,
|
|
103
|
+
scrollTop: number,
|
|
104
|
+
scrollLeft: number
|
|
105
|
+
): { effectiveScrollTop: number; effectiveScrollLeft: number } {
|
|
106
|
+
switch (region) {
|
|
107
|
+
case 'top-left':
|
|
108
|
+
// Never scrolls
|
|
109
|
+
return { effectiveScrollTop: 0, effectiveScrollLeft: 0 };
|
|
110
|
+
case 'top':
|
|
111
|
+
// Only scrolls horizontally
|
|
112
|
+
return { effectiveScrollTop: 0, effectiveScrollLeft: scrollLeft };
|
|
113
|
+
case 'left':
|
|
114
|
+
// Only scrolls vertically
|
|
115
|
+
return { effectiveScrollTop: scrollTop, effectiveScrollLeft: 0 };
|
|
116
|
+
case 'main':
|
|
117
|
+
// Scrolls both ways
|
|
118
|
+
return { effectiveScrollTop: scrollTop, effectiveScrollLeft: scrollLeft };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Convert a canvas point to grid coordinates, accounting for freeze regions
|
|
124
|
+
*
|
|
125
|
+
* @param canvasX - X position on canvas (including headers)
|
|
126
|
+
* @param canvasY - Y position on canvas (including headers)
|
|
127
|
+
* @param scrollTop - Current vertical scroll position
|
|
128
|
+
* @param scrollLeft - Current horizontal scroll position
|
|
129
|
+
* @param frozenWidth - Total width of frozen columns
|
|
130
|
+
* @param frozenHeight - Total height of frozen rows
|
|
131
|
+
* @param headerWidth - Width of row headers
|
|
132
|
+
* @param headerHeight - Height of column headers
|
|
133
|
+
*/
|
|
134
|
+
export function canvasToGridCoordinates(
|
|
135
|
+
canvasX: number,
|
|
136
|
+
canvasY: number,
|
|
137
|
+
scrollTop: number,
|
|
138
|
+
scrollLeft: number,
|
|
139
|
+
frozenWidth: number,
|
|
140
|
+
frozenHeight: number,
|
|
141
|
+
headerWidth: number,
|
|
142
|
+
headerHeight: number
|
|
143
|
+
): { gridX: number; gridY: number } {
|
|
144
|
+
const region = getRegionAtPoint(
|
|
145
|
+
canvasX, canvasY,
|
|
146
|
+
frozenWidth, frozenHeight,
|
|
147
|
+
headerWidth, headerHeight
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const { effectiveScrollTop, effectiveScrollLeft } = getScrollForRegion(
|
|
151
|
+
region, scrollTop, scrollLeft
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Convert to grid coordinates based on region
|
|
155
|
+
let gridX: number;
|
|
156
|
+
let gridY: number;
|
|
157
|
+
|
|
158
|
+
if (region === 'top-left' || region === 'left') {
|
|
159
|
+
// In frozen columns - no horizontal scroll adjustment
|
|
160
|
+
gridX = canvasX - headerWidth;
|
|
161
|
+
} else {
|
|
162
|
+
// In scrollable columns - apply scroll offset
|
|
163
|
+
gridX = canvasX - headerWidth + effectiveScrollLeft;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (region === 'top-left' || region === 'top') {
|
|
167
|
+
// In frozen rows - no vertical scroll adjustment
|
|
168
|
+
gridY = canvasY - headerHeight;
|
|
169
|
+
} else {
|
|
170
|
+
// In scrollable rows - apply scroll offset
|
|
171
|
+
gridY = canvasY - headerHeight + effectiveScrollTop;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { gridX, gridY };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Calculate clipping rectangle for a freeze region
|
|
179
|
+
*
|
|
180
|
+
* @returns Clip rect in canvas coordinates { x, y, width, height }
|
|
181
|
+
*/
|
|
182
|
+
export function getClipRectForRegion(
|
|
183
|
+
region: FreezeRegion,
|
|
184
|
+
canvasWidth: number,
|
|
185
|
+
canvasHeight: number,
|
|
186
|
+
frozenWidth: number,
|
|
187
|
+
frozenHeight: number,
|
|
188
|
+
headerWidth: number,
|
|
189
|
+
headerHeight: number
|
|
190
|
+
): { x: number; y: number; width: number; height: number } {
|
|
191
|
+
switch (region) {
|
|
192
|
+
case 'top-left':
|
|
193
|
+
return {
|
|
194
|
+
x: headerWidth,
|
|
195
|
+
y: headerHeight,
|
|
196
|
+
width: frozenWidth,
|
|
197
|
+
height: frozenHeight,
|
|
198
|
+
};
|
|
199
|
+
case 'top':
|
|
200
|
+
return {
|
|
201
|
+
x: headerWidth + frozenWidth,
|
|
202
|
+
y: headerHeight,
|
|
203
|
+
width: canvasWidth - headerWidth - frozenWidth,
|
|
204
|
+
height: frozenHeight,
|
|
205
|
+
};
|
|
206
|
+
case 'left':
|
|
207
|
+
return {
|
|
208
|
+
x: headerWidth,
|
|
209
|
+
y: headerHeight + frozenHeight,
|
|
210
|
+
width: frozenWidth,
|
|
211
|
+
height: canvasHeight - headerHeight - frozenHeight,
|
|
212
|
+
};
|
|
213
|
+
case 'main':
|
|
214
|
+
return {
|
|
215
|
+
x: headerWidth + frozenWidth,
|
|
216
|
+
y: headerHeight + frozenHeight,
|
|
217
|
+
width: canvasWidth - headerWidth - frozenWidth,
|
|
218
|
+
height: canvasHeight - headerHeight - frozenHeight,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if a row is in the frozen area
|
|
225
|
+
*/
|
|
226
|
+
export function isRowFrozen(row: number, frozenRows: number): boolean {
|
|
227
|
+
return row < frozenRows;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Check if a column is in the frozen area
|
|
232
|
+
*/
|
|
233
|
+
export function isColFrozen(col: number, frozenCols: number): boolean {
|
|
234
|
+
return col < frozenCols;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Check if a cell is fully in the frozen area (both row and col frozen)
|
|
239
|
+
*/
|
|
240
|
+
export function isCellFullyFrozen(
|
|
241
|
+
row: number,
|
|
242
|
+
col: number,
|
|
243
|
+
frozenRows: number,
|
|
244
|
+
frozenCols: number
|
|
245
|
+
): boolean {
|
|
246
|
+
return isRowFrozen(row, frozenRows) && isColFrozen(col, frozenCols);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get the region a cell belongs to based on its row/col indices
|
|
251
|
+
*/
|
|
252
|
+
export function getCellRegion(
|
|
253
|
+
row: number,
|
|
254
|
+
col: number,
|
|
255
|
+
frozenRows: number,
|
|
256
|
+
frozenCols: number
|
|
257
|
+
): FreezeRegion {
|
|
258
|
+
const rowFrozen = isRowFrozen(row, frozenRows);
|
|
259
|
+
const colFrozen = isColFrozen(col, frozenCols);
|
|
260
|
+
|
|
261
|
+
if (rowFrozen && colFrozen) {
|
|
262
|
+
return 'top-left';
|
|
263
|
+
} else if (rowFrozen) {
|
|
264
|
+
return 'top';
|
|
265
|
+
} else if (colFrozen) {
|
|
266
|
+
return 'left';
|
|
267
|
+
} else {
|
|
268
|
+
return 'main';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|