@svadmin/ui 0.73.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 +53 -28
- package/dist/app.css +3733 -4119
- package/dist/app.theme.css +3733 -4119
- 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 +33 -3
- package/dist/components/AutoForm.svelte.d.ts +1 -0
- package/dist/components/AutoTable.svelte +610 -47
- package/dist/components/AutoTable.svelte.d.ts +17 -1
- package/dist/components/BoundRecordDetailDrawer.svelte +9 -0
- package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +2 -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 +114 -27
- package/dist/components/ComboboxField.svelte.d.ts +1 -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 +85 -5
- package/dist/components/DateRangeInput.svelte.d.ts +10 -0
- package/dist/components/DateRangePicker.svelte +10 -0
- package/dist/components/DateRangePicker.svelte.d.ts +6 -0
- package/dist/components/DateTimeInput.svelte +74 -5
- package/dist/components/DateTimeInput.svelte.d.ts +8 -0
- package/dist/components/DateTimePicker.svelte +10 -0
- package/dist/components/DateTimePicker.svelte.d.ts +6 -0
- package/dist/components/DevTools.svelte +34 -26
- package/dist/components/DevframePanel.svelte +123 -0
- package/dist/components/DevframePanel.svelte.d.ts +3 -0
- package/dist/components/ErrorSummary.svelte +52 -0
- package/dist/components/ErrorSummary.svelte.d.ts +14 -0
- package/dist/components/FieldRenderer.svelte +82 -81
- package/dist/components/FileUpload.svelte +214 -52
- package/dist/components/FileUpload.svelte.d.ts +11 -0
- package/dist/components/FilterBuilder.svelte +88 -66
- 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 +339 -17
- package/dist/components/ImportWizard.svelte.d.ts +11 -0
- package/dist/components/JsonSchemaForm.svelte +120 -52
- package/dist/components/JsonSchemaForm.svelte.d.ts +7 -1
- package/dist/components/KanbanBoard.svelte +195 -40
- package/dist/components/KanbanBoard.svelte.d.ts +9 -2
- package/dist/components/Layout.svelte +4 -2
- 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/ListPage.svelte +5 -1
- package/dist/components/ListPage.svelte.d.ts +3 -0
- package/dist/components/LoginPage.svelte +1 -1
- 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/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/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 +4 -2
- package/dist/components/RecordDetailDrawer.svelte.d.ts +2 -0
- package/dist/components/RelationPicker.svelte +341 -0
- package/dist/components/RelationPicker.svelte.d.ts +26 -0
- package/dist/components/ResourceOperationsPage.svelte +18 -18
- package/dist/components/ResourceOperationsPage.svelte.d.ts +2 -2
- 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/ShowPage.svelte +18 -3
- package/dist/components/ShowPage.svelte.d.ts +3 -0
- package/dist/components/Sidebar.svelte +2 -2
- package/dist/components/Slider.svelte +100 -0
- package/dist/components/Slider.svelte.d.ts +19 -0
- package/dist/components/SpreadsheetView.svelte +1213 -95
- package/dist/components/SpreadsheetView.svelte.d.ts +19 -0
- package/dist/components/TagsInput.svelte +178 -0
- package/dist/components/TagsInput.svelte.d.ts +17 -0
- package/dist/components/TimePicker.svelte +10 -0
- package/dist/components/TimePicker.svelte.d.ts +6 -0
- package/dist/components/TreeSelect.svelte +267 -53
- package/dist/components/TreeSelect.svelte.d.ts +3 -0
- 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/buttons/ExportButton.svelte +56 -12
- package/dist/components/buttons/ExportButton.svelte.d.ts +11 -0
- package/dist/components/content/ContentPageHeader.svelte +1 -1
- package/dist/components/content/ContentPageShell.svelte +1 -1
- package/dist/components/content/MetricBlock.svelte +1 -1
- package/dist/components/content/PageToolbar.svelte +1 -1
- package/dist/components/content/ReferenceNotice.svelte +5 -0
- package/dist/components/content/ReferenceNotice.svelte.d.ts +18 -0
- package/dist/components/content/SectionHeader.svelte +1 -1
- package/dist/components/content/SettingsFieldRow.svelte +1 -1
- package/dist/components/content/SettingsGroup.svelte +1 -1
- package/dist/components/content/StatusBadge.svelte +1 -1
- package/dist/components/content/WorkspaceLayout.svelte +1 -1
- package/dist/components/export-reference.d.ts +2 -0
- package/dist/components/export-reference.js +9 -0
- package/dist/components/fieldComponentMap.js +2 -5
- package/dist/components/fields/DateField.svelte +12 -13
- package/dist/components/fields/date-display.d.ts +6 -0
- package/dist/components/fields/date-display.js +24 -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/saved-list-views.d.ts +74 -1
- package/dist/components/saved-list-views.js +191 -3
- 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 -3
- 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 -5
- 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 +7 -2
- 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 -2
- package/dist/components.css +33 -2
- package/dist/date-input-policy.d.ts +4 -0
- package/dist/date-input-policy.js +35 -0
- package/dist/design-system-components.d.ts +100 -0
- package/dist/design-system-components.js +101 -0
- package/dist/design-system.js +1 -1
- package/dist/devtools-bridge.d.ts +80 -0
- package/dist/devtools-bridge.js +92 -0
- package/dist/index.d.ts +37 -2
- package/dist/index.js +23 -2
- package/dist/recipes.d.ts +960 -4
- package/dist/recipes.js +264 -4
- package/dist/tree-table-model.d.ts +24 -0
- package/dist/tree-table-model.js +111 -0
- package/package.json +22 -10
- package/dist/styled-system/css/conditions.js +0 -36
- package/dist/styled-system/css/css.d.ts +0 -22
- package/dist/styled-system/css/css.js +0 -45
- package/dist/styled-system/css/cva.d.ts +0 -6
- package/dist/styled-system/css/cva.js +0 -87
- package/dist/styled-system/css/cx.d.ts +0 -5
- package/dist/styled-system/css/cx.js +0 -15
- package/dist/styled-system/css/index.d.ts +0 -5
- package/dist/styled-system/css/index.js +0 -4
- package/dist/styled-system/css/sva.d.ts +0 -4
- package/dist/styled-system/css/sva.js +0 -46
- package/dist/styled-system/helpers.js +0 -316
- package/dist/styled-system/patterns/aspect-ratio.d.ts +0 -20
- package/dist/styled-system/patterns/aspect-ratio.js +0 -38
- package/dist/styled-system/patterns/bleed.d.ts +0 -21
- package/dist/styled-system/patterns/bleed.js +0 -24
- package/dist/styled-system/patterns/box.d.ts +0 -20
- package/dist/styled-system/patterns/box.js +0 -15
- package/dist/styled-system/patterns/center.d.ts +0 -20
- package/dist/styled-system/patterns/center.js +0 -21
- package/dist/styled-system/patterns/circle.d.ts +0 -20
- package/dist/styled-system/patterns/circle.js +0 -25
- package/dist/styled-system/patterns/container.d.ts +0 -20
- package/dist/styled-system/patterns/container.js +0 -21
- package/dist/styled-system/patterns/cq.d.ts +0 -21
- package/dist/styled-system/patterns/cq.js +0 -21
- package/dist/styled-system/patterns/divider.d.ts +0 -22
- package/dist/styled-system/patterns/divider.js +0 -25
- package/dist/styled-system/patterns/flex.d.ts +0 -26
- package/dist/styled-system/patterns/flex.js +0 -26
- package/dist/styled-system/patterns/float.d.ts +0 -23
- package/dist/styled-system/patterns/float.js +0 -52
- package/dist/styled-system/patterns/grid-item.d.ts +0 -25
- package/dist/styled-system/patterns/grid-item.js +0 -25
- package/dist/styled-system/patterns/grid.d.ts +0 -24
- package/dist/styled-system/patterns/grid.js +0 -27
- package/dist/styled-system/patterns/hstack.d.ts +0 -21
- package/dist/styled-system/patterns/hstack.js +0 -24
- package/dist/styled-system/patterns/index.d.ts +0 -21
- package/dist/styled-system/patterns/index.js +0 -20
- package/dist/styled-system/patterns/link-overlay.d.ts +0 -20
- package/dist/styled-system/patterns/link-overlay.js +0 -24
- package/dist/styled-system/patterns/spacer.d.ts +0 -20
- package/dist/styled-system/patterns/spacer.js +0 -25
- package/dist/styled-system/patterns/square.d.ts +0 -20
- package/dist/styled-system/patterns/square.js +0 -24
- package/dist/styled-system/patterns/stack.d.ts +0 -23
- package/dist/styled-system/patterns/stack.js +0 -24
- package/dist/styled-system/patterns/visually-hidden.d.ts +0 -20
- package/dist/styled-system/patterns/visually-hidden.js +0 -18
- package/dist/styled-system/patterns/vstack.d.ts +0 -21
- package/dist/styled-system/patterns/vstack.js +0 -24
- package/dist/styled-system/patterns/wrap.d.ts +0 -24
- package/dist/styled-system/patterns/wrap.js +0 -25
- package/dist/styled-system/recipes/content-header.d.ts +0 -31
- package/dist/styled-system/recipes/content-header.js +0 -65
- package/dist/styled-system/recipes/content-page.d.ts +0 -34
- package/dist/styled-system/recipes/content-page.js +0 -43
- package/dist/styled-system/recipes/create-recipe.js +0 -82
- package/dist/styled-system/recipes/index.d.ts +0 -17
- package/dist/styled-system/recipes/index.js +0 -16
- package/dist/styled-system/recipes/metric-block.d.ts +0 -34
- package/dist/styled-system/recipes/metric-block.js +0 -76
- package/dist/styled-system/recipes/product-list.d.ts +0 -31
- package/dist/styled-system/recipes/product-list.js +0 -73
- package/dist/styled-system/recipes/product-section.d.ts +0 -31
- package/dist/styled-system/recipes/product-section.js +0 -49
- package/dist/styled-system/recipes/product-settings-row.d.ts +0 -34
- package/dist/styled-system/recipes/product-settings-row.js +0 -58
- package/dist/styled-system/recipes/product-settings.d.ts +0 -31
- package/dist/styled-system/recipes/product-settings.js +0 -57
- package/dist/styled-system/recipes/product-status.d.ts +0 -34
- package/dist/styled-system/recipes/product-status.js +0 -45
- package/dist/styled-system/recipes/product-toolbar.d.ts +0 -31
- package/dist/styled-system/recipes/product-toolbar.js +0 -41
- package/dist/styled-system/recipes/product-workspace.d.ts +0 -34
- package/dist/styled-system/recipes/product-workspace.js +0 -58
- package/dist/styled-system/recipes/surface-metric.d.ts +0 -38
- package/dist/styled-system/recipes/surface-metric.js +0 -63
- package/dist/styled-system/recipes/surface-table.d.ts +0 -34
- package/dist/styled-system/recipes/surface-table.js +0 -62
- package/dist/styled-system/recipes/ui-badge.d.ts +0 -31
- package/dist/styled-system/recipes/ui-badge.js +0 -38
- package/dist/styled-system/recipes/ui-button.d.ts +0 -32
- package/dist/styled-system/recipes/ui-button.js +0 -43
- package/dist/styled-system/recipes/ui-input.d.ts +0 -31
- package/dist/styled-system/recipes/ui-input.js +0 -49
- package/dist/styled-system/recipes/ui-textarea.d.ts +0 -31
- package/dist/styled-system/recipes/ui-textarea.js +0 -24
- package/dist/styled-system/styles.css +0 -1257
- package/dist/styled-system/tokens/index.d.ts +0 -9
- package/dist/styled-system/tokens/index.js +0 -268
- package/dist/styled-system/tokens/tokens.d.ts +0 -36
- package/dist/styled-system/types/composition.d.ts +0 -227
- package/dist/styled-system/types/conditions.d.ts +0 -236
- package/dist/styled-system/types/csstype.d.ts +0 -22570
- package/dist/styled-system/types/index.d.ts +0 -6
- package/dist/styled-system/types/parts.d.ts +0 -8
- package/dist/styled-system/types/pattern.d.ts +0 -78
- package/dist/styled-system/types/prop-type.d.ts +0 -223
- package/dist/styled-system/types/recipe.d.ts +0 -181
- package/dist/styled-system/types/selectors.d.ts +0 -59
- package/dist/styled-system/types/static-css.d.ts +0 -56
- package/dist/styled-system/types/style-props.d.ts +0 -8088
- package/dist/styled-system/types/system-types.d.ts +0 -151
- package/dist/styles/recipes.css +0 -1262
|
@@ -1,143 +1,1239 @@
|
|
|
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';
|
|
5
|
-
import {
|
|
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();
|
|
6
10
|
|
|
7
11
|
export interface SheetData {
|
|
8
12
|
id: string;
|
|
9
13
|
name: string;
|
|
10
14
|
rows: number;
|
|
11
15
|
cols: number;
|
|
12
|
-
cells: Record<string, string>;
|
|
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;
|
|
13
30
|
}
|
|
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
58
|
let {
|
|
23
|
-
sheets = $bindable([
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
59
|
+
sheets = $bindable([
|
|
60
|
+
{
|
|
61
|
+
id: 'sheet1',
|
|
62
|
+
name: 'Financial Summary',
|
|
63
|
+
rows: 8,
|
|
64
|
+
cols: 6,
|
|
65
|
+
cells: {
|
|
66
|
+
A1: 'Category',
|
|
67
|
+
B1: 'Q1',
|
|
68
|
+
C1: 'Q2',
|
|
69
|
+
D1: 'Q3',
|
|
70
|
+
E1: 'Q4',
|
|
71
|
+
F1: 'Total',
|
|
72
|
+
A2: 'Revenue',
|
|
73
|
+
B2: '12000',
|
|
74
|
+
C2: '15000',
|
|
75
|
+
D2: '18000',
|
|
76
|
+
E2: '21000',
|
|
77
|
+
F2: '=SUM(B2:E2)',
|
|
78
|
+
A3: 'Cost',
|
|
79
|
+
B3: '4000',
|
|
80
|
+
C3: '5000',
|
|
81
|
+
D3: '6000',
|
|
82
|
+
E3: '7000',
|
|
83
|
+
F3: '=SUM(B3:E3)',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
]),
|
|
87
|
+
activeSheetId = $bindable('sheet1'),
|
|
88
|
+
scopeKey,
|
|
89
|
+
readonly = false,
|
|
90
|
+
onchange,
|
|
91
|
+
onexport,
|
|
92
|
+
onworkbookexport,
|
|
93
|
+
onxlsxexport,
|
|
94
|
+
xlsxexporter,
|
|
95
|
+
xlsximporter,
|
|
96
|
+
limits: limitOverrides = {},
|
|
97
|
+
class: className = '',
|
|
29
98
|
}: Props = $props();
|
|
30
|
-
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
|
|
99
|
+
|
|
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;
|
|
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;
|
|
124
|
+
|
|
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
|
+
}
|
|
134
|
+
|
|
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
|
+
}
|
|
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);
|
|
34
260
|
|
|
35
261
|
function getColName(index: number): string {
|
|
36
262
|
let result = '', current = index;
|
|
37
263
|
while (current >= 0) { result = String.fromCharCode(current % 26 + 65) + result; current = Math.floor(current / 26) - 1; }
|
|
38
264
|
return result;
|
|
39
265
|
}
|
|
266
|
+
|
|
267
|
+
function colNameToIndex(col: string): number {
|
|
268
|
+
let idx = 0;
|
|
269
|
+
for (let i = 0; i < col.length; i++) {
|
|
270
|
+
idx = idx * 26 + (col.charCodeAt(i) - 64);
|
|
271
|
+
if (!Number.isSafeInteger(idx)) throw new Error('limit');
|
|
272
|
+
}
|
|
273
|
+
return idx - 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function parseCellKey(key: string): { col: string; row: number } | null {
|
|
277
|
+
const match = key.match(/^([A-Z]+)([1-9]\d*)$/);
|
|
278
|
+
if (!match || !match[1] || !match[2]) return null;
|
|
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');
|
|
393
|
+
}
|
|
394
|
+
return computed;
|
|
395
|
+
}
|
|
396
|
+
consumeStep(raw.length);
|
|
397
|
+
const result = Number(raw);
|
|
398
|
+
return Number.isFinite(result) ? result : 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
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;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function readPrimary(): number {
|
|
439
|
+
consumeStep();
|
|
440
|
+
skipSpace();
|
|
441
|
+
state.depth += 1;
|
|
442
|
+
try {
|
|
443
|
+
if (state.depth > limits.maxFormulaDepth) throw new Error('limit');
|
|
444
|
+
return readAtom();
|
|
445
|
+
} finally {
|
|
446
|
+
state.depth -= 1;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
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
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
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);
|
|
549
|
+
}
|
|
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
|
+
|
|
40
579
|
function getRenderedValue(key: string): string {
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function selectCell(key: string) {
|
|
614
|
+
selectionError = false;
|
|
615
|
+
selectedRangeEnd = key;
|
|
616
|
+
selectedCell = key;
|
|
617
|
+
if (currentSheet) {
|
|
618
|
+
formulaInput = currentSheet.cells[key] ?? '';
|
|
619
|
+
}
|
|
620
|
+
}
|
|
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
|
+
|
|
839
|
+
function updateCellValue(key: string, val: string) {
|
|
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;
|
|
844
|
+
const nextSheets = sheets.map((s) => {
|
|
845
|
+
if (s.id === currentSheet.id) {
|
|
846
|
+
return {
|
|
847
|
+
...s,
|
|
848
|
+
cells: {
|
|
849
|
+
...s.cells,
|
|
850
|
+
[key]: val,
|
|
851
|
+
},
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
return s;
|
|
855
|
+
});
|
|
856
|
+
commitSheets(nextSheets);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function addRow() {
|
|
860
|
+
if (readonly || !currentSheet || !canAddRow) return;
|
|
861
|
+
const next = sheets.map((s) => (s.id === currentSheet.id ? { ...s, rows: s.rows + 1 } : s));
|
|
862
|
+
commitSheets(next);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function addCol() {
|
|
866
|
+
if (readonly || !currentSheet || !canAddCol) return;
|
|
867
|
+
const next = sheets.map((s) => (s.id === currentSheet.id ? { ...s, cols: s.cols + 1 } : s));
|
|
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);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function exportCsv() {
|
|
903
|
+
if (!currentSheet || !canRender) return;
|
|
904
|
+
const records: Record<string, unknown>[] = [];
|
|
905
|
+
for (let r = 1; r <= currentSheet.rows; r++) {
|
|
906
|
+
const row: Record<string, unknown> = {};
|
|
907
|
+
for (let c = 0; c < currentSheet.cols; c++) {
|
|
908
|
+
const k = `${getColName(c)}${r}`;
|
|
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;
|
|
913
|
+
}
|
|
914
|
+
records.push(row);
|
|
915
|
+
}
|
|
916
|
+
// 公共编码器包含表头;工作表导出只保留原有数据行。
|
|
917
|
+
const csvContent = toCsv(records).split('\n').slice(1).join('\n');
|
|
918
|
+
if (onexport) {
|
|
919
|
+
onexport(csvContent);
|
|
920
|
+
} else {
|
|
921
|
+
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
922
|
+
const a = document.createElement('a');
|
|
923
|
+
const url = URL.createObjectURL(blob);
|
|
924
|
+
a.href = url;
|
|
925
|
+
a.download = `${currentSheet.name}_${Date.now()}.csv`;
|
|
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';
|
|
84
945
|
try {
|
|
85
|
-
const anchor = document.createElement('a');
|
|
86
|
-
anchor.href = url;
|
|
87
|
-
anchor.download = `${sheet.name}_${Date.now()}.csv`;
|
|
88
946
|
anchor.click();
|
|
89
|
-
} finally {
|
|
947
|
+
} finally {
|
|
948
|
+
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
|
949
|
+
}
|
|
90
950
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
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
|
+
|
|
1045
|
+
function addSheet() {
|
|
1046
|
+
if (readonly || !validWorkbook || sheets.length >= limits.maxSheets) return;
|
|
1047
|
+
const id = `sheet_${crypto.randomUUID()}`;
|
|
1048
|
+
const newSheet: SheetData = {
|
|
1049
|
+
id,
|
|
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)))),
|
|
1053
|
+
cells: {},
|
|
1054
|
+
};
|
|
1055
|
+
const nextSheets = [...sheets, newSheet];
|
|
1056
|
+
commitSheets(nextSheets, id);
|
|
98
1057
|
}
|
|
99
1058
|
</script>
|
|
100
1059
|
|
|
101
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)}>
|
|
102
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">
|
|
103
1062
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
|
|
104
|
-
<FileSpreadsheet class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-20aaf08a7ed1 svadmin-u-012fbd121f37"
|
|
105
|
-
<span class="svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{currentSheet?.name ?? '
|
|
1063
|
+
<FileSpreadsheet class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-20aaf08a7ed1 svadmin-u-012fbd121f37" />
|
|
1064
|
+
<span class="svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{currentSheet?.name ?? i18n.t('spreadsheet.title')}</span>
|
|
106
1065
|
</div>
|
|
107
|
-
|
|
1066
|
+
|
|
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} />
|
|
108
1071
|
{#if !readonly}
|
|
109
|
-
<Button
|
|
110
|
-
|
|
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}>
|
|
1117
|
+
<Plus class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
|
|
1118
|
+
{i18n.t('spreadsheet.row')}
|
|
1119
|
+
</Button>
|
|
1120
|
+
<Button variant="outline" size="sm" disabled={!canAddCol} class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421" onclick={addCol}>
|
|
1121
|
+
<Plus class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
|
|
1122
|
+
{i18n.t('spreadsheet.column')}
|
|
1123
|
+
</Button>
|
|
1124
|
+
{/if}
|
|
1125
|
+
|
|
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}>
|
|
1131
|
+
<Download class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
|
|
1132
|
+
{i18n.t('spreadsheet.exportWorkbook')}
|
|
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>
|
|
111
1140
|
{/if}
|
|
112
|
-
<Button type="button" size="sm" class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421" onclick={exportCsv}><Download size={14} aria-hidden="true" />Export CSV</Button>
|
|
113
1141
|
</div>
|
|
114
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}
|
|
1152
|
+
|
|
1153
|
+
<!-- Formula Bar -->
|
|
115
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">
|
|
116
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>
|
|
117
1156
|
<span class="svadmin-u-bfa603190748 svadmin-u-359090c2d529">fx</span>
|
|
118
|
-
<input
|
|
1157
|
+
<input
|
|
1158
|
+
type="text"
|
|
1159
|
+
bind:value={formulaInput}
|
|
1160
|
+
{readonly}
|
|
1161
|
+
disabled={!canRender}
|
|
1162
|
+
maxlength={limits.maxCellLength}
|
|
1163
|
+
aria-label={i18n.t('spreadsheet.formula')}
|
|
119
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"
|
|
120
|
-
oninput={(
|
|
121
|
-
onkeydown={(
|
|
1165
|
+
oninput={(e) => updateCellValue(selectedCell, e.currentTarget.value)}
|
|
1166
|
+
onkeydown={(e) => { if (e.key === 'Enter') updateCellValue(selectedCell, e.currentTarget.value); }}
|
|
1167
|
+
/>
|
|
122
1168
|
</div>
|
|
123
|
-
|
|
1169
|
+
|
|
1170
|
+
<!-- Spreadsheet Grid -->
|
|
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}
|
|
124
1179
|
<div class="svadmin-u-73fc3fb18ceb svadmin-u-b63b7d8eef5d svadmin-u-ca6bcd4b6f3f svadmin-u-05faf5c801ff svadmin-u-5f22e64f2282">
|
|
125
|
-
<table
|
|
126
|
-
<thead class="svadmin-u-706701550477 svadmin-u-3e0fd166d494 svadmin-u-2167406b24d7 svadmin-u-236812d64c82 svadmin-u-bfa603190748"
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
1180
|
+
<table class="svadmin-u-6da6a3c3f741 svadmin-u-4583f90cd9bd svadmin-u-2eba0d65d059 svadmin-u-0e65706bcccd">
|
|
1181
|
+
<thead class="svadmin-u-706701550477 svadmin-u-3e0fd166d494 svadmin-u-2167406b24d7 svadmin-u-236812d64c82 svadmin-u-bfa603190748">
|
|
1182
|
+
<tr>
|
|
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>
|
|
1184
|
+
{#each Array.from({ length: gridCols }) as _, c (c)}
|
|
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">
|
|
1186
|
+
{getColName(c)}
|
|
1187
|
+
</th>
|
|
1188
|
+
{/each}
|
|
1189
|
+
</tr>
|
|
1190
|
+
</thead>
|
|
130
1191
|
<tbody class="svadmin-u-fa6acbf81d74 svadmin-u-08b5607c7258">
|
|
131
|
-
{#each Array.from({ length:
|
|
1192
|
+
{#each Array.from({ length: gridRows }) as _, r (r)}
|
|
1193
|
+
{@const rowNum = r + 1}
|
|
132
1194
|
<tr class="svadmin-u-9fc5d875f5ca">
|
|
133
|
-
<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">
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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">
|
|
1196
|
+
{rowNum}
|
|
1197
|
+
</td>
|
|
1198
|
+
{#each Array.from({ length: gridCols }) as _, c (c)}
|
|
1199
|
+
{@const cellKey = `${getColName(c)}${rowNum}`}
|
|
1200
|
+
{@const computedCell = evaluatedCells.get(cellKey)}
|
|
1201
|
+
{@const errorCell = typeof computedCell === 'string' ? computedCell : ''}
|
|
1202
|
+
{@const isSelected = isCellSelected(cellKey)}
|
|
1203
|
+
<td
|
|
1204
|
+
data-selected={isSelected || undefined}
|
|
1205
|
+
class={cn(
|
|
1206
|
+
'svadmin-u-8a539c7fe216 svadmin-u-5ceb636bd9f3 svadmin-u-6ee2d41e2d2d svadmin-u-d89972fe17d6 svadmin-u-34516836730d',
|
|
1207
|
+
isSelected ? 'svadmin-u-16b1efa5875e svadmin-u-fd1dda07320a svadmin-u-236812d64c82 svadmin-u-989c466fdbe7' : ''
|
|
1208
|
+
)}
|
|
1209
|
+
onmousedown={(event) => {
|
|
1210
|
+
if (event.shiftKey) event.preventDefault();
|
|
1211
|
+
}}
|
|
1212
|
+
onclick={(event) => selectRange(cellKey, event.shiftKey)}
|
|
1213
|
+
>
|
|
1214
|
+
<input
|
|
1215
|
+
type="text"
|
|
1216
|
+
{readonly}
|
|
1217
|
+
aria-label={cellKey}
|
|
1218
|
+
maxlength={limits.maxCellLength}
|
|
1219
|
+
value={selectedCell === cellKey && !readonly
|
|
1220
|
+
&& !errorCell.startsWith('#')
|
|
1221
|
+
? (cellSnapshot?.[cellKey] ?? '')
|
|
1222
|
+
: getRenderedValue(cellKey)}
|
|
1223
|
+
class={cn(
|
|
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',
|
|
1225
|
+
typeof computedCell === 'number' ? 'svadmin-u-308fc069e46e' : 'svadmin-u-2eba0d65d059'
|
|
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'}
|
|
1231
|
+
onfocus={() => selectCell(cellKey)}
|
|
1232
|
+
oninput={(e) => updateCellValue(cellKey, e.currentTarget.value)}
|
|
1233
|
+
onkeydown={handleCellKeydown}
|
|
1234
|
+
oncopy={handleCopy}
|
|
1235
|
+
onpaste={handlePaste}
|
|
1236
|
+
/>
|
|
141
1237
|
</td>
|
|
142
1238
|
{/each}
|
|
143
1239
|
</tr>
|
|
@@ -148,11 +1244,33 @@
|
|
|
148
1244
|
{/if}
|
|
149
1245
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-77a2a20e90d4 svadmin-u-6b7d6e21ccbd svadmin-u-b950dda299d3 svadmin-u-05faf5c801ff">
|
|
150
1246
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421 svadmin-u-1384f66f41d0">
|
|
151
|
-
{#each sheets as sheet (sheet.id)}
|
|
152
|
-
|
|
153
|
-
|
|
1247
|
+
{#each (validWorkbook ? sheets : []) as sheet (sheet.id)}
|
|
1248
|
+
{@const isActive = sheet.id === activeSheetId}
|
|
1249
|
+
<button
|
|
1250
|
+
type="button"
|
|
1251
|
+
class={cn(
|
|
1252
|
+
'svadmin-u-0e17f2bd9074 svadmin-u-660d2effb880 svadmin-u-421ac2be5045 svadmin-u-359090c2d529 svadmin-u-2689f3958069 svadmin-u-ceb69a6b0e5f svadmin-u-34516836730d svadmin-u-ca6bcd4b6f3f',
|
|
1253
|
+
isActive
|
|
1254
|
+
? 'svadmin-u-75b1bec3ea0e svadmin-u-30ca335ae9c2 svadmin-u-6cbc84dd9e1a svadmin-u-cef5b893cf23'
|
|
1255
|
+
: 'svadmin-u-cd0ad9a56558 svadmin-u-bfa603190748 svadmin-u-05faf5c801ff svadmin-u-ea7b2e9e070e svadmin-u-68646cdcc246'
|
|
1256
|
+
)}
|
|
1257
|
+
onclick={() => {
|
|
1258
|
+
activeSheetId = sheet.id;
|
|
1259
|
+
selectedCell = 'A1';
|
|
1260
|
+
selectedRangeEnd = 'A1';
|
|
1261
|
+
selectionError = false;
|
|
1262
|
+
formulaInput = sheet.cells['A1'] ?? '';
|
|
1263
|
+
}}
|
|
1264
|
+
>
|
|
1265
|
+
{sheet.name}
|
|
1266
|
+
</button>
|
|
154
1267
|
{/each}
|
|
155
|
-
|
|
1268
|
+
|
|
1269
|
+
{#if !readonly}
|
|
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')}>
|
|
1271
|
+
<Plus class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
|
|
1272
|
+
</Button>
|
|
1273
|
+
{/if}
|
|
156
1274
|
</div>
|
|
157
1275
|
</div>
|
|
158
1276
|
</div>
|