@svadmin/ui 0.72.0 → 0.74.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 +62 -22
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/dist/app.css +8488 -6297
- package/dist/app.theme.css +8514 -6374
- package/dist/class-properties.generated.d.ts +1 -0
- package/dist/class-properties.generated.js +2 -0
- package/dist/classnames.d.ts +8 -0
- package/dist/classnames.js +26 -0
- package/dist/component-registry.d.ts +14977 -0
- package/dist/component-registry.js +216 -0
- package/dist/components/AboutSettings.svelte +7 -1
- package/dist/components/AboutSettings.svelte.d.ts +4 -1
- package/dist/components/ActivityFeed.svelte +9 -3
- package/dist/components/ActivityFeed.svelte.d.ts +3 -0
- package/dist/components/AdminApp.svelte +19 -10
- package/dist/components/AdminApp.svelte.d.ts +6 -0
- package/dist/components/ApiSettings.svelte +41 -5
- package/dist/components/ApprovalCenter.svelte +283 -0
- package/dist/components/ApprovalCenter.svelte.d.ts +10 -0
- package/dist/components/AuditLogViewer.svelte +3 -1
- package/dist/components/AutoForm.svelte +53 -9
- package/dist/components/AutoForm.svelte.d.ts +3 -0
- package/dist/components/AutoTable.svelte +846 -43
- package/dist/components/AutoTable.svelte.d.ts +22 -1
- package/dist/components/BoundRecordDetailDrawer.svelte +17 -2
- package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +4 -0
- package/dist/components/Breadcrumbs.svelte +11 -1
- package/dist/components/CancelTaskButton.svelte +12 -42
- package/dist/components/ColorPicker.svelte +73 -0
- package/dist/components/ColorPicker.svelte.d.ts +15 -0
- package/dist/components/ComboboxField.svelte +143 -34
- package/dist/components/ComboboxField.svelte.d.ts +4 -2
- package/dist/components/CreatePage.svelte +4 -0
- package/dist/components/CreatePage.svelte.d.ts +2 -0
- package/dist/components/DashboardView.svelte +99 -0
- package/dist/components/DashboardView.svelte.d.ts +14 -0
- package/dist/components/DatePicker.svelte +10 -0
- package/dist/components/DatePicker.svelte.d.ts +6 -0
- package/dist/components/DateRangeInput.svelte +155 -0
- package/dist/components/DateRangeInput.svelte.d.ts +35 -0
- package/dist/components/DateRangePicker.svelte +10 -0
- package/dist/components/DateRangePicker.svelte.d.ts +6 -0
- package/dist/components/DateTimeInput.svelte +134 -0
- package/dist/components/DateTimeInput.svelte.d.ts +27 -0
- package/dist/components/DateTimePicker.svelte +10 -0
- package/dist/components/DateTimePicker.svelte.d.ts +6 -0
- package/dist/components/DevTools.svelte +36 -28
- package/dist/components/DevframePanel.svelte +123 -0
- package/dist/components/DevframePanel.svelte.d.ts +3 -0
- package/dist/components/EditPage.svelte +4 -0
- package/dist/components/EditPage.svelte.d.ts +2 -0
- package/dist/components/ErrorSummary.svelte +52 -0
- package/dist/components/ErrorSummary.svelte.d.ts +14 -0
- package/dist/components/FieldRenderer.svelte +172 -99
- package/dist/components/FileUpload.svelte +412 -0
- package/dist/components/FileUpload.svelte.d.ts +43 -0
- package/dist/components/FilterBuilder.svelte +244 -265
- package/dist/components/FilterBuilder.svelte.d.ts +3 -0
- package/dist/components/FilterCollectionInput.svelte +80 -0
- package/dist/components/FilterCollectionInput.svelte.d.ts +10 -0
- package/dist/components/GanttChart.svelte +66 -14
- package/dist/components/GanttChart.svelte.d.ts +6 -0
- package/dist/components/Header.svelte +18 -2
- package/dist/components/ImportWizard.svelte +509 -100
- package/dist/components/ImportWizard.svelte.d.ts +11 -0
- package/dist/components/JsonSchemaForm.svelte +322 -98
- package/dist/components/JsonSchemaForm.svelte.d.ts +18 -1
- package/dist/components/KanbanBoard.svelte +195 -40
- package/dist/components/KanbanBoard.svelte.d.ts +9 -2
- package/dist/components/Layout.svelte +4 -3
- package/dist/components/LazyAutoForm.svelte +28 -0
- package/dist/components/LazyAutoForm.svelte.d.ts +8 -0
- package/dist/components/LazyAutoTable.svelte +28 -0
- package/dist/components/LazyAutoTable.svelte.d.ts +8 -0
- package/dist/components/LazySettingsPage.svelte +28 -0
- package/dist/components/LazySettingsPage.svelte.d.ts +8 -0
- package/dist/components/LazyShowPage.svelte +28 -0
- package/dist/components/LazyShowPage.svelte.d.ts +8 -0
- package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +1 -1
- package/dist/components/ListPage.svelte +9 -1
- package/dist/components/ListPage.svelte.d.ts +5 -0
- package/dist/components/LoginPage.svelte +2 -2
- package/dist/components/MoneyInput.svelte +25 -0
- package/dist/components/MoneyInput.svelte.d.ts +20 -0
- package/dist/components/MultiTabKeepAlive.svelte +11 -6
- package/dist/components/NotificationsSettings.svelte +69 -2
- package/dist/components/NotificationsSettings.svelte.d.ts +25 -1
- package/dist/components/NumberInput.svelte +25 -0
- package/dist/components/NumberInput.svelte.d.ts +19 -0
- package/dist/components/NumericInput.svelte +110 -0
- package/dist/components/NumericInput.svelte.d.ts +23 -0
- package/dist/components/PercentInput.svelte +25 -0
- package/dist/components/PercentInput.svelte.d.ts +20 -0
- package/dist/components/PermissionMatrix.svelte +20 -1
- package/dist/components/PivotExportButton.svelte +157 -0
- package/dist/components/PivotExportButton.svelte.d.ts +16 -0
- package/dist/components/PivotTable.svelte +229 -74
- package/dist/components/PivotTable.svelte.d.ts +19 -2
- package/dist/components/QuickEditDrawer.svelte +4 -1
- package/dist/components/QuickEditDrawer.svelte.d.ts +2 -0
- package/dist/components/RadioGroup.svelte +121 -0
- package/dist/components/RadioGroup.svelte.d.ts +43 -0
- package/dist/components/RangeSlider.svelte +127 -0
- package/dist/components/RangeSlider.svelte.d.ts +22 -0
- package/dist/components/Rate.svelte +121 -0
- package/dist/components/Rate.svelte.d.ts +16 -0
- package/dist/components/RecordDetailDrawer.svelte +6 -2
- package/dist/components/RecordDetailDrawer.svelte.d.ts +4 -0
- package/dist/components/RelationPicker.svelte +341 -0
- package/dist/components/RelationPicker.svelte.d.ts +26 -0
- package/dist/components/ResourceOperationsPage.svelte +25 -19
- package/dist/components/ResourceOperationsPage.svelte.d.ts +5 -1
- package/dist/components/RetryTaskButton.svelte +12 -42
- package/dist/components/SecuritySettings.svelte +29 -5
- package/dist/components/SegmentedControl.svelte +145 -0
- package/dist/components/SegmentedControl.svelte.d.ts +38 -0
- package/dist/components/SettingsPage.svelte +3 -1
- package/dist/components/SettingsPage.svelte.d.ts +1 -0
- package/dist/components/ShowInferencer.svelte +4 -2
- package/dist/components/ShowInferencer.svelte.d.ts +2 -0
- package/dist/components/ShowPage.svelte +25 -6
- package/dist/components/ShowPage.svelte.d.ts +5 -0
- package/dist/components/Sidebar.svelte +5 -5
- package/dist/components/Slider.svelte +100 -0
- package/dist/components/Slider.svelte.d.ts +19 -0
- package/dist/components/SpreadsheetView.svelte +1037 -124
- package/dist/components/SpreadsheetView.svelte.d.ts +19 -0
- package/dist/components/StepsForm.svelte +5 -5
- package/dist/components/SvarAutoTable.svelte +39 -0
- package/dist/components/SvarAutoTable.svelte.d.ts +13 -0
- package/dist/components/SvarDataGrid.svelte +362 -0
- package/dist/components/SvarDataGrid.svelte.d.ts +57 -0
- package/dist/components/SvarGridCell.svelte +10 -0
- package/dist/components/SvarGridCell.svelte.d.ts +7 -0
- package/dist/components/SvarResourceRead.svelte +50 -0
- package/dist/components/SvarResourceRead.svelte.d.ts +23 -0
- package/dist/components/SvarResourceTable.svelte +63 -0
- package/dist/components/SvarResourceTable.svelte.d.ts +42 -0
- package/dist/components/SvarResourceTableView.svelte +364 -0
- package/dist/components/SvarResourceTableView.svelte.d.ts +4 -0
- package/dist/components/TagsInput.svelte +178 -0
- package/dist/components/TagsInput.svelte.d.ts +17 -0
- package/dist/components/TaskQueueDrawer.svelte +72 -16
- package/dist/components/TimePicker.svelte +10 -0
- package/dist/components/TimePicker.svelte.d.ts +6 -0
- package/dist/components/TreeSelect.svelte +440 -46
- package/dist/components/TreeSelect.svelte.d.ts +6 -1
- package/dist/components/TreeTable.svelte +484 -82
- package/dist/components/TreeTable.svelte.d.ts +24 -1
- package/dist/components/TwoFactorAuthPage.svelte +85 -48
- package/dist/components/TwoFactorAuthPage.svelte.d.ts +12 -1
- package/dist/components/VirtualTable.svelte +116 -20
- package/dist/components/VirtualTable.svelte.d.ts +5 -0
- package/dist/components/account/CompanyProfilePage.svelte +74 -38
- package/dist/components/account/GetStartedPage.svelte +2 -1
- package/dist/components/account/ImportMembersPage.svelte +22 -167
- package/dist/components/account/ImportMembersPage.svelte.d.ts +4 -1
- package/dist/components/account/MemberDirectory.svelte +156 -0
- package/dist/components/account/MemberDirectory.svelte.d.ts +19 -0
- package/dist/components/account/MembersStarterPage.svelte +9 -55
- package/dist/components/account/MembersStarterPage.svelte.d.ts +5 -1
- package/dist/components/account/SecurityLogPage.svelte +4 -1
- package/dist/components/account/SettingsPlainPage.svelte +7 -38
- package/dist/components/account/SettingsPlainPage.svelte.d.ts +4 -1
- package/dist/components/account/SettingsSidebarPage.svelte +7 -20
- package/dist/components/account/TeamMembersPage.svelte +4 -20
- package/dist/components/account/TeamMembersPage.svelte.d.ts +5 -1
- package/dist/components/account/UserProfilePage.svelte +3 -25
- package/dist/components/account/UserProfilePage.svelte.d.ts +17 -2
- package/dist/components/auto-table-grid.d.ts +26 -0
- package/dist/components/auto-table-grid.js +1 -0
- package/dist/components/buttons/ExportButton.svelte +56 -12
- package/dist/components/buttons/ExportButton.svelte.d.ts +11 -0
- package/dist/components/content/ContentPageHeader.svelte +12 -9
- package/dist/components/content/ContentPageShell.svelte +6 -9
- package/dist/components/content/FileBrowser.svelte +35 -0
- package/dist/components/content/FileBrowser.svelte.d.ts +14 -0
- package/dist/components/content/MetricBlock.svelte +10 -13
- package/dist/components/content/NetworkTable.svelte +3 -3
- package/dist/components/content/NetworkTable.svelte.d.ts +1 -0
- package/dist/components/content/PageToolbar.svelte +6 -10
- package/dist/components/content/ReferenceNotice.svelte +5 -0
- package/dist/components/content/ReferenceNotice.svelte.d.ts +18 -0
- package/dist/components/content/SectionHeader.svelte +8 -13
- package/dist/components/content/SettingsFieldRow.svelte +9 -23
- package/dist/components/content/SettingsGroup.svelte +11 -27
- package/dist/components/content/StatusBadge.svelte +3 -8
- package/dist/components/content/WorkspaceLayout.svelte +8 -12
- package/dist/components/enterprise/filter-tree.d.ts +48 -0
- package/dist/components/enterprise/filter-tree.js +224 -0
- package/dist/components/enterprise/json-schema-form.d.ts +39 -0
- package/dist/components/enterprise/json-schema-form.js +195 -0
- package/dist/components/enterprise/recursive-schema-form.d.ts +30 -0
- package/dist/components/enterprise/recursive-schema-form.js +196 -0
- package/dist/components/enterprise/spreadsheet-formula.d.ts +12 -0
- package/dist/components/enterprise/spreadsheet-formula.js +245 -0
- package/dist/components/export-reference.d.ts +2 -0
- package/dist/components/export-reference.js +9 -0
- package/dist/components/fieldComponentMap.js +2 -0
- package/dist/components/fields/DateField.svelte +12 -12
- package/dist/components/fields/date-display.d.ts +6 -0
- package/dist/components/fields/date-display.js +24 -0
- package/dist/components/fields/date-field-value.d.ts +2 -0
- package/dist/components/fields/date-field-value.js +13 -0
- package/dist/components/json-schema-form-state.d.ts +37 -0
- package/dist/components/json-schema-form-state.js +168 -0
- package/dist/components/network/TeamCrewTablePage.svelte +6 -1
- package/dist/components/network/UserCardsNFTPage.svelte +2 -0
- package/dist/components/profile/PublicProfilePage.svelte +2 -0
- package/dist/components/record-detail.svelte.d.ts +3 -0
- package/dist/components/record-detail.svelte.js +10 -4
- package/dist/components/saved-list-views.d.ts +74 -1
- package/dist/components/saved-list-views.js +191 -3
- package/dist/components/svar-grid-cells.d.ts +14 -0
- package/dist/components/svar-grid-cells.js +2 -0
- package/dist/components/svar-grid-contract.d.ts +80 -0
- package/dist/components/svar-grid-contract.js +159 -0
- package/dist/components/svar-grid-interactions.d.ts +54 -0
- package/dist/components/svar-grid-interactions.js +174 -0
- package/dist/components/svar-grid-loading.d.ts +40 -0
- package/dist/components/svar-grid-loading.js +180 -0
- package/dist/components/svar-grid-operations.d.ts +37 -0
- package/dist/components/svar-grid-operations.js +186 -0
- package/dist/components/svar-grid-panes.d.ts +34 -0
- package/dist/components/svar-grid-panes.js +66 -0
- package/dist/components/svar-grid-resource-loading.d.ts +18 -0
- package/dist/components/svar-grid-resource-loading.js +68 -0
- package/dist/components/svar-grid-resource-session.svelte.d.ts +29 -0
- package/dist/components/svar-grid-resource-session.svelte.js +61 -0
- package/dist/components/table-contract.d.ts +12 -1
- package/dist/components/task-action.svelte.d.ts +13 -0
- package/dist/components/task-action.svelte.js +73 -0
- package/dist/components/ui/DesignSystemShowcase.svelte +164 -0
- package/dist/components/ui/DesignSystemShowcase.svelte.d.ts +3 -0
- package/dist/components/ui/badge/badge-variants.d.ts +3 -2
- package/dist/components/ui/badge/badge-variants.js +2 -1
- package/dist/components/ui/badge/badge.svelte +3 -0
- package/dist/components/ui/badge/badge.svelte.d.ts +1 -0
- package/dist/components/ui/button/button-variants.d.ts +5 -4
- package/dist/components/ui/button/button-variants.js +7 -2
- package/dist/components/ui/checkbox/checkbox.svelte +5 -1
- package/dist/components/ui/checkbox/checkbox.svelte.d.ts +5 -1
- package/dist/components/ui/input/input.svelte +8 -6
- package/dist/components/ui/input/input.svelte.d.ts +4 -1
- package/dist/components/ui/select/select.svelte +4 -1
- package/dist/components/ui/select/select.svelte.d.ts +2 -1
- package/dist/components/ui/textarea/textarea.svelte +2 -1
- package/dist/components.css +3110 -0
- package/dist/date-input-policy.d.ts +4 -0
- package/dist/date-input-policy.js +35 -0
- package/dist/design-contract.d.ts +13 -0
- package/dist/design-contract.js +11 -0
- package/dist/design-system-components.d.ts +100 -0
- package/dist/design-system-components.js +101 -0
- package/dist/design-system.d.ts +15 -0
- package/dist/design-system.js +7 -0
- package/dist/devtools-bridge.d.ts +80 -0
- package/dist/devtools-bridge.js +92 -0
- package/dist/index.d.ts +50 -1
- package/dist/index.js +32 -2
- package/dist/page-chrome.css +12 -49
- package/dist/recipes.d.ts +960 -0
- package/dist/recipes.js +264 -0
- package/dist/rendering/index.d.ts +87 -0
- package/dist/rendering/index.js +117 -0
- package/dist/styles/aliases.css +49 -0
- package/dist/styles/compatibility.epilogue.css +384 -0
- package/dist/styles/compatibility.prelude.css +5513 -0
- package/dist/styles/conditional-states.css +28 -0
- package/dist/styles/conditional-utilities.css +22 -0
- package/dist/tree-table-model.d.ts +24 -0
- package/dist/tree-table-model.js +111 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -1
- package/package.json +40 -10
- package/styles-compatibility.json +12 -0
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
2
3
|
import { Button } from './ui/button/index.js';
|
|
3
|
-
import { Plus, Download, FileSpreadsheet } from '@lucide/svelte';
|
|
4
|
+
import { Plus, Download, FileSpreadsheet, Redo2, Undo2, Trash2, Upload, Bold, Italic, Copy } from '@lucide/svelte';
|
|
4
5
|
import { cn } from '../utils.js';
|
|
6
|
+
import { parseSpreadsheetClipboard, serializeSpreadsheetClipboard, parseSpreadsheetWorkbook, snapshotSpreadsheetWorkbook, serializeSpreadsheetWorkbook, toCsv, type SpreadsheetCellFormat, type SpreadsheetWorkbook } from '@svadmin/core';
|
|
7
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
8
|
+
|
|
9
|
+
const i18n = useTranslation();
|
|
5
10
|
|
|
6
11
|
export interface SheetData {
|
|
7
12
|
id: string;
|
|
@@ -9,17 +14,47 @@
|
|
|
9
14
|
rows: number;
|
|
10
15
|
cols: number;
|
|
11
16
|
cells: Record<string, string>; // e.g. "A1": "100", "A2": "=SUM(A1:A1)"
|
|
17
|
+
formats?: Record<string, SpreadsheetCellFormat>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SpreadsheetLimits {
|
|
21
|
+
maxRows: number;
|
|
22
|
+
maxCols: number;
|
|
23
|
+
maxCells: number;
|
|
24
|
+
maxSheets: number;
|
|
25
|
+
maxCellLength: number;
|
|
26
|
+
maxFormulaLength: number;
|
|
27
|
+
maxFormulaDepth: number;
|
|
28
|
+
maxRangeCells: number;
|
|
29
|
+
maxFormulaSteps: number;
|
|
12
30
|
}
|
|
13
31
|
|
|
32
|
+
const defaultLimits: SpreadsheetLimits = {
|
|
33
|
+
maxRows: 1000,
|
|
34
|
+
maxCols: 100,
|
|
35
|
+
maxCells: 10_000,
|
|
36
|
+
maxSheets: 32,
|
|
37
|
+
maxCellLength: 10_000,
|
|
38
|
+
maxFormulaLength: 1000,
|
|
39
|
+
maxFormulaDepth: 32,
|
|
40
|
+
maxRangeCells: 10_000,
|
|
41
|
+
maxFormulaSteps: 50_000,
|
|
42
|
+
};
|
|
43
|
+
|
|
14
44
|
interface Props {
|
|
15
45
|
sheets?: SheetData[];
|
|
16
46
|
activeSheetId?: string;
|
|
47
|
+
scopeKey?: string;
|
|
17
48
|
readonly?: boolean;
|
|
18
49
|
onchange?: (sheets: SheetData[]) => void;
|
|
19
50
|
onexport?: (csv: string) => void;
|
|
51
|
+
onworkbookexport?: (json: string) => void;
|
|
52
|
+
onxlsxexport?: (bytes: Uint8Array) => void | Promise<void>;
|
|
53
|
+
xlsxexporter?: (workbook: SpreadsheetWorkbook) => Promise<Uint8Array | undefined>;
|
|
54
|
+
xlsximporter?: (bytes: Uint8Array, limits: SpreadsheetLimits, signal: AbortSignal) => Promise<unknown>;
|
|
55
|
+
limits?: Partial<SpreadsheetLimits>;
|
|
20
56
|
class?: string;
|
|
21
57
|
}
|
|
22
|
-
|
|
23
58
|
let {
|
|
24
59
|
sheets = $bindable([
|
|
25
60
|
{
|
|
@@ -50,26 +85,182 @@
|
|
|
50
85
|
},
|
|
51
86
|
]),
|
|
52
87
|
activeSheetId = $bindable('sheet1'),
|
|
88
|
+
scopeKey,
|
|
53
89
|
readonly = false,
|
|
54
90
|
onchange,
|
|
55
91
|
onexport,
|
|
92
|
+
onworkbookexport,
|
|
93
|
+
onxlsxexport,
|
|
94
|
+
xlsxexporter,
|
|
95
|
+
xlsximporter,
|
|
96
|
+
limits: limitOverrides = {},
|
|
56
97
|
class: className = '',
|
|
57
98
|
}: Props = $props();
|
|
58
99
|
|
|
59
100
|
let selectedCell = $state<string>('A1');
|
|
101
|
+
let fileInput: HTMLInputElement;
|
|
102
|
+
let workbookImportError = $state(false);
|
|
103
|
+
let workbookXlsxError = $state(false);
|
|
104
|
+
let workbookXlsxExporting = $state(false);
|
|
105
|
+
let importRequest: object | undefined;
|
|
106
|
+
let importController: AbortController | undefined;
|
|
107
|
+
let importMounted = true;
|
|
108
|
+
let pendingWorkbook = $state.raw<{ workbook: SpreadsheetWorkbook; context: object }>();
|
|
109
|
+
onDestroy(() => { importMounted = false; importRequest = undefined; importController?.abort(); });
|
|
110
|
+
let selectedRangeEnd = $state<string>('A1');
|
|
111
|
+
let selectionError = $state(false);
|
|
112
|
+
let selectionSheetId: string | undefined;
|
|
60
113
|
let formulaInput = $state('');
|
|
114
|
+
const HISTORY_LIMIT = 100;
|
|
115
|
+
const HISTORY_TEXT_LIMIT = 2_000_000;
|
|
116
|
+
interface HistoryEntry { data: string; activeSheetId: string }
|
|
117
|
+
let undoHistory = $state.raw<HistoryEntry[]>([]);
|
|
118
|
+
let redoHistory = $state.raw<HistoryEntry[]>([]);
|
|
119
|
+
let committedFingerprint = '';
|
|
120
|
+
let committedScopeKey: string | undefined;
|
|
121
|
+
let committedSheets: SheetData[] | undefined;
|
|
122
|
+
let committedLimits = '';
|
|
123
|
+
let xlsxExportRequest: object | undefined;
|
|
61
124
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
125
|
+
function snapshotSheets(value: SheetData[]): SheetData[] {
|
|
126
|
+
return value.map((sheet) => ({
|
|
127
|
+
...sheet,
|
|
128
|
+
cells: { ...sheet.cells },
|
|
129
|
+
...(sheet.formats ? {
|
|
130
|
+
formats: Object.fromEntries(Object.entries(sheet.formats).map(([key, format]) => [key, { ...format }])),
|
|
131
|
+
} : {}),
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
65
134
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
135
|
+
function fingerprintSheets(value: SheetData[]): string {
|
|
136
|
+
return JSON.stringify(value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function pushBounded(history: HistoryEntry[]): HistoryEntry[] {
|
|
140
|
+
const data = fingerprintSheets(sheets);
|
|
141
|
+
if (data.length > HISTORY_TEXT_LIMIT) return [];
|
|
142
|
+
const next = [...history, { data, activeSheetId }].slice(-HISTORY_LIMIT);
|
|
143
|
+
let size = next.reduce((total, entry) => total + entry.data.length, 0);
|
|
144
|
+
while (size > HISTORY_TEXT_LIMIT) {
|
|
145
|
+
const oldest = next.shift();
|
|
146
|
+
if (!oldest) break;
|
|
147
|
+
size -= oldest.data.length;
|
|
148
|
+
}
|
|
149
|
+
return next;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function notifyChange(value: SheetData[]) {
|
|
153
|
+
onchange?.(snapshotSheets(value));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function commitSheets(next: SheetData[], nextActiveSheetId = activeSheetId) {
|
|
157
|
+
synchronizeHistory();
|
|
158
|
+
const nextSnapshot = snapshotSheets(next);
|
|
159
|
+
if (fingerprintSheets(nextSnapshot) === committedFingerprint && nextActiveSheetId === activeSheetId) return;
|
|
160
|
+
undoHistory = pushBounded(undoHistory);
|
|
161
|
+
redoHistory = [];
|
|
162
|
+
sheets = nextSnapshot;
|
|
163
|
+
activeSheetId = nextActiveSheetId;
|
|
164
|
+
committedSheets = sheets;
|
|
165
|
+
committedFingerprint = fingerprintSheets(nextSnapshot);
|
|
166
|
+
committedScopeKey = scopeKey;
|
|
167
|
+
notifyChange(nextSnapshot);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function synchronizeHistory() {
|
|
171
|
+
const fingerprint = fingerprintSheets(sheets);
|
|
172
|
+
const limitKey = JSON.stringify(limits);
|
|
173
|
+
const scopeChanged = committedScopeKey !== scopeKey;
|
|
174
|
+
const externalDataChanged = committedSheets !== sheets || committedFingerprint !== fingerprint;
|
|
175
|
+
if (scopeChanged || externalDataChanged || committedLimits !== limitKey) {
|
|
176
|
+
undoHistory = [];
|
|
177
|
+
redoHistory = [];
|
|
178
|
+
committedSheets = sheets;
|
|
179
|
+
committedLimits = limitKey;
|
|
180
|
+
committedFingerprint = fingerprint;
|
|
181
|
+
committedScopeKey = scopeKey;
|
|
182
|
+
if (scopeChanged || externalDataChanged) {
|
|
183
|
+
selectedCell = 'A1';
|
|
184
|
+
selectedRangeEnd = 'A1';
|
|
185
|
+
selectionError = false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const limits = $derived.by(() => {
|
|
191
|
+
const positive = (value: number | undefined, fallback: number) =>
|
|
192
|
+
typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? Math.min(value, fallback) : fallback;
|
|
193
|
+
return {
|
|
194
|
+
maxRows: positive(limitOverrides.maxRows, defaultLimits.maxRows),
|
|
195
|
+
maxCols: positive(limitOverrides.maxCols, defaultLimits.maxCols),
|
|
196
|
+
maxCells: positive(limitOverrides.maxCells, defaultLimits.maxCells),
|
|
197
|
+
maxSheets: positive(limitOverrides.maxSheets, defaultLimits.maxSheets),
|
|
198
|
+
maxCellLength: positive(limitOverrides.maxCellLength, defaultLimits.maxCellLength),
|
|
199
|
+
maxFormulaLength: positive(limitOverrides.maxFormulaLength, defaultLimits.maxFormulaLength),
|
|
200
|
+
maxFormulaDepth: positive(limitOverrides.maxFormulaDepth, defaultLimits.maxFormulaDepth),
|
|
201
|
+
maxRangeCells: positive(limitOverrides.maxRangeCells, defaultLimits.maxRangeCells),
|
|
202
|
+
maxFormulaSteps: positive(limitOverrides.maxFormulaSteps, defaultLimits.maxFormulaSteps),
|
|
203
|
+
} satisfies SpreadsheetLimits;
|
|
204
|
+
});
|
|
205
|
+
$effect.pre(synchronizeHistory);
|
|
206
|
+
const validWorkbook = $derived(sheets.length <= limits.maxSheets && new Set(sheets.map(sheet => sheet.id)).size === sheets.length);
|
|
207
|
+
const currentSheet = $derived(validWorkbook ? (sheets.find(s => s.id === activeSheetId) ?? sheets[0]) : undefined);
|
|
208
|
+
const validGrid = $derived(Boolean(currentSheet
|
|
209
|
+
&& Number.isSafeInteger(currentSheet.rows) && currentSheet.rows > 0 && currentSheet.rows <= limits.maxRows
|
|
210
|
+
&& Number.isSafeInteger(currentSheet.cols) && currentSheet.cols > 0 && currentSheet.cols <= limits.maxCols
|
|
211
|
+
&& currentSheet.rows * currentSheet.cols <= limits.maxCells));
|
|
212
|
+
const gridRows = $derived(validGrid && currentSheet ? currentSheet.rows : 0);
|
|
213
|
+
const gridCols = $derived(validGrid && currentSheet ? currentSheet.cols : 0);
|
|
214
|
+
const cellSnapshot = $derived.by(() => {
|
|
215
|
+
if (!validGrid || !currentSheet || !currentSheet.cells || typeof currentSheet.cells !== 'object') return null;
|
|
216
|
+
const cells: Record<string, string> = {};
|
|
217
|
+
let textLength = 0;
|
|
218
|
+
for (let row = 1; row <= gridRows; row++) {
|
|
219
|
+
for (let col = 0; col < gridCols; col++) {
|
|
220
|
+
const key = `${getColName(col)}${row}`;
|
|
221
|
+
const raw = Object.hasOwn(currentSheet.cells, key) ? currentSheet.cells[key] : '';
|
|
222
|
+
if (typeof raw !== 'string' || raw.length > limits.maxCellLength) return null;
|
|
223
|
+
textLength += raw.length;
|
|
224
|
+
if (textLength > 1_000_000) return null;
|
|
225
|
+
cells[key] = raw;
|
|
226
|
+
}
|
|
72
227
|
}
|
|
228
|
+
return cells;
|
|
229
|
+
});
|
|
230
|
+
const canRender = $derived(validGrid && cellSnapshot !== null);
|
|
231
|
+
const importContext = $derived({
|
|
232
|
+
scopeKey, readonly, data: fingerprintSheets(sheets), limits: JSON.stringify(limits), activeSheetId, xlsximporter,
|
|
233
|
+
});
|
|
234
|
+
const importReady = $derived(pendingWorkbook?.context === importContext && !readonly);
|
|
235
|
+
const xlsxContext = $derived({ context: importContext, xlsxexporter, onxlsxexport });
|
|
236
|
+
$effect.pre(() => {
|
|
237
|
+
void xlsxContext;
|
|
238
|
+
xlsxExportRequest = undefined;
|
|
239
|
+
workbookXlsxExporting = false;
|
|
240
|
+
workbookXlsxError = false;
|
|
241
|
+
});
|
|
242
|
+
$effect.pre(() => {
|
|
243
|
+
void importContext;
|
|
244
|
+
pendingWorkbook = undefined;
|
|
245
|
+
workbookImportError = false;
|
|
246
|
+
importRequest = undefined;
|
|
247
|
+
importController?.abort();
|
|
248
|
+
});
|
|
249
|
+
const workbookJson = $derived.by(() => {
|
|
250
|
+
if (!validWorkbook || !currentSheet || sheets.some(sheet =>
|
|
251
|
+
sheet.rows > limits.maxRows || sheet.cols > limits.maxCols
|
|
252
|
+
|| sheet.rows * sheet.cols > limits.maxCells
|
|
253
|
+
|| Object.values(sheet.cells).some(value => typeof value !== 'string' || value.length > limits.maxCellLength))) return;
|
|
254
|
+
return serializeSpreadsheetWorkbook({
|
|
255
|
+
protocolVersion: 1, sheets: snapshotSheets(sheets), activeSheetId: currentSheet.id,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
const canAddRow = $derived(canRender && gridRows < limits.maxRows && (gridRows + 1) * gridCols <= limits.maxCells);
|
|
259
|
+
const canAddCol = $derived(canRender && gridCols < limits.maxCols && gridRows * (gridCols + 1) <= limits.maxCells);
|
|
260
|
+
|
|
261
|
+
function getColName(index: number): string {
|
|
262
|
+
let result = '', current = index;
|
|
263
|
+
while (current >= 0) { result = String.fromCharCode(current % 26 + 65) + result; current = Math.floor(current / 26) - 1; }
|
|
73
264
|
return result;
|
|
74
265
|
}
|
|
75
266
|
|
|
@@ -77,90 +268,579 @@
|
|
|
77
268
|
let idx = 0;
|
|
78
269
|
for (let i = 0; i < col.length; i++) {
|
|
79
270
|
idx = idx * 26 + (col.charCodeAt(i) - 64);
|
|
271
|
+
if (!Number.isSafeInteger(idx)) throw new Error('limit');
|
|
80
272
|
}
|
|
81
273
|
return idx - 1;
|
|
82
274
|
}
|
|
83
275
|
|
|
84
276
|
function parseCellKey(key: string): { col: string; row: number } | null {
|
|
85
|
-
const match = key.match(/^([A-Z]+)(\d
|
|
277
|
+
const match = key.match(/^([A-Z]+)([1-9]\d*)$/);
|
|
86
278
|
if (!match || !match[1] || !match[2]) return null;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
279
|
+
const row = Number(match[2]);
|
|
280
|
+
if (!Number.isSafeInteger(row)) throw new Error('limit');
|
|
281
|
+
return { col: match[1], row };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
interface EvaluationState {
|
|
285
|
+
depth: number;
|
|
286
|
+
steps: number;
|
|
287
|
+
cache: Map<string, number | string>;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface SpreadsheetReference {
|
|
291
|
+
sheet?: string;
|
|
292
|
+
cell: string;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function evaluateFormula(
|
|
296
|
+
formula: string,
|
|
297
|
+
cells: Record<string, string>,
|
|
298
|
+
visiting: Set<string>,
|
|
299
|
+
state: EvaluationState,
|
|
300
|
+
resolveSheet: (name: string | undefined) => Record<string, string> | undefined,
|
|
301
|
+
): number | string {
|
|
302
|
+
if (!formula.startsWith('=')) return formula;
|
|
303
|
+
if (formula.length > limits.maxFormulaLength) return '#LIMIT!';
|
|
304
|
+
state.depth += 1;
|
|
305
|
+
if (state.depth > limits.maxFormulaDepth) {
|
|
306
|
+
state.depth -= 1;
|
|
307
|
+
return '#LIMIT!';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 空白是分隔符,不能把 “1 2” 拼成 “12”。
|
|
311
|
+
const source = formula.slice(1);
|
|
312
|
+
let cursor = 0;
|
|
313
|
+
const consumeStep = (cost = 1) => {
|
|
314
|
+
state.steps += cost;
|
|
315
|
+
if (state.steps > limits.maxFormulaSteps) throw new Error('limit');
|
|
316
|
+
};
|
|
317
|
+
const skipSpace = () => {
|
|
318
|
+
while (cursor < source.length && /\s/.test(source[cursor] ?? '')) cursor++;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
function readNumber(): number {
|
|
322
|
+
const start = cursor;
|
|
323
|
+
while (cursor < source.length && /[\d.]/.test(source[cursor] ?? '')) cursor += 1;
|
|
324
|
+
const result = Number(source.slice(start, cursor));
|
|
325
|
+
if (!Number.isFinite(result) || !/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(source.slice(start, cursor))) throw new Error('number');
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function readCellReference(): SpreadsheetReference {
|
|
330
|
+
const start = cursor;
|
|
331
|
+
if (source[cursor] === "'") {
|
|
332
|
+
cursor += 1;
|
|
333
|
+
const sheetStart = cursor;
|
|
334
|
+
while (cursor < source.length && source[cursor] !== "'") cursor += 1;
|
|
335
|
+
if (source[cursor] !== "'" || source[cursor + 1] !== '!') throw new Error('reference');
|
|
336
|
+
const sheet = source.slice(sheetStart, cursor);
|
|
337
|
+
cursor += 2;
|
|
338
|
+
const cellStart = cursor;
|
|
339
|
+
if (source[cursor] === '$') cursor += 1;
|
|
340
|
+
while (cursor < source.length && /[A-Za-z]/.test(source[cursor] ?? '')) cursor += 1;
|
|
341
|
+
if (source[cursor] === '$') cursor += 1;
|
|
342
|
+
while (cursor < source.length && /\d/.test(source[cursor] ?? '')) cursor += 1;
|
|
343
|
+
return { sheet, cell: source.slice(cellStart, cursor).toUpperCase() };
|
|
344
|
+
}
|
|
345
|
+
const sheetMatch = source.slice(cursor).match(/^([A-Za-z_][A-Za-z0-9_]*)!/);
|
|
346
|
+
if (sheetMatch) {
|
|
347
|
+
const sheet = sheetMatch[1];
|
|
348
|
+
if (!sheet) throw new Error('reference');
|
|
349
|
+
cursor += sheetMatch[0].length;
|
|
350
|
+
const cellStart = cursor;
|
|
351
|
+
if (source[cursor] === '$') cursor += 1;
|
|
352
|
+
while (cursor < source.length && /[A-Za-z]/.test(source[cursor] ?? '')) cursor += 1;
|
|
353
|
+
if (source[cursor] === '$') cursor += 1;
|
|
354
|
+
while (cursor < source.length && /\d/.test(source[cursor] ?? '')) cursor += 1;
|
|
355
|
+
return { sheet, cell: source.slice(cellStart, cursor).toUpperCase() };
|
|
356
|
+
}
|
|
357
|
+
if (source[cursor] === '$') cursor += 1;
|
|
358
|
+
while (cursor < source.length && /[A-Za-z]/.test(source[cursor] ?? '')) cursor += 1;
|
|
359
|
+
if (source[cursor] === '$') cursor += 1;
|
|
360
|
+
while (cursor < source.length && /\d/.test(source[cursor] ?? '')) cursor += 1;
|
|
361
|
+
return { cell: source.slice(start, cursor).toUpperCase() };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function normalizeCellReference(value: string): string {
|
|
365
|
+
const match = value.match(/^\$?([A-Z]+)\$?([1-9]\d*)$/);
|
|
366
|
+
if (!match || !match[1] || !match[2]) throw new Error('reference');
|
|
367
|
+
return `${match[1]}${match[2]}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function cellValue(reference: SpreadsheetReference): number {
|
|
371
|
+
consumeStep();
|
|
372
|
+
const targetCells = resolveSheet(reference.sheet);
|
|
373
|
+
const key = normalizeCellReference(reference.cell);
|
|
374
|
+
const targetSheetKey = reference.sheet === undefined ? '__current__' : reference.sheet.toLowerCase();
|
|
375
|
+
const visitKey = `${targetSheetKey}!${key}`;
|
|
376
|
+
const targetCell = parseCellKey(key);
|
|
377
|
+
const targetSheet = reference.sheet === undefined
|
|
378
|
+
? currentSheet
|
|
379
|
+
: sheets.find(sheet => sheet.id.toLowerCase() === reference.sheet?.toLowerCase()
|
|
380
|
+
|| sheet.name.toLowerCase() === reference.sheet?.toLowerCase());
|
|
381
|
+
if (!targetCells || !targetCell || !targetSheet
|
|
382
|
+
|| targetCell.row > targetSheet.rows || colNameToIndex(targetCell.col) >= targetSheet.cols) throw new Error('reference');
|
|
383
|
+
if (visiting.has(visitKey)) throw new Error('cycle');
|
|
384
|
+
const raw = targetCells[key] ?? '';
|
|
385
|
+
if (raw.startsWith('=')) {
|
|
386
|
+
const next = new Set(visiting);
|
|
387
|
+
next.add(visitKey);
|
|
388
|
+
const cacheKey = `${targetSheetKey}!${key}`;
|
|
389
|
+
const computed = state.cache.get(cacheKey) ?? evaluateFormula(raw, targetCells, next, state, resolveSheet);
|
|
390
|
+
state.cache.set(cacheKey, computed);
|
|
391
|
+
if (typeof computed !== 'number') {
|
|
392
|
+
throw new Error(computed === '#CYCLE!' ? 'cycle' : computed === '#LIMIT!' ? 'limit' : 'value');
|
|
117
393
|
}
|
|
394
|
+
return computed;
|
|
118
395
|
}
|
|
396
|
+
consumeStep(raw.length);
|
|
397
|
+
const result = Number(raw);
|
|
398
|
+
return Number.isFinite(result) ? result : 0;
|
|
399
|
+
}
|
|
119
400
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
401
|
+
function readRange(): number[] {
|
|
402
|
+
skipSpace();
|
|
403
|
+
const start = readCellReference();
|
|
404
|
+
skipSpace();
|
|
405
|
+
if (source[cursor] !== ':') throw new Error('range');
|
|
406
|
+
cursor += 1;
|
|
407
|
+
skipSpace();
|
|
408
|
+
const end = readCellReference();
|
|
409
|
+
if (start.sheet !== end.sheet) throw new Error('range');
|
|
410
|
+
const startCell = parseCellKey(normalizeCellReference(start.cell));
|
|
411
|
+
const endCell = parseCellKey(normalizeCellReference(end.cell));
|
|
412
|
+
if (!startCell || !endCell) throw new Error('range');
|
|
413
|
+
const firstCol = Math.min(colNameToIndex(startCell.col), colNameToIndex(endCell.col));
|
|
414
|
+
const lastCol = Math.max(colNameToIndex(startCell.col), colNameToIndex(endCell.col));
|
|
415
|
+
const firstRow = Math.min(startCell.row, endCell.row);
|
|
416
|
+
const lastRow = Math.max(startCell.row, endCell.row);
|
|
417
|
+
const area = (lastCol - firstCol + 1) * (lastRow - firstRow + 1);
|
|
418
|
+
if (!Number.isSafeInteger(area) || area > limits.maxRangeCells) throw new Error('limit');
|
|
419
|
+
const targetSheet = start.sheet === undefined
|
|
420
|
+
? currentSheet
|
|
421
|
+
: sheets.find(sheet => sheet.id.toLowerCase() === start.sheet?.toLowerCase()
|
|
422
|
+
|| sheet.name.toLowerCase() === start.sheet?.toLowerCase());
|
|
423
|
+
if (!targetSheet || lastRow > targetSheet.rows || lastCol >= targetSheet.cols) throw new Error('reference');
|
|
424
|
+
const values: number[] = [];
|
|
425
|
+
for (let col = firstCol; col <= lastCol; col += 1) {
|
|
426
|
+
for (let row = firstRow; row <= lastRow; row += 1) {
|
|
427
|
+
consumeStep();
|
|
428
|
+
if (values.length >= limits.maxRangeCells) throw new Error('limit');
|
|
429
|
+
values.push(cellValue({
|
|
430
|
+
...(start.sheet === undefined ? {} : { sheet: start.sheet }),
|
|
431
|
+
cell: `${getColName(col)}${row}`,
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return values;
|
|
123
436
|
}
|
|
124
437
|
|
|
125
|
-
|
|
126
|
-
|
|
438
|
+
function readPrimary(): number {
|
|
439
|
+
consumeStep();
|
|
440
|
+
skipSpace();
|
|
441
|
+
state.depth += 1;
|
|
127
442
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const sanitized = expr.replace(/[^0-9+\-*/(). ]/g, '');
|
|
133
|
-
const fn = new Function(`return (${sanitized || 0})`);
|
|
134
|
-
return Number(fn());
|
|
135
|
-
} catch {
|
|
136
|
-
return '#VALUE!';
|
|
443
|
+
if (state.depth > limits.maxFormulaDepth) throw new Error('limit');
|
|
444
|
+
return readAtom();
|
|
445
|
+
} finally {
|
|
446
|
+
state.depth -= 1;
|
|
137
447
|
}
|
|
138
448
|
}
|
|
139
449
|
|
|
140
|
-
|
|
450
|
+
function readAtom(): number {
|
|
451
|
+
if (source[cursor] === '(') {
|
|
452
|
+
cursor += 1;
|
|
453
|
+
const result = readExpression();
|
|
454
|
+
skipSpace();
|
|
455
|
+
if (source[cursor] !== ')') throw new Error('parenthesis');
|
|
456
|
+
cursor += 1;
|
|
457
|
+
return result;
|
|
458
|
+
}
|
|
459
|
+
if (source[cursor] === '-' || source[cursor] === '+') {
|
|
460
|
+
const sign = source[cursor++] === '-' ? -1 : 1;
|
|
461
|
+
return sign * readPrimary();
|
|
462
|
+
}
|
|
463
|
+
if (/[0-9.]/.test(source[cursor] ?? '')) return readNumber();
|
|
464
|
+
|
|
465
|
+
const identifier = readCellReference();
|
|
466
|
+
skipSpace();
|
|
467
|
+
if (identifier.cell === 'IF' && identifier.sheet === undefined && source[cursor] === '(') {
|
|
468
|
+
cursor += 1;
|
|
469
|
+
const condition = readExpression();
|
|
470
|
+
skipSpace();
|
|
471
|
+
if (source[cursor] !== ',') throw new Error('function');
|
|
472
|
+
cursor += 1;
|
|
473
|
+
const whenTrue = readExpression();
|
|
474
|
+
skipSpace();
|
|
475
|
+
if (source[cursor] !== ',') throw new Error('function');
|
|
476
|
+
cursor += 1;
|
|
477
|
+
const whenFalse = readExpression();
|
|
478
|
+
skipSpace();
|
|
479
|
+
if (source[cursor] !== ')') throw new Error('function');
|
|
480
|
+
cursor += 1;
|
|
481
|
+
return condition !== 0 ? whenTrue : whenFalse;
|
|
482
|
+
}
|
|
483
|
+
if (['SUM', 'AVG', 'COUNT', 'MIN', 'MAX'].includes(identifier.cell)
|
|
484
|
+
&& source[cursor] === '(' && identifier.sheet === undefined) {
|
|
485
|
+
cursor += 1;
|
|
486
|
+
const values = readRange();
|
|
487
|
+
skipSpace();
|
|
488
|
+
if (source[cursor] !== ')') throw new Error('function');
|
|
489
|
+
cursor += 1;
|
|
490
|
+
if (identifier.cell === 'SUM') return values.reduce((sum, value) => sum + value, 0);
|
|
491
|
+
if (identifier.cell === 'AVG') return values.length > 0 ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
492
|
+
if (identifier.cell === 'COUNT') return values.length;
|
|
493
|
+
if (identifier.cell === 'MIN') return values.length > 0 ? Math.min(...values) : 0;
|
|
494
|
+
return values.length > 0 ? Math.max(...values) : 0;
|
|
495
|
+
}
|
|
496
|
+
return cellValue(identifier);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function readTerm(): number {
|
|
500
|
+
let result = readPrimary();
|
|
501
|
+
skipSpace();
|
|
502
|
+
while (source[cursor] === '*' || source[cursor] === '/') {
|
|
503
|
+
const operator = source[cursor++];
|
|
504
|
+
const next = readPrimary();
|
|
505
|
+
if (operator === '/' && next === 0) throw new Error('division');
|
|
506
|
+
result = operator === '*' ? result * next : result / next;
|
|
507
|
+
skipSpace();
|
|
508
|
+
}
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function readExpression(): number {
|
|
513
|
+
let result = readTerm();
|
|
514
|
+
skipSpace();
|
|
515
|
+
while (source[cursor] === '+' || source[cursor] === '-') {
|
|
516
|
+
const operator = source[cursor++];
|
|
517
|
+
const next = readTerm();
|
|
518
|
+
result = operator === '+' ? result + next : result - next;
|
|
519
|
+
skipSpace();
|
|
520
|
+
}
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
state.steps += formula.length;
|
|
526
|
+
if (state.steps > limits.maxFormulaSteps) throw new Error('limit');
|
|
527
|
+
const result = readExpression();
|
|
528
|
+
if (cursor !== source.length || !Number.isFinite(result)) return '#VALUE!';
|
|
529
|
+
return result;
|
|
530
|
+
} catch (error) {
|
|
531
|
+
return error instanceof Error && error.message === 'cycle'
|
|
532
|
+
? '#CYCLE!'
|
|
533
|
+
: error instanceof Error && error.message === 'limit' ? '#LIMIT!' : '#VALUE!';
|
|
534
|
+
} finally {
|
|
535
|
+
state.depth -= 1;
|
|
536
|
+
}
|
|
141
537
|
}
|
|
142
538
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
539
|
+
const evaluatedCells = $derived.by(() => {
|
|
540
|
+
const cells = cellSnapshot;
|
|
541
|
+
const values = new Map<string, number | string>();
|
|
542
|
+
if (!cells) return values;
|
|
543
|
+
const sheetCells = new Map<string, Record<string, string>>();
|
|
544
|
+
const sheetNames = new Map<string, string>();
|
|
545
|
+
for (const sheet of sheets) {
|
|
546
|
+
sheetCells.set(sheet.id.toLowerCase(), sheet.cells);
|
|
547
|
+
sheetNames.set(sheet.id.toLowerCase(), sheet.id);
|
|
548
|
+
sheetNames.set(sheet.name.toLowerCase(), sheet.id);
|
|
151
549
|
}
|
|
152
|
-
|
|
550
|
+
const resolveSheet = (name: string | undefined): Record<string, string> | undefined => {
|
|
551
|
+
if (name === undefined) return cells;
|
|
552
|
+
const sheetId = sheetNames.get(name.toLowerCase());
|
|
553
|
+
return sheetId === undefined ? undefined : sheetCells.get(sheetId.toLowerCase());
|
|
554
|
+
};
|
|
555
|
+
// 同一数据快照共享预算和依赖缓存,禁止为每个单元格重新启动昂贵计算。
|
|
556
|
+
const state: EvaluationState = { depth: 0, steps: 0, cache: values };
|
|
557
|
+
for (const [key, raw] of Object.entries(cells)) {
|
|
558
|
+
if (!values.has(`__current__!${key}`)) {
|
|
559
|
+
const computed = evaluateFormula(raw, cells, new Set([`__current__!${key}`]), state, resolveSheet);
|
|
560
|
+
values.set(`__current__!${key}`, computed);
|
|
561
|
+
values.set(key, computed);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return values;
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
$effect.pre(() => {
|
|
568
|
+
if (selectionSheetId !== currentSheet?.id) {
|
|
569
|
+
selectionSheetId = currentSheet?.id;
|
|
570
|
+
selectedCell = 'A1';
|
|
571
|
+
selectedRangeEnd = 'A1';
|
|
572
|
+
selectionError = false;
|
|
573
|
+
}
|
|
574
|
+
if (cellSnapshot && !Object.hasOwn(cellSnapshot, selectedCell)) selectedCell = 'A1';
|
|
575
|
+
if (cellSnapshot && !Object.hasOwn(cellSnapshot, selectedRangeEnd)) selectedRangeEnd = selectedCell;
|
|
576
|
+
formulaInput = cellSnapshot?.[selectedCell] ?? '';
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
function getRenderedValue(key: string): string {
|
|
580
|
+
const computed = evaluatedCells.get(key) ?? '';
|
|
581
|
+
const format = cellFormat(key);
|
|
582
|
+
const value = typeof computed === 'number' ? computed
|
|
583
|
+
: format.numberFormat !== undefined && format.numberFormat !== 'general'
|
|
584
|
+
&& /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(computed) ? Number(computed) : computed;
|
|
585
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return String(computed);
|
|
586
|
+
const decimals = format.decimalPlaces;
|
|
587
|
+
const options = decimals === undefined || !Number.isInteger(decimals) || decimals < 0 || decimals > 20
|
|
588
|
+
? { maximumFractionDigits: 2 } : {
|
|
589
|
+
minimumFractionDigits: decimals, maximumFractionDigits: decimals,
|
|
590
|
+
};
|
|
591
|
+
return value.toLocaleString(i18n.locale, {
|
|
592
|
+
...options, style: format.numberFormat === 'percent' ? 'percent' : 'decimal',
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function cellFormat(key = selectedCell): SpreadsheetCellFormat {
|
|
597
|
+
return currentSheet?.formats?.[key] ?? {};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function updateCellFormat(patch: Partial<SpreadsheetCellFormat>) {
|
|
601
|
+
if (readonly || !currentSheet || !canRender) return;
|
|
602
|
+
const keys = selectedKeys();
|
|
603
|
+
const targets = keys.length > 1 ? keys : [selectedCell];
|
|
604
|
+
const nextSheets = sheets.map(sheet => {
|
|
605
|
+
if (sheet.id !== currentSheet.id) return sheet;
|
|
606
|
+
const formats = { ...(sheet.formats ?? {}) };
|
|
607
|
+
for (const key of targets) formats[key] = { ...(formats[key] ?? {}), ...patch };
|
|
608
|
+
return { ...sheet, formats };
|
|
609
|
+
});
|
|
610
|
+
commitSheets(nextSheets);
|
|
153
611
|
}
|
|
154
612
|
|
|
155
613
|
function selectCell(key: string) {
|
|
614
|
+
selectionError = false;
|
|
615
|
+
selectedRangeEnd = key;
|
|
156
616
|
selectedCell = key;
|
|
157
617
|
if (currentSheet) {
|
|
158
618
|
formulaInput = currentSheet.cells[key] ?? '';
|
|
159
619
|
}
|
|
160
620
|
}
|
|
161
621
|
|
|
622
|
+
function selectRange(key: string, extend: boolean) {
|
|
623
|
+
selectionError = false;
|
|
624
|
+
if (extend) {
|
|
625
|
+
selectedRangeEnd = key;
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
selectCell(key);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function rangeBounds() {
|
|
632
|
+
const start = parseCellKey(selectedCell);
|
|
633
|
+
const end = parseCellKey(selectedRangeEnd);
|
|
634
|
+
if (!start || !end) return null;
|
|
635
|
+
const startCol = colNameToIndex(start.col);
|
|
636
|
+
const endCol = colNameToIndex(end.col);
|
|
637
|
+
return {
|
|
638
|
+
firstCol: Math.min(startCol, endCol),
|
|
639
|
+
lastCol: Math.max(startCol, endCol),
|
|
640
|
+
firstRow: Math.min(start.row, end.row),
|
|
641
|
+
lastRow: Math.max(start.row, end.row),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function selectedKeys() {
|
|
646
|
+
const bounds = rangeBounds();
|
|
647
|
+
if (!bounds) return [];
|
|
648
|
+
const keys: string[] = [];
|
|
649
|
+
for (let row = bounds.firstRow; row <= bounds.lastRow; row += 1) {
|
|
650
|
+
for (let col = bounds.firstCol; col <= bounds.lastCol; col += 1) {
|
|
651
|
+
keys.push(`${getColName(col)}${row}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return keys;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function isCellSelected(key: string) {
|
|
658
|
+
const cell = parseCellKey(key);
|
|
659
|
+
const bounds = rangeBounds();
|
|
660
|
+
if (!cell || !bounds) return false;
|
|
661
|
+
const col = colNameToIndex(cell.col);
|
|
662
|
+
return cell.row >= bounds.firstRow && cell.row <= bounds.lastRow
|
|
663
|
+
&& col >= bounds.firstCol && col <= bounds.lastCol;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function applyCellValues(values: Map<string, string>): boolean {
|
|
667
|
+
if (readonly || !currentSheet || !canRender || !cellSnapshot) return false;
|
|
668
|
+
let textLength = Object.values(cellSnapshot).reduce((total, value) => total + value.length, 0);
|
|
669
|
+
for (const [key, value] of values) {
|
|
670
|
+
if (!Object.hasOwn(cellSnapshot, key) || value.length > limits.maxCellLength) return false;
|
|
671
|
+
textLength += value.length - (cellSnapshot[key]?.length ?? 0);
|
|
672
|
+
}
|
|
673
|
+
if (textLength > 1_000_000) return false;
|
|
674
|
+
const nextSheets = sheets.map((sheet) => sheet.id === currentSheet.id
|
|
675
|
+
? { ...sheet, cells: { ...sheet.cells, ...Object.fromEntries(values) } }
|
|
676
|
+
: sheet);
|
|
677
|
+
commitSheets(nextSheets);
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function clearSelection() {
|
|
682
|
+
synchronizeHistory();
|
|
683
|
+
const values = new Map(selectedKeys().map(key => [key, '']));
|
|
684
|
+
applyCellValues(values);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function translateFormula(value: string, rowOffset: number, colOffset: number): string {
|
|
688
|
+
if (!value.startsWith('=')) return value;
|
|
689
|
+
return value.replace(/(?<![A-Z0-9_$])(\$?)([A-Z]+)(\$?)([1-9]\d*)\b/gi, (
|
|
690
|
+
_match,
|
|
691
|
+
columnAbsolute: string,
|
|
692
|
+
letters: string,
|
|
693
|
+
rowAbsolute: string,
|
|
694
|
+
digits: string,
|
|
695
|
+
) => {
|
|
696
|
+
const row = Number(digits);
|
|
697
|
+
const col = colNameToIndex(letters.toUpperCase());
|
|
698
|
+
const nextRow = rowAbsolute ? row : row + rowOffset;
|
|
699
|
+
const nextCol = columnAbsolute ? col : col + colOffset;
|
|
700
|
+
if (nextRow < 1 || nextRow > gridRows || nextCol < 0 || nextCol >= gridCols) throw new Error('reference');
|
|
701
|
+
return `${columnAbsolute ? '$' : ''}${getColName(nextCol)}${rowAbsolute ? '$' : ''}${nextRow}`;
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function parseFillNumber(value: string): { value: number; precision: number } | undefined {
|
|
706
|
+
if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) return undefined;
|
|
707
|
+
const number = Number(value);
|
|
708
|
+
if (!Number.isFinite(number)) return undefined;
|
|
709
|
+
return { value: number, precision: value.includes('.') ? value.length - value.indexOf('.') - 1 : 0 };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function fillNumberSequence(bounds: NonNullable<ReturnType<typeof rangeBounds>>): {
|
|
713
|
+
first: number;
|
|
714
|
+
step: number;
|
|
715
|
+
precision: number;
|
|
716
|
+
} | undefined {
|
|
717
|
+
const vertical = bounds.firstCol === bounds.lastCol && bounds.lastRow - bounds.firstRow >= 1;
|
|
718
|
+
const horizontal = bounds.firstRow === bounds.lastRow && bounds.lastCol - bounds.firstCol >= 1;
|
|
719
|
+
if (!vertical && !horizontal) return undefined;
|
|
720
|
+
const firstKey = `${getColName(bounds.firstCol)}${bounds.firstRow}`;
|
|
721
|
+
const secondKey = vertical
|
|
722
|
+
? `${getColName(bounds.firstCol)}${bounds.firstRow + 1}`
|
|
723
|
+
: `${getColName(bounds.firstCol + 1)}${bounds.firstRow}`;
|
|
724
|
+
const first = parseFillNumber(currentSheet?.cells[firstKey] ?? '');
|
|
725
|
+
const second = parseFillNumber(currentSheet?.cells[secondKey] ?? '');
|
|
726
|
+
if (!first || !second) return undefined;
|
|
727
|
+
return {
|
|
728
|
+
first: first.value,
|
|
729
|
+
step: second.value - first.value,
|
|
730
|
+
precision: Math.max(first.precision, second.precision),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function fillSelection() {
|
|
735
|
+
if (readonly || !currentSheet || !canRender) return;
|
|
736
|
+
const bounds = rangeBounds();
|
|
737
|
+
if (!bounds || (bounds.firstRow === bounds.lastRow && bounds.firstCol === bounds.lastCol)) return;
|
|
738
|
+
const originKey = `${getColName(bounds.firstCol)}${bounds.firstRow}`;
|
|
739
|
+
const origin = currentSheet.cells[originKey] ?? '';
|
|
740
|
+
const sequence = fillNumberSequence(bounds);
|
|
741
|
+
const values = new Map<string, string>();
|
|
742
|
+
try {
|
|
743
|
+
for (let row = bounds.firstRow; row <= bounds.lastRow; row += 1) {
|
|
744
|
+
for (let col = bounds.firstCol; col <= bounds.lastCol; col += 1) {
|
|
745
|
+
const key = `${getColName(col)}${row}`;
|
|
746
|
+
const offset = bounds.firstCol === bounds.lastCol
|
|
747
|
+
? row - bounds.firstRow
|
|
748
|
+
: col - bounds.firstCol;
|
|
749
|
+
const translated = translateFormula(origin, row - bounds.firstRow, col - bounds.firstCol);
|
|
750
|
+
if (sequence && !origin.startsWith('=')) {
|
|
751
|
+
const value = sequence.first + sequence.step * offset;
|
|
752
|
+
if (!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER
|
|
753
|
+
|| sequence.precision > 20) throw new Error('sequence');
|
|
754
|
+
values.set(key, offset < 2 ? currentSheet.cells[key] ?? ''
|
|
755
|
+
: sequence.precision > 0 ? value.toFixed(sequence.precision) : String(value));
|
|
756
|
+
} else {
|
|
757
|
+
values.set(key, translated);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
} catch {
|
|
762
|
+
selectionError = true;
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
selectionError = !applyCellValues(values);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function handleCellKeydown(event: KeyboardEvent) {
|
|
769
|
+
if (event.isComposing) return;
|
|
770
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'd') {
|
|
771
|
+
event.preventDefault();
|
|
772
|
+
fillSelection();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const end = parseCellKey(selectedRangeEnd);
|
|
776
|
+
if (event.shiftKey && end && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
|
777
|
+
event.preventDefault();
|
|
778
|
+
const col = Math.max(0, Math.min(gridCols - 1, colNameToIndex(end.col)
|
|
779
|
+
+ (event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0)));
|
|
780
|
+
const row = Math.max(1, Math.min(gridRows, end.row
|
|
781
|
+
+ (event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0)));
|
|
782
|
+
selectedRangeEnd = `${getColName(col)}${row}`;
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if ((event.key === 'Delete' || event.key === 'Backspace') && selectedKeys().length > 1) {
|
|
786
|
+
event.preventDefault();
|
|
787
|
+
clearSelection();
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function handlePaste(event: ClipboardEvent) {
|
|
792
|
+
event.preventDefault();
|
|
793
|
+
synchronizeHistory();
|
|
794
|
+
if (readonly || !currentSheet || !canRender || !event.clipboardData) return;
|
|
795
|
+
const csv = event.clipboardData.getData('text/csv');
|
|
796
|
+
const text = csv || event.clipboardData.getData('text/plain');
|
|
797
|
+
const rows = parseSpreadsheetClipboard(text, csv ? 'csv' : 'tsv');
|
|
798
|
+
const bounds = rangeBounds();
|
|
799
|
+
selectionError = true;
|
|
800
|
+
const firstPastedRow = rows?.[0];
|
|
801
|
+
if (!rows || !firstPastedRow || !bounds) return;
|
|
802
|
+
const firstRow = bounds.firstRow;
|
|
803
|
+
const firstCol = bounds.firstCol;
|
|
804
|
+
const values = new Map<string, string>();
|
|
805
|
+
for (const [rowOffset, row] of rows.entries()) {
|
|
806
|
+
for (const [colOffset, value] of row.entries()) {
|
|
807
|
+
const rowIndex = firstRow + rowOffset;
|
|
808
|
+
const colIndex = firstCol + colOffset;
|
|
809
|
+
if (rowIndex > gridRows || colIndex >= gridCols) return;
|
|
810
|
+
values.set(`${getColName(colIndex)}${rowIndex}`, value);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (!applyCellValues(values)) return;
|
|
814
|
+
selectionError = false;
|
|
815
|
+
selectedCell = `${getColName(firstCol)}${firstRow}`;
|
|
816
|
+
selectedRangeEnd = `${getColName(firstCol + firstPastedRow.length - 1)}${firstRow + rows.length - 1}`;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function handleCopy(event: ClipboardEvent) {
|
|
820
|
+
if (!canRender || !currentSheet || selectedKeys().length <= 1 || !event.clipboardData) return;
|
|
821
|
+
const bounds = rangeBounds();
|
|
822
|
+
if (!bounds) return;
|
|
823
|
+
const rows: (string | number)[][] = [];
|
|
824
|
+
for (let row = bounds.firstRow; row <= bounds.lastRow; row += 1) {
|
|
825
|
+
const values: (string | number)[] = [];
|
|
826
|
+
for (let col = bounds.firstCol; col <= bounds.lastCol; col += 1) {
|
|
827
|
+
values.push(evaluatedCells.get(`${getColName(col)}${row}`) ?? '');
|
|
828
|
+
}
|
|
829
|
+
rows.push(values);
|
|
830
|
+
}
|
|
831
|
+
event.preventDefault();
|
|
832
|
+
const tsv = serializeSpreadsheetClipboard(rows, 'tsv');
|
|
833
|
+
const csv = serializeSpreadsheetClipboard(rows, 'csv');
|
|
834
|
+
if (tsv === undefined || csv === undefined) return;
|
|
835
|
+
event.clipboardData.setData('text/plain', tsv);
|
|
836
|
+
event.clipboardData.setData('text/csv', csv);
|
|
837
|
+
}
|
|
838
|
+
|
|
162
839
|
function updateCellValue(key: string, val: string) {
|
|
163
|
-
if (readonly || !currentSheet)
|
|
840
|
+
if (readonly || !currentSheet || !canRender || !cellSnapshot || !Object.hasOwn(cellSnapshot, key)
|
|
841
|
+
|| val.length > limits.maxCellLength) return;
|
|
842
|
+
const textLength = Object.values(cellSnapshot).reduce((total, value) => total + value.length, 0);
|
|
843
|
+
if (textLength - (cellSnapshot[key]?.length ?? 0) + val.length > 1_000_000) return;
|
|
164
844
|
const nextSheets = sheets.map((s) => {
|
|
165
845
|
if (s.id === currentSheet.id) {
|
|
166
846
|
return {
|
|
@@ -173,120 +853,335 @@
|
|
|
173
853
|
}
|
|
174
854
|
return s;
|
|
175
855
|
});
|
|
176
|
-
|
|
177
|
-
onchange?.(sheets);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function handleFormulaCommit() {
|
|
181
|
-
if (!selectedCell) return;
|
|
182
|
-
updateCellValue(selectedCell, formulaInput);
|
|
856
|
+
commitSheets(nextSheets);
|
|
183
857
|
}
|
|
184
858
|
|
|
185
859
|
function addRow() {
|
|
186
|
-
if (!currentSheet) return;
|
|
860
|
+
if (readonly || !currentSheet || !canAddRow) return;
|
|
187
861
|
const next = sheets.map((s) => (s.id === currentSheet.id ? { ...s, rows: s.rows + 1 } : s));
|
|
188
|
-
|
|
189
|
-
onchange?.(sheets);
|
|
862
|
+
commitSheets(next);
|
|
190
863
|
}
|
|
191
864
|
|
|
192
865
|
function addCol() {
|
|
193
|
-
if (!currentSheet) return;
|
|
866
|
+
if (readonly || !currentSheet || !canAddCol) return;
|
|
194
867
|
const next = sheets.map((s) => (s.id === currentSheet.id ? { ...s, cols: s.cols + 1 } : s));
|
|
195
|
-
|
|
196
|
-
|
|
868
|
+
commitSheets(next);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function undo() {
|
|
872
|
+
synchronizeHistory();
|
|
873
|
+
if (readonly || undoHistory.length === 0) return;
|
|
874
|
+
const previous = undoHistory.at(-1);
|
|
875
|
+
if (!previous) return;
|
|
876
|
+
redoHistory = pushBounded(redoHistory);
|
|
877
|
+
undoHistory = undoHistory.slice(0, -1);
|
|
878
|
+
restoreHistory(previous);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function redo() {
|
|
882
|
+
synchronizeHistory();
|
|
883
|
+
if (readonly || redoHistory.length === 0) return;
|
|
884
|
+
const next = redoHistory.at(-1);
|
|
885
|
+
if (!next) return;
|
|
886
|
+
undoHistory = pushBounded(undoHistory);
|
|
887
|
+
redoHistory = redoHistory.slice(0, -1);
|
|
888
|
+
restoreHistory(next);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function restoreHistory(entry: HistoryEntry) {
|
|
892
|
+
// 历史仅来自当前组件序列化的 SheetData,不接收外部历史载荷。
|
|
893
|
+
const restored = JSON.parse(entry.data) as SheetData[];
|
|
894
|
+
sheets = restored;
|
|
895
|
+
activeSheetId = entry.activeSheetId;
|
|
896
|
+
committedSheets = sheets;
|
|
897
|
+
committedFingerprint = fingerprintSheets(restored);
|
|
898
|
+
committedScopeKey = scopeKey;
|
|
899
|
+
notifyChange(restored);
|
|
197
900
|
}
|
|
198
901
|
|
|
199
902
|
function exportCsv() {
|
|
200
|
-
if (!currentSheet) return;
|
|
201
|
-
const
|
|
903
|
+
if (!currentSheet || !canRender) return;
|
|
904
|
+
const records: Record<string, unknown>[] = [];
|
|
202
905
|
for (let r = 1; r <= currentSheet.rows; r++) {
|
|
203
|
-
const
|
|
906
|
+
const row: Record<string, unknown> = {};
|
|
204
907
|
for (let c = 0; c < currentSheet.cols; c++) {
|
|
205
908
|
const k = `${getColName(c)}${r}`;
|
|
206
|
-
const
|
|
207
|
-
|
|
909
|
+
const computed = evaluatedCells.get(k) ?? '';
|
|
910
|
+
// 文本先中和公式前缀,数值保留机器可读精度;CSV 引号由公共编码器负责。
|
|
911
|
+
row[getColName(c)] = typeof computed === 'string' && (/^[\t\r\n]/.test(computed) || /^\s*[=+@-]/.test(computed))
|
|
912
|
+
? `'${computed}` : computed;
|
|
208
913
|
}
|
|
209
|
-
|
|
914
|
+
records.push(row);
|
|
210
915
|
}
|
|
211
|
-
|
|
916
|
+
// 公共编码器包含表头;工作表导出只保留原有数据行。
|
|
917
|
+
const csvContent = toCsv(records).split('\n').slice(1).join('\n');
|
|
212
918
|
if (onexport) {
|
|
213
919
|
onexport(csvContent);
|
|
214
920
|
} else {
|
|
215
921
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
216
922
|
const a = document.createElement('a');
|
|
217
|
-
|
|
923
|
+
const url = URL.createObjectURL(blob);
|
|
924
|
+
a.href = url;
|
|
218
925
|
a.download = `${currentSheet.name}_${Date.now()}.csv`;
|
|
219
|
-
|
|
926
|
+
try {
|
|
927
|
+
a.click();
|
|
928
|
+
} finally {
|
|
929
|
+
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function exportWorkbook() {
|
|
935
|
+
const json = workbookJson;
|
|
936
|
+
if (json === undefined) return;
|
|
937
|
+
if (onworkbookexport) {
|
|
938
|
+
onworkbookexport(json);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const url = URL.createObjectURL(new Blob([json], { type: 'application/json;charset=utf-8;' }));
|
|
942
|
+
const anchor = document.createElement('a');
|
|
943
|
+
anchor.href = url;
|
|
944
|
+
anchor.download = 'workbook.json';
|
|
945
|
+
try {
|
|
946
|
+
anchor.click();
|
|
947
|
+
} finally {
|
|
948
|
+
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function workbookFitsLimits(workbook: SpreadsheetWorkbook): boolean {
|
|
953
|
+
return workbook.sheets.length <= limits.maxSheets && workbook.sheets.every(sheet =>
|
|
954
|
+
sheet.rows <= limits.maxRows && sheet.cols <= limits.maxCols
|
|
955
|
+
&& sheet.rows * sheet.cols <= limits.maxCells
|
|
956
|
+
&& Object.values(sheet.cells).every(value => value.length <= limits.maxCellLength));
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async function exportWorkbookXlsx() {
|
|
960
|
+
const json = workbookJson;
|
|
961
|
+
const exporter = xlsxexporter;
|
|
962
|
+
const deliver = onxlsxexport;
|
|
963
|
+
if (json === undefined || !exporter || workbookXlsxExporting) return;
|
|
964
|
+
const request = {};
|
|
965
|
+
const context = xlsxContext;
|
|
966
|
+
xlsxExportRequest = request;
|
|
967
|
+
const current = () => importMounted && xlsxExportRequest === request && context === xlsxContext;
|
|
968
|
+
workbookXlsxError = false;
|
|
969
|
+
workbookXlsxExporting = true;
|
|
970
|
+
try {
|
|
971
|
+
const workbook = parseSpreadsheetWorkbook(json);
|
|
972
|
+
if (!workbook) throw new Error('invalid workbook');
|
|
973
|
+
const bytes = await exporter(workbook);
|
|
974
|
+
if (!current()) return;
|
|
975
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength < 4 || bytes.byteLength > 50 * 1024 * 1024
|
|
976
|
+
|| bytes[0] !== 80 || bytes[1] !== 75 || bytes[2] !== 3 || bytes[3] !== 4) throw new Error('invalid export');
|
|
977
|
+
const snapshot = new Uint8Array(bytes);
|
|
978
|
+
if (deliver) {
|
|
979
|
+
await deliver(snapshot);
|
|
980
|
+
} else {
|
|
981
|
+
const url = URL.createObjectURL(new Blob([snapshot], {
|
|
982
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
983
|
+
}));
|
|
984
|
+
try {
|
|
985
|
+
const anchor = document.createElement('a');
|
|
986
|
+
anchor.href = url;
|
|
987
|
+
anchor.download = 'workbook.xlsx';
|
|
988
|
+
anchor.click();
|
|
989
|
+
} finally {
|
|
990
|
+
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
} catch {
|
|
994
|
+
if (current()) workbookXlsxError = true;
|
|
995
|
+
} finally {
|
|
996
|
+
if (current()) workbookXlsxExporting = false;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async function importWorkbook(event: Event) {
|
|
1001
|
+
const input = event.currentTarget;
|
|
1002
|
+
if (!(input instanceof HTMLInputElement)) return;
|
|
1003
|
+
const file = input.files?.[0];
|
|
1004
|
+
input.value = '';
|
|
1005
|
+
workbookImportError = false;
|
|
1006
|
+
pendingWorkbook = undefined;
|
|
1007
|
+
importController?.abort();
|
|
1008
|
+
const controller = new AbortController();
|
|
1009
|
+
importController = controller;
|
|
1010
|
+
const importer = xlsximporter;
|
|
1011
|
+
const request = {};
|
|
1012
|
+
importRequest = request;
|
|
1013
|
+
const context = importContext;
|
|
1014
|
+
const current = () => importMounted && importRequest === request && context === importContext && !readonly;
|
|
1015
|
+
if (readonly || !file || file.size > 10_000_000) {
|
|
1016
|
+
workbookImportError = Boolean(file);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
1021
|
+
if (!current()) return;
|
|
1022
|
+
const isXlsx = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
1023
|
+
|| file.name.toLowerCase().endsWith('.xlsx')
|
|
1024
|
+
|| bytes[0] === 0x50 && bytes[1] === 0x4b;
|
|
1025
|
+
const workbook = isXlsx
|
|
1026
|
+
? importer ? snapshotSpreadsheetWorkbook(await importer(bytes.slice(), { ...limits }, controller.signal)) : undefined
|
|
1027
|
+
: parseSpreadsheetWorkbook(new TextDecoder().decode(bytes));
|
|
1028
|
+
if (!current()) return;
|
|
1029
|
+
if (!workbook || !workbookFitsLimits(workbook)) throw new Error('invalid workbook');
|
|
1030
|
+
pendingWorkbook = { workbook, context };
|
|
1031
|
+
} catch {
|
|
1032
|
+
if (current()) workbookImportError = true;
|
|
220
1033
|
}
|
|
221
1034
|
}
|
|
222
1035
|
|
|
1036
|
+
function confirmWorkbookImport() {
|
|
1037
|
+
if (!importReady || !pendingWorkbook) return;
|
|
1038
|
+
const workbook = pendingWorkbook.workbook;
|
|
1039
|
+
pendingWorkbook = undefined;
|
|
1040
|
+
const firstSheet = workbook.sheets[0];
|
|
1041
|
+
if (!firstSheet) return;
|
|
1042
|
+
commitSheets(workbook.sheets, workbook.activeSheetId ?? firstSheet.id);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
223
1045
|
function addSheet() {
|
|
224
|
-
|
|
1046
|
+
if (readonly || !validWorkbook || sheets.length >= limits.maxSheets) return;
|
|
1047
|
+
const id = `sheet_${crypto.randomUUID()}`;
|
|
225
1048
|
const newSheet: SheetData = {
|
|
226
1049
|
id,
|
|
227
|
-
name:
|
|
228
|
-
rows: 6,
|
|
229
|
-
cols: 5,
|
|
1050
|
+
name: i18n.t('spreadsheet.sheetName', { number: String(sheets.length + 1) }),
|
|
1051
|
+
rows: Math.min(6, limits.maxRows, limits.maxCells),
|
|
1052
|
+
cols: Math.min(5, limits.maxCols, Math.max(1, Math.floor(limits.maxCells / Math.min(6, limits.maxRows, limits.maxCells)))),
|
|
230
1053
|
cells: {},
|
|
231
1054
|
};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
onchange?.(sheets);
|
|
1055
|
+
const nextSheets = [...sheets, newSheet];
|
|
1056
|
+
commitSheets(nextSheets, id);
|
|
235
1057
|
}
|
|
236
1058
|
</script>
|
|
237
1059
|
|
|
238
|
-
<div class={cn('svadmin-u-a217b4eaa918 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 svadmin-u-cef5b893cf23 svadmin-u-359090c2d529 svadmin-u-6f7e013d6499 svadmin-u-eb6e8b881acd svadmin-u-2cd02d11d1af', className)}>
|
|
239
|
-
<!-- Header Toolbar -->
|
|
1060
|
+
<div data-testid="spreadsheet-view" class={cn('svadmin-u-a217b4eaa918 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 svadmin-u-cef5b893cf23 svadmin-u-359090c2d529 svadmin-u-6f7e013d6499 svadmin-u-eb6e8b881acd svadmin-u-2cd02d11d1af', className)}>
|
|
240
1061
|
<div class="svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-77a2a20e90d4 svadmin-u-f4cc511ff0c1 svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff">
|
|
241
1062
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
|
|
242
1063
|
<FileSpreadsheet class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-20aaf08a7ed1 svadmin-u-012fbd121f37" />
|
|
243
|
-
<span class="svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{currentSheet?.name ?? '
|
|
1064
|
+
<span class="svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{currentSheet?.name ?? i18n.t('spreadsheet.title')}</span>
|
|
244
1065
|
</div>
|
|
245
1066
|
|
|
246
|
-
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-58284b4ea568">
|
|
1067
|
+
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-58284b4ea568" style:flex-wrap="wrap">
|
|
1068
|
+
<input bind:this={fileInput} aria-label={i18n.t('spreadsheet.importWorkbook')} hidden type="file"
|
|
1069
|
+
accept={xlsximporter ? 'application/json,.json,.xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : 'application/json,.json'}
|
|
1070
|
+
onchange={importWorkbook} />
|
|
247
1071
|
{#if !readonly}
|
|
248
|
-
<Button variant="outline" size="sm"
|
|
1072
|
+
<Button variant="outline" size="sm" onclick={() => fileInput?.click()}>
|
|
1073
|
+
<Upload class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
|
|
1074
|
+
{i18n.t('spreadsheet.importWorkbook')}
|
|
1075
|
+
</Button>
|
|
1076
|
+
<Button variant="ghost" size="sm" disabled={undoHistory.length === 0} class="svadmin-u-f6fe902450dc svadmin-u-8a539c7fe216" onclick={undo} aria-label={i18n.t('spreadsheet.undo')} title={i18n.t('spreadsheet.undo')}>
|
|
1077
|
+
<Undo2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
|
|
1078
|
+
</Button>
|
|
1079
|
+
<Button variant="ghost" size="sm" disabled={redoHistory.length === 0} class="svadmin-u-f6fe902450dc svadmin-u-8a539c7fe216" onclick={redo} aria-label={i18n.t('spreadsheet.redo')} title={i18n.t('spreadsheet.redo')}>
|
|
1080
|
+
<Redo2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
|
|
1081
|
+
</Button>
|
|
1082
|
+
<Button variant="ghost" size="sm" disabled={!canRender} onclick={clearSelection} aria-label={i18n.t('spreadsheet.clear')} title={i18n.t('spreadsheet.clear')}>
|
|
1083
|
+
<Trash2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
|
|
1084
|
+
</Button>
|
|
1085
|
+
<Button variant="ghost" size="sm" disabled={!canRender || selectedKeys().length < 2} onclick={fillSelection} aria-label={i18n.t('spreadsheet.fill')} title={i18n.t('spreadsheet.fill')}>
|
|
1086
|
+
<Copy aria-hidden="true" />
|
|
1087
|
+
</Button>
|
|
1088
|
+
<Button variant="ghost" size="sm" disabled={!canRender} aria-pressed={cellFormat().bold === true} onclick={() => updateCellFormat({ bold: !cellFormat().bold })} aria-label={i18n.t('spreadsheet.bold')} title={i18n.t('spreadsheet.bold')}>
|
|
1089
|
+
<Bold aria-hidden="true" />
|
|
1090
|
+
</Button>
|
|
1091
|
+
<Button variant="ghost" size="sm" disabled={!canRender} aria-pressed={cellFormat().italic === true} onclick={() => updateCellFormat({ italic: !cellFormat().italic })} aria-label={i18n.t('spreadsheet.italic')} title={i18n.t('spreadsheet.italic')}>
|
|
1092
|
+
<Italic aria-hidden="true" />
|
|
1093
|
+
</Button>
|
|
1094
|
+
<select disabled={!canRender} aria-label={i18n.t('spreadsheet.numberFormat')} value={cellFormat().numberFormat ?? 'general'} onchange={(event) => {
|
|
1095
|
+
const value = event.currentTarget.value;
|
|
1096
|
+
if (value === 'general' || value === 'number' || value === 'percent') updateCellFormat({ numberFormat: value });
|
|
1097
|
+
}}>
|
|
1098
|
+
<option value="general">{i18n.t('spreadsheet.general')}</option>
|
|
1099
|
+
<option value="number">{i18n.t('spreadsheet.number')}</option>
|
|
1100
|
+
<option value="percent">{i18n.t('spreadsheet.percent')}</option>
|
|
1101
|
+
</select>
|
|
1102
|
+
<input type="number" min="0" max="20" step="1" style:width="4rem"
|
|
1103
|
+
disabled={!canRender} aria-label={i18n.t('spreadsheet.decimalPlaces')}
|
|
1104
|
+
value={cellFormat().decimalPlaces ?? 2} onchange={(event) => {
|
|
1105
|
+
const value = event.currentTarget.valueAsNumber;
|
|
1106
|
+
if (Number.isInteger(value) && value >= 0 && value <= 20) updateCellFormat({ decimalPlaces: value });
|
|
1107
|
+
}} />
|
|
1108
|
+
<select disabled={!canRender} aria-label={i18n.t('spreadsheet.align')} value={cellFormat().align ?? 'left'} onchange={(event) => {
|
|
1109
|
+
const value = event.currentTarget.value;
|
|
1110
|
+
if (value === 'left' || value === 'center' || value === 'right') updateCellFormat({ align: value });
|
|
1111
|
+
}}>
|
|
1112
|
+
<option value="left">{i18n.t('spreadsheet.left')}</option>
|
|
1113
|
+
<option value="center">{i18n.t('spreadsheet.center')}</option>
|
|
1114
|
+
<option value="right">{i18n.t('spreadsheet.right')}</option>
|
|
1115
|
+
</select>
|
|
1116
|
+
<Button variant="outline" size="sm" disabled={!canAddRow} class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421" onclick={addRow}>
|
|
249
1117
|
<Plus class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
|
|
250
|
-
|
|
1118
|
+
{i18n.t('spreadsheet.row')}
|
|
251
1119
|
</Button>
|
|
252
|
-
<Button variant="outline" size="sm" class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421" onclick={addCol}>
|
|
1120
|
+
<Button variant="outline" size="sm" disabled={!canAddCol} class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421" onclick={addCol}>
|
|
253
1121
|
<Plus class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
|
|
254
|
-
|
|
1122
|
+
{i18n.t('spreadsheet.column')}
|
|
255
1123
|
</Button>
|
|
256
1124
|
{/if}
|
|
257
1125
|
|
|
258
|
-
<Button size="sm" class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421" onclick={exportCsv}>
|
|
1126
|
+
<Button size="sm" disabled={!canRender} class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421" onclick={exportCsv}>
|
|
1127
|
+
<Download class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
|
|
1128
|
+
{i18n.t('spreadsheet.export')}
|
|
1129
|
+
</Button>
|
|
1130
|
+
<Button size="sm" disabled={workbookJson === undefined} class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421" onclick={exportWorkbook}>
|
|
259
1131
|
<Download class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
|
|
260
|
-
|
|
1132
|
+
{i18n.t('spreadsheet.exportWorkbook')}
|
|
261
1133
|
</Button>
|
|
1134
|
+
{#if xlsxexporter}
|
|
1135
|
+
<Button size="sm" disabled={workbookJson === undefined || workbookXlsxExporting} aria-busy={workbookXlsxExporting}
|
|
1136
|
+
class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421" onclick={exportWorkbookXlsx}>
|
|
1137
|
+
<Download class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
|
|
1138
|
+
{i18n.t('spreadsheet.exportXlsx')}
|
|
1139
|
+
</Button>
|
|
1140
|
+
{/if}
|
|
262
1141
|
</div>
|
|
263
1142
|
</div>
|
|
1143
|
+
{#if workbookImportError}<p role="alert">{i18n.t('spreadsheet.importRejected')}</p>{/if}
|
|
1144
|
+
{#if workbookXlsxError}<p role="alert">{i18n.t('common.operationFailed')}</p>{/if}
|
|
1145
|
+
{#if importReady && pendingWorkbook}
|
|
1146
|
+
<div role="group" aria-label={i18n.t('spreadsheet.confirmImport')}>
|
|
1147
|
+
<p>{i18n.t('spreadsheet.replaceWorkbook')}</p>
|
|
1148
|
+
<Button onclick={confirmWorkbookImport}>{i18n.t('spreadsheet.confirmImport')}</Button>
|
|
1149
|
+
<Button variant="ghost" onclick={() => { pendingWorkbook = undefined; }}>{i18n.t('common.cancel')}</Button>
|
|
1150
|
+
</div>
|
|
1151
|
+
{/if}
|
|
264
1152
|
|
|
265
1153
|
<!-- Formula Bar -->
|
|
266
1154
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-d5eab218aa34 svadmin-u-ec0091ee009b svadmin-u-5f22e64f2282 svadmin-u-b00f43c30c2b svadmin-u-ca6bcd4b6f3f svadmin-u-05faf5c801ff svadmin-u-0e65706bcccd">
|
|
267
|
-
<div class="svadmin-u-d5eab218aa34 svadmin-u-465609a240a8 svadmin-u-07389a777c1f svadmin-u-e6f9e383a762 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-d4108abe6359 svadmin-u-e83a7042bc91 svadmin-u-d058ca6de60f svadmin-u-28ae52cc20ae svadmin-u-ca6bf63030aa">
|
|
268
|
-
{selectedCell}
|
|
269
|
-
</div>
|
|
1155
|
+
<div class="svadmin-u-d5eab218aa34 svadmin-u-465609a240a8 svadmin-u-07389a777c1f svadmin-u-e6f9e383a762 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-d4108abe6359 svadmin-u-e83a7042bc91 svadmin-u-d058ca6de60f svadmin-u-28ae52cc20ae svadmin-u-ca6bf63030aa">{selectedCell}</div>
|
|
270
1156
|
<span class="svadmin-u-bfa603190748 svadmin-u-359090c2d529">fx</span>
|
|
271
1157
|
<input
|
|
272
1158
|
type="text"
|
|
273
1159
|
bind:value={formulaInput}
|
|
274
1160
|
{readonly}
|
|
275
|
-
|
|
1161
|
+
disabled={!canRender}
|
|
1162
|
+
maxlength={limits.maxCellLength}
|
|
1163
|
+
aria-label={i18n.t('spreadsheet.formula')}
|
|
276
1164
|
class="svadmin-u-36e579c0b41c svadmin-u-f6fe902450dc svadmin-u-7f19cdf4c5bb svadmin-u-119b2aa0b8f6 svadmin-u-d8e0e382c67b svadmin-u-359090c2d529 svadmin-u-d4108abe6359 svadmin-u-f10f771f87e9 svadmin-u-35ca6b75d707 svadmin-u-0e65706bcccd"
|
|
277
|
-
oninput={
|
|
278
|
-
onkeydown={(e) => { if (e.key === 'Enter')
|
|
1165
|
+
oninput={(e) => updateCellValue(selectedCell, e.currentTarget.value)}
|
|
1166
|
+
onkeydown={(e) => { if (e.key === 'Enter') updateCellValue(selectedCell, e.currentTarget.value); }}
|
|
279
1167
|
/>
|
|
280
1168
|
</div>
|
|
281
1169
|
|
|
282
1170
|
<!-- Spreadsheet Grid -->
|
|
283
|
-
{#if
|
|
1171
|
+
{#if selectionError}
|
|
1172
|
+
<p role="alert">{i18n.t('spreadsheet.pasteRejected')}</p>
|
|
1173
|
+
{/if}
|
|
1174
|
+
{#if !validWorkbook || (currentSheet && !canRender)}
|
|
1175
|
+
<p role="alert">{i18n.t('spreadsheet.invalid')}</p>
|
|
1176
|
+
{:else if !currentSheet}
|
|
1177
|
+
<p role="status">{i18n.t('spreadsheet.empty')}</p>
|
|
1178
|
+
{:else}
|
|
284
1179
|
<div class="svadmin-u-73fc3fb18ceb svadmin-u-b63b7d8eef5d svadmin-u-ca6bcd4b6f3f svadmin-u-05faf5c801ff svadmin-u-5f22e64f2282">
|
|
285
1180
|
<table class="svadmin-u-6da6a3c3f741 svadmin-u-4583f90cd9bd svadmin-u-2eba0d65d059 svadmin-u-0e65706bcccd">
|
|
286
1181
|
<thead class="svadmin-u-706701550477 svadmin-u-3e0fd166d494 svadmin-u-2167406b24d7 svadmin-u-236812d64c82 svadmin-u-bfa603190748">
|
|
287
1182
|
<tr>
|
|
288
1183
|
<th class="svadmin-u-d854e5698b57 svadmin-u-cd009d7d208c svadmin-u-ca6bf63030aa svadmin-u-5ceb636bd9f3 svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff svadmin-u-85d919893645 svadmin-u-1dc571a3609f">#</th>
|
|
289
|
-
{#each Array.from({ length:
|
|
1184
|
+
{#each Array.from({ length: gridCols }) as _, c (c)}
|
|
290
1185
|
<th class="svadmin-u-25effcb585ab svadmin-u-cd009d7d208c svadmin-u-ca6bf63030aa svadmin-u-5ceb636bd9f3 svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359 svadmin-u-359090c2d529">
|
|
291
1186
|
{getColName(c)}
|
|
292
1187
|
</th>
|
|
@@ -294,32 +1189,50 @@
|
|
|
294
1189
|
</tr>
|
|
295
1190
|
</thead>
|
|
296
1191
|
<tbody class="svadmin-u-fa6acbf81d74 svadmin-u-08b5607c7258">
|
|
297
|
-
{#each Array.from({ length:
|
|
1192
|
+
{#each Array.from({ length: gridRows }) as _, r (r)}
|
|
298
1193
|
{@const rowNum = r + 1}
|
|
299
1194
|
<tr class="svadmin-u-9fc5d875f5ca">
|
|
300
1195
|
<td class="svadmin-u-d854e5698b57 svadmin-u-eb6a3cef9686 svadmin-u-ca6bf63030aa svadmin-u-5ceb636bd9f3 svadmin-u-05faf5c801ff svadmin-u-2859c861d7de svadmin-u-1dc571a3609f svadmin-u-e83a7042bc91 svadmin-u-bfa603190748 svadmin-u-7f6912283f11">
|
|
301
1196
|
{rowNum}
|
|
302
1197
|
</td>
|
|
303
|
-
{#each Array.from({ length:
|
|
1198
|
+
{#each Array.from({ length: gridCols }) as _, c (c)}
|
|
304
1199
|
{@const cellKey = `${getColName(c)}${rowNum}`}
|
|
305
|
-
{@const
|
|
1200
|
+
{@const computedCell = evaluatedCells.get(cellKey)}
|
|
1201
|
+
{@const errorCell = typeof computedCell === 'string' ? computedCell : ''}
|
|
1202
|
+
{@const isSelected = isCellSelected(cellKey)}
|
|
306
1203
|
<td
|
|
1204
|
+
data-selected={isSelected || undefined}
|
|
307
1205
|
class={cn(
|
|
308
1206
|
'svadmin-u-8a539c7fe216 svadmin-u-5ceb636bd9f3 svadmin-u-6ee2d41e2d2d svadmin-u-d89972fe17d6 svadmin-u-34516836730d',
|
|
309
1207
|
isSelected ? 'svadmin-u-16b1efa5875e svadmin-u-fd1dda07320a svadmin-u-236812d64c82 svadmin-u-989c466fdbe7' : ''
|
|
310
1208
|
)}
|
|
311
|
-
|
|
1209
|
+
onmousedown={(event) => {
|
|
1210
|
+
if (event.shiftKey) event.preventDefault();
|
|
1211
|
+
}}
|
|
1212
|
+
onclick={(event) => selectRange(cellKey, event.shiftKey)}
|
|
312
1213
|
>
|
|
313
1214
|
<input
|
|
314
1215
|
type="text"
|
|
315
1216
|
{readonly}
|
|
316
|
-
|
|
1217
|
+
aria-label={cellKey}
|
|
1218
|
+
maxlength={limits.maxCellLength}
|
|
1219
|
+
value={selectedCell === cellKey && !readonly
|
|
1220
|
+
&& !errorCell.startsWith('#')
|
|
1221
|
+
? (cellSnapshot?.[cellKey] ?? '')
|
|
1222
|
+
: getRenderedValue(cellKey)}
|
|
317
1223
|
class={cn(
|
|
318
1224
|
'svadmin-u-6da6a3c3f741 svadmin-u-d0a52b312f7d svadmin-u-d5eab218aa34 svadmin-u-7f19cdf4c5bb svadmin-u-119b2aa0b8f6 svadmin-u-359090c2d529 svadmin-u-0e65706bcccd svadmin-u-f10f771f87e9',
|
|
319
|
-
typeof
|
|
1225
|
+
typeof computedCell === 'number' ? 'svadmin-u-308fc069e46e' : 'svadmin-u-2eba0d65d059'
|
|
320
1226
|
)}
|
|
1227
|
+
style:font-weight={cellFormat(cellKey).bold === true ? '700' : '400'}
|
|
1228
|
+
style:font-style={cellFormat(cellKey).italic === true ? 'italic' : 'normal'}
|
|
1229
|
+
style:text-align={['left', 'center', 'right'].includes(cellFormat(cellKey).align ?? '')
|
|
1230
|
+
? cellFormat(cellKey).align : typeof computedCell === 'number' ? 'right' : 'left'}
|
|
321
1231
|
onfocus={() => selectCell(cellKey)}
|
|
322
1232
|
oninput={(e) => updateCellValue(cellKey, e.currentTarget.value)}
|
|
1233
|
+
onkeydown={handleCellKeydown}
|
|
1234
|
+
oncopy={handleCopy}
|
|
1235
|
+
onpaste={handlePaste}
|
|
323
1236
|
/>
|
|
324
1237
|
</td>
|
|
325
1238
|
{/each}
|
|
@@ -329,11 +1242,9 @@
|
|
|
329
1242
|
</table>
|
|
330
1243
|
</div>
|
|
331
1244
|
{/if}
|
|
332
|
-
|
|
333
|
-
<!-- Sheet Tabs Footer -->
|
|
334
1245
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-77a2a20e90d4 svadmin-u-6b7d6e21ccbd svadmin-u-b950dda299d3 svadmin-u-05faf5c801ff">
|
|
335
1246
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421 svadmin-u-1384f66f41d0">
|
|
336
|
-
{#each sheets as sheet (sheet.id)}
|
|
1247
|
+
{#each (validWorkbook ? sheets : []) as sheet (sheet.id)}
|
|
337
1248
|
{@const isActive = sheet.id === activeSheetId}
|
|
338
1249
|
<button
|
|
339
1250
|
type="button"
|
|
@@ -346,6 +1257,8 @@
|
|
|
346
1257
|
onclick={() => {
|
|
347
1258
|
activeSheetId = sheet.id;
|
|
348
1259
|
selectedCell = 'A1';
|
|
1260
|
+
selectedRangeEnd = 'A1';
|
|
1261
|
+
selectionError = false;
|
|
349
1262
|
formulaInput = sheet.cells['A1'] ?? '';
|
|
350
1263
|
}}
|
|
351
1264
|
>
|
|
@@ -354,7 +1267,7 @@
|
|
|
354
1267
|
{/each}
|
|
355
1268
|
|
|
356
1269
|
{#if !readonly}
|
|
357
|
-
<Button variant="ghost" size="sm" class="svadmin-u-f6fe902450dc svadmin-u-7ec10f86d9b1 svadmin-u-8a539c7fe216 svadmin-u-bfa603190748" onclick={addSheet} title=
|
|
1270
|
+
<Button variant="ghost" size="sm" disabled={!validWorkbook || sheets.length >= limits.maxSheets} class="svadmin-u-f6fe902450dc svadmin-u-7ec10f86d9b1 svadmin-u-8a539c7fe216 svadmin-u-bfa603190748" onclick={addSheet} aria-label={i18n.t('spreadsheet.newSheet')} title={i18n.t('spreadsheet.newSheet')}>
|
|
358
1271
|
<Plus class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
|
|
359
1272
|
</Button>
|
|
360
1273
|
{/if}
|