@casualoffice/sheets 0.9.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/LICENSE +200 -0
- package/dist/embed/embed-runtime.js +537 -0
- package/dist/embed/embed.html +29 -0
- package/dist/embed/parser.worker.js +48474 -0
- package/dist/embed.cjs +225 -0
- package/dist/embed.cjs.map +1 -0
- package/dist/embed.d.cts +100 -0
- package/dist/embed.d.ts +100 -0
- package/dist/embed.js +204 -0
- package/dist/embed.js.map +1 -0
- package/dist/index.cjs +1549 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1530 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.worker.cjs +48469 -0
- package/dist/parser.worker.cjs.map +1 -0
- package/dist/parser.worker.js +48474 -0
- package/dist/parser.worker.js.map +1 -0
- package/dist/protocol--KyBQUjU.d.cts +171 -0
- package/dist/protocol-cEzy7S0i.d.ts +171 -0
- package/dist/sheets.cjs +677 -0
- package/dist/sheets.cjs.map +1 -0
- package/dist/sheets.d.cts +177 -0
- package/dist/sheets.d.ts +177 -0
- package/dist/sheets.js +658 -0
- package/dist/sheets.js.map +1 -0
- package/dist/signing.cjs +706 -0
- package/dist/signing.cjs.map +1 -0
- package/dist/signing.d.cts +141 -0
- package/dist/signing.d.ts +141 -0
- package/dist/signing.js +683 -0
- package/dist/signing.js.map +1 -0
- package/dist/styles.cjs +10 -0
- package/dist/styles.cjs.map +1 -0
- package/dist/styles.d.cts +2 -0
- package/dist/styles.d.ts +2 -0
- package/dist/styles.js +8 -0
- package/dist/styles.js.map +1 -0
- package/dist/types-s_O0u6Cg.d.cts +90 -0
- package/dist/types-s_O0u6Cg.d.ts +90 -0
- package/dist/univer.cjs +220 -0
- package/dist/univer.cjs.map +1 -0
- package/dist/univer.d.cts +60 -0
- package/dist/univer.d.ts +60 -0
- package/dist/univer.js +187 -0
- package/dist/univer.js.map +1 -0
- package/dist/xlsx.cjs +3388 -0
- package/dist/xlsx.cjs.map +1 -0
- package/dist/xlsx.d.cts +383 -0
- package/dist/xlsx.d.ts +383 -0
- package/dist/xlsx.js +3383 -0
- package/dist/xlsx.js.map +1 -0
- package/package.json +293 -0
- package/src/embed/EmbedHostTransport.ts +226 -0
- package/src/embed/EmbedTransport.ts +323 -0
- package/src/embed/EmbedTransport.unit.test.ts +161 -0
- package/src/embed/index.ts +40 -0
- package/src/embed/protocol.ts +258 -0
- package/src/embed-runtime/embed.html +29 -0
- package/src/embed-runtime/index.tsx +440 -0
- package/src/index.ts +16 -0
- package/src/sheets/CasualSheets.tsx +319 -0
- package/src/sheets/CasualSheetsIframe.tsx +220 -0
- package/src/sheets/api.ts +108 -0
- package/src/sheets/index.ts +11 -0
- package/src/signing/SigningPane.tsx +374 -0
- package/src/signing/SigningProvider.tsx +126 -0
- package/src/signing/captures.tsx +316 -0
- package/src/signing/controller.ts +151 -0
- package/src/signing/controller.unit.test.ts +133 -0
- package/src/signing/index.ts +44 -0
- package/src/signing/types.ts +89 -0
- package/src/styles.ts +16 -0
- package/src/univer/index.ts +17 -0
- package/src/univer/lazy-plugins.ts +280 -0
- package/src/xlsx/_perf.ts +14 -0
- package/src/xlsx/_snapshot-constants.ts +15 -0
- package/src/xlsx/comments-resource.ts +209 -0
- package/src/xlsx/constants.ts +9 -0
- package/src/xlsx/data-validation-resource.ts +219 -0
- package/src/xlsx/import.ts +35 -0
- package/src/xlsx/index.ts +40 -0
- package/src/xlsx/page-setup-resource.ts +205 -0
- package/src/xlsx/parse-impl.ts +418 -0
- package/src/xlsx/parse-in-worker.ts +82 -0
- package/src/xlsx/parser.worker.ts +39 -0
- package/src/xlsx/passthrough-resource.ts +175 -0
- package/src/xlsx/pivot-passthrough.ts +359 -0
- package/src/xlsx/style-mapping.ts +171 -0
- package/src/xlsx/tables-resource.ts +211 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signing types — mirror the iframe-protocol envelopes from
|
|
3
|
+
* `docs/internal/13-iframe-protocol.md` so the SDK and iframe
|
|
4
|
+
* deliveries can hand the same payloads back and forth.
|
|
5
|
+
*
|
|
6
|
+
* Uniform across `app: 'docs' | 'sheet'`. Only the `anchor`
|
|
7
|
+
* discriminator changes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type SignatureMethod = 'drawn' | 'typed' | 'uploaded';
|
|
11
|
+
|
|
12
|
+
export type SignatureMode = 'sequential' | 'concurrent';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Doc-anchored field — paragraph id from the editor's `w14:paraId`,
|
|
16
|
+
* with an optional sub-paragraph search for placing the signature
|
|
17
|
+
* inside a phrase rather than a whole block.
|
|
18
|
+
*/
|
|
19
|
+
export interface DocAnchor {
|
|
20
|
+
kind: 'doc';
|
|
21
|
+
paraId: string;
|
|
22
|
+
search?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Sheet-anchored field — `sheet` name + A1-style `cell` ref. */
|
|
26
|
+
export interface SheetAnchor {
|
|
27
|
+
kind: 'sheet';
|
|
28
|
+
sheet: string;
|
|
29
|
+
cell: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SignatureAnchor = DocAnchor | SheetAnchor;
|
|
33
|
+
|
|
34
|
+
export interface SignatureField {
|
|
35
|
+
/** Per-field id supplied by the host; echoed on every progress event. */
|
|
36
|
+
fieldId: string;
|
|
37
|
+
/** Label rendered next to the field — "Employee signature", etc. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** Required fields must complete before `onComplete` fires. */
|
|
40
|
+
required: boolean;
|
|
41
|
+
/** Where the signature lands in the document. */
|
|
42
|
+
anchor: SignatureAnchor;
|
|
43
|
+
/** Allowed signature methods. */
|
|
44
|
+
methods: SignatureMethod[];
|
|
45
|
+
/** Optional signer identity the host knows about. */
|
|
46
|
+
signer?: { name?: string; email?: string };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Payload emitted when a signer completes one field. */
|
|
50
|
+
export interface SignedFieldPayload {
|
|
51
|
+
fieldId: string;
|
|
52
|
+
method: SignatureMethod;
|
|
53
|
+
/** Raw signature material — PNG for drawn, UTF-8 string-as-bytes for typed, host-attested bytes for uploaded. */
|
|
54
|
+
bytes: ArrayBuffer;
|
|
55
|
+
mime: string;
|
|
56
|
+
/** Client wall-clock at completion; host typically pairs with a server timestamp. */
|
|
57
|
+
signedAt: string;
|
|
58
|
+
/** Optional placement hint a host generating a flat PDF can use. */
|
|
59
|
+
placement?: { page: number; xPct: number; yPct: number };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Payload emitted when all required fields are signed. */
|
|
63
|
+
export interface SignatureCompletePayload {
|
|
64
|
+
fieldIds: string[];
|
|
65
|
+
/** Document bytes WITH stamps applied — note (v1): the editor
|
|
66
|
+
* returns the unmodified document and a `fields` map so the
|
|
67
|
+
* HOST stamps the final bytes. v2 lands editor-side stamping. */
|
|
68
|
+
bytes: ArrayBuffer;
|
|
69
|
+
fields: Record<string, SignedFieldPayload>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type CancelReason = 'signer_cancelled' | 'session_expired' | 'host_aborted';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Configuration the host hands the editor when opening a signing
|
|
76
|
+
* session. Maps 1:1 to the iframe `signature.request` envelope.
|
|
77
|
+
*/
|
|
78
|
+
export interface SigningSessionConfig {
|
|
79
|
+
fields: SignatureField[];
|
|
80
|
+
mode: SignatureMode;
|
|
81
|
+
/** Optional banner the editor renders at the top of the pane. */
|
|
82
|
+
banner?: string;
|
|
83
|
+
/** Fires once per field as the signer completes it. */
|
|
84
|
+
onFieldSigned?: (payload: SignedFieldPayload) => void | Promise<void>;
|
|
85
|
+
/** Fires after every required field is signed. */
|
|
86
|
+
onComplete?: (payload: SignatureCompletePayload) => void | Promise<void>;
|
|
87
|
+
/** Fires when either side aborts the session. */
|
|
88
|
+
onCancel?: (payload: { reason: CancelReason }) => void;
|
|
89
|
+
}
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Univer plugin CSS — side-effect-only imports for the eager plugin
|
|
3
|
+
* set CasualSheets boots. Hosts import this once at app boot:
|
|
4
|
+
*
|
|
5
|
+
* import '@casualoffice/sheets/styles';
|
|
6
|
+
*
|
|
7
|
+
* If the host adds lazy plugins (sort, filter, drawing, comments,
|
|
8
|
+
* conditional formatting, …) it imports the corresponding CSS
|
|
9
|
+
* separately — those plugins ship their own /lib/index.css.
|
|
10
|
+
*/
|
|
11
|
+
import '@univerjs/design/lib/index.css';
|
|
12
|
+
import '@univerjs/ui/lib/index.css';
|
|
13
|
+
import '@univerjs/docs-ui/lib/index.css';
|
|
14
|
+
import '@univerjs/sheets-ui/lib/index.css';
|
|
15
|
+
import '@univerjs/sheets-formula-ui/lib/index.css';
|
|
16
|
+
import '@univerjs/sheets-numfmt-ui/lib/index.css';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal Univer wiring shared by the SDK editor and the host app.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 (Batch 1) of the SDK migration lifts the lazy plugin loader out of
|
|
5
|
+
* `apps/web` so the editor can eventually run entirely from `@casualoffice/sheets`.
|
|
6
|
+
* It's pure Univer/DI code (no React context, FileSource, collab, or routing) with
|
|
7
|
+
* module-level singleton state, so the host and the SDK must resolve this one
|
|
8
|
+
* module instance. Later batches add the facade-coupled helpers (dev-helpers,
|
|
9
|
+
* paste-merge-hook, zoom override) + the editor core. See
|
|
10
|
+
* `docs/SDK_MIGRATION_PIPELINE.md` Phase 1.
|
|
11
|
+
*
|
|
12
|
+
* @internal — not part of the SDK's semver surface; consumers use `<CasualSheets>`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Lazy plugin loader (CF, DV, hyperlink, note, table, thread-comment, drawing,
|
|
16
|
+
// sort, filter, find-replace) + the module-level Univer holder.
|
|
17
|
+
export * from './lazy-plugins';
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type { Univer, IWorkbookData } from '@univerjs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lazy plugin loading — pipeline Stage 4. The cheap plugins (render,
|
|
5
|
+
* formula, sheets, sheets-ui, numfmt, docs) stay eager because every
|
|
6
|
+
* workbook needs them. The heavy / feature-specific plugins (CF, DV,
|
|
7
|
+
* hyperlink, table, note, thread-comment, drawing, sort, filter,
|
|
8
|
+
* find-replace) load on demand:
|
|
9
|
+
*
|
|
10
|
+
* 1. Eagerly when a snapshot is about to mount and we detect the
|
|
11
|
+
* plugin's resource key on it (e.g. `SHEET_CONDITIONAL_FORMATTING_PLUGIN`
|
|
12
|
+
* in `data.resources`). This is the safety net: missing this would
|
|
13
|
+
* silently drop plugin data on file open.
|
|
14
|
+
* 2. Lazily when the user reaches for the feature — opens the Data
|
|
15
|
+
* tab (sort/filter), hits Ctrl+F (find-replace), uses the Insert
|
|
16
|
+
* tab (drawing), etc. The shell hooks `ensurePlugin(...)` into
|
|
17
|
+
* these triggers and awaits before the action runs.
|
|
18
|
+
*
|
|
19
|
+
* Each loader returns the plugins-to-register in the correct order
|
|
20
|
+
* (base before UI, same as `plugins.ts`). Registration order matters
|
|
21
|
+
* in Univer; the loaders bundle a small group together to keep that
|
|
22
|
+
* locality explicit.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type LazyPluginGroup =
|
|
26
|
+
| 'cf'
|
|
27
|
+
| 'dv'
|
|
28
|
+
| 'hyperlink'
|
|
29
|
+
| 'note'
|
|
30
|
+
| 'table'
|
|
31
|
+
| 'threadComment'
|
|
32
|
+
| 'drawing'
|
|
33
|
+
| 'sort'
|
|
34
|
+
| 'filter'
|
|
35
|
+
| 'findReplace';
|
|
36
|
+
|
|
37
|
+
type Loader = () => Promise<Array<[unknown, unknown?]>>;
|
|
38
|
+
|
|
39
|
+
const LOADERS: Record<LazyPluginGroup, Loader> = {
|
|
40
|
+
cf: async () => {
|
|
41
|
+
const [base, ui] = await Promise.all([
|
|
42
|
+
import('@univerjs/sheets-conditional-formatting'),
|
|
43
|
+
import('@univerjs/sheets-conditional-formatting-ui'),
|
|
44
|
+
]);
|
|
45
|
+
return [
|
|
46
|
+
[base.UniverSheetsConditionalFormattingPlugin],
|
|
47
|
+
[ui.UniverSheetsConditionalFormattingUIPlugin],
|
|
48
|
+
];
|
|
49
|
+
},
|
|
50
|
+
dv: async () => {
|
|
51
|
+
const [base, ui] = await Promise.all([
|
|
52
|
+
import('@univerjs/sheets-data-validation'),
|
|
53
|
+
import('@univerjs/sheets-data-validation-ui'),
|
|
54
|
+
]);
|
|
55
|
+
return [
|
|
56
|
+
[base.UniverSheetsDataValidationPlugin],
|
|
57
|
+
[ui.UniverSheetsDataValidationUIPlugin],
|
|
58
|
+
];
|
|
59
|
+
},
|
|
60
|
+
hyperlink: async () => {
|
|
61
|
+
const [base, ui] = await Promise.all([
|
|
62
|
+
import('@univerjs/sheets-hyper-link'),
|
|
63
|
+
import('@univerjs/sheets-hyper-link-ui'),
|
|
64
|
+
]);
|
|
65
|
+
return [
|
|
66
|
+
[base.UniverSheetsHyperLinkPlugin],
|
|
67
|
+
[ui.UniverSheetsHyperLinkUIPlugin],
|
|
68
|
+
];
|
|
69
|
+
},
|
|
70
|
+
note: async () => {
|
|
71
|
+
const [base, ui] = await Promise.all([
|
|
72
|
+
import('@univerjs/sheets-note'),
|
|
73
|
+
import('@univerjs/sheets-note-ui'),
|
|
74
|
+
]);
|
|
75
|
+
return [[base.UniverSheetsNotePlugin], [ui.UniverSheetsNoteUIPlugin]];
|
|
76
|
+
},
|
|
77
|
+
table: async () => {
|
|
78
|
+
const [base, ui] = await Promise.all([
|
|
79
|
+
import('@univerjs/sheets-table'),
|
|
80
|
+
import('@univerjs/sheets-table-ui'),
|
|
81
|
+
]);
|
|
82
|
+
return [[base.UniverSheetsTablePlugin], [ui.UniverSheetsTableUIPlugin]];
|
|
83
|
+
},
|
|
84
|
+
threadComment: async () => {
|
|
85
|
+
const [tc, tcUi, sheetsTc, sheetsTcUi] = await Promise.all([
|
|
86
|
+
import('@univerjs/thread-comment'),
|
|
87
|
+
import('@univerjs/thread-comment-ui'),
|
|
88
|
+
import('@univerjs/sheets-thread-comment'),
|
|
89
|
+
import('@univerjs/sheets-thread-comment-ui'),
|
|
90
|
+
]);
|
|
91
|
+
return [
|
|
92
|
+
[tc.UniverThreadCommentPlugin],
|
|
93
|
+
[tcUi.UniverThreadCommentUIPlugin],
|
|
94
|
+
[sheetsTc.UniverSheetsThreadCommentPlugin],
|
|
95
|
+
[sheetsTcUi.UniverSheetsThreadCommentUIPlugin],
|
|
96
|
+
];
|
|
97
|
+
},
|
|
98
|
+
drawing: async () => {
|
|
99
|
+
const [d, dUi, sd, sdUi] = await Promise.all([
|
|
100
|
+
import('@univerjs/drawing'),
|
|
101
|
+
import('@univerjs/drawing-ui'),
|
|
102
|
+
import('@univerjs/sheets-drawing'),
|
|
103
|
+
import('@univerjs/sheets-drawing-ui'),
|
|
104
|
+
// Side-effect imports: install FWorksheet.insertImage / getImages /
|
|
105
|
+
// updateImages on the facade prototype. Without these, code that
|
|
106
|
+
// reaches in via the FUniver facade (e2e specs, future shell glue)
|
|
107
|
+
// sees an undefined method even though the plugin is registered.
|
|
108
|
+
import('@univerjs/sheets-drawing/facade'),
|
|
109
|
+
import('@univerjs/sheets-drawing-ui/facade'),
|
|
110
|
+
]);
|
|
111
|
+
return [
|
|
112
|
+
[d.UniverDrawingPlugin],
|
|
113
|
+
[dUi.UniverDrawingUIPlugin],
|
|
114
|
+
[sd.UniverSheetsDrawingPlugin],
|
|
115
|
+
[sdUi.UniverSheetsDrawingUIPlugin],
|
|
116
|
+
];
|
|
117
|
+
},
|
|
118
|
+
sort: async () => {
|
|
119
|
+
const [base, ui] = await Promise.all([
|
|
120
|
+
import('@univerjs/sheets-sort'),
|
|
121
|
+
import('@univerjs/sheets-sort-ui'),
|
|
122
|
+
]);
|
|
123
|
+
return [[base.UniverSheetsSortPlugin], [ui.UniverSheetsSortUIPlugin]];
|
|
124
|
+
},
|
|
125
|
+
filter: async () => {
|
|
126
|
+
const [base, ui] = await Promise.all([
|
|
127
|
+
import('@univerjs/sheets-filter'),
|
|
128
|
+
import('@univerjs/sheets-filter-ui'),
|
|
129
|
+
]);
|
|
130
|
+
return [[base.UniverSheetsFilterPlugin], [ui.UniverSheetsFilterUIPlugin]];
|
|
131
|
+
},
|
|
132
|
+
findReplace: async () => {
|
|
133
|
+
const [base, sheets] = await Promise.all([
|
|
134
|
+
import('@univerjs/find-replace'),
|
|
135
|
+
import('@univerjs/sheets-find-replace'),
|
|
136
|
+
]);
|
|
137
|
+
return [[base.UniverFindReplacePlugin], [sheets.UniverSheetsFindReplacePlugin]];
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Map from a Univer `data.resources[].name` to the lazy group that owns
|
|
143
|
+
* it. Used by `eagerLoadForSnapshot` to pre-register plugins whose
|
|
144
|
+
* state already lives on the workbook so file-open never silently
|
|
145
|
+
* drops a CF rule / table / drawing / etc.
|
|
146
|
+
*/
|
|
147
|
+
const RESOURCE_NAME_TO_GROUP: Record<string, LazyPluginGroup> = {
|
|
148
|
+
SHEET_CONDITIONAL_FORMATTING_PLUGIN: 'cf',
|
|
149
|
+
SHEET_DATA_VALIDATION_PLUGIN: 'dv',
|
|
150
|
+
SHEET_HYPER_LINK_PLUGIN: 'hyperlink',
|
|
151
|
+
SHEET_NOTE_PLUGIN: 'note',
|
|
152
|
+
SHEET_TABLE_PLUGIN: 'table',
|
|
153
|
+
SHEET_THREAD_COMMENT_BASE_PLUGIN: 'threadComment',
|
|
154
|
+
SHEET_DRAWING_PLUGIN: 'drawing',
|
|
155
|
+
SHEET_SORT_PLUGIN: 'sort',
|
|
156
|
+
SHEET_FILTER_PLUGIN: 'filter',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const loaded = new Set<LazyPluginGroup>();
|
|
160
|
+
const inflight = new Map<LazyPluginGroup, Promise<void>>();
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Module-level reference to the live Univer instance, set by
|
|
164
|
+
* `UniverSheet.tsx` immediately after `new Univer()`. Lets shell code
|
|
165
|
+
* call `ensurePluginByName(group)` without plumbing the Univer
|
|
166
|
+
* instance through React context (the FUniver facade doesn't expose
|
|
167
|
+
* its host). Cleared on dispose so callers fail loudly if they hit
|
|
168
|
+
* the lazy path after teardown.
|
|
169
|
+
*/
|
|
170
|
+
let currentUniver: Univer | null = null;
|
|
171
|
+
|
|
172
|
+
export function setUniverForLazyLoad(univer: Univer | null): void {
|
|
173
|
+
currentUniver = univer;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Idempotent: subsequent calls for the same group resolve immediately
|
|
178
|
+
* if the plugin is already loaded; concurrent calls share the same
|
|
179
|
+
* in-flight promise so we never double-register.
|
|
180
|
+
*/
|
|
181
|
+
export function ensurePlugin(univer: Univer, group: LazyPluginGroup): Promise<void> {
|
|
182
|
+
if (loaded.has(group)) return Promise.resolve();
|
|
183
|
+
const existing = inflight.get(group);
|
|
184
|
+
if (existing) return existing;
|
|
185
|
+
const loader = LOADERS[group];
|
|
186
|
+
if (!loader) return Promise.resolve();
|
|
187
|
+
const p = loader().then((plugins) => {
|
|
188
|
+
for (const [PluginCtor, config] of plugins) {
|
|
189
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
190
|
+
univer.registerPlugin(PluginCtor as any, config);
|
|
191
|
+
}
|
|
192
|
+
loaded.add(group);
|
|
193
|
+
inflight.delete(group);
|
|
194
|
+
});
|
|
195
|
+
inflight.set(group, p);
|
|
196
|
+
return p;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Shell-friendly variant of `ensurePlugin` that pulls the Univer
|
|
201
|
+
* instance from the module-level holder set by `UniverSheet.tsx`. Use
|
|
202
|
+
* this anywhere we only have the FUniver facade (toolbar callbacks,
|
|
203
|
+
* panel handlers). Returns a resolved promise if the holder is empty —
|
|
204
|
+
* the worst case is a no-op, never a throw.
|
|
205
|
+
*/
|
|
206
|
+
export function ensurePluginByName(group: LazyPluginGroup): Promise<void> {
|
|
207
|
+
if (loaded.has(group)) return Promise.resolve();
|
|
208
|
+
if (!currentUniver) return Promise.resolve();
|
|
209
|
+
return ensurePlugin(currentUniver, group);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Walk a snapshot for plugin-owned resources + side-channel hyperlinks
|
|
214
|
+
* and eagerly load every group that's referenced. Returns a promise
|
|
215
|
+
* the caller MUST await before `createUnit` — Univer's resource manager
|
|
216
|
+
* silently discards keys for plugins that aren't yet registered.
|
|
217
|
+
*/
|
|
218
|
+
export async function eagerLoadForSnapshot(univer: Univer, snapshot: IWorkbookData): Promise<void> {
|
|
219
|
+
const groups = new Set<LazyPluginGroup>();
|
|
220
|
+
const resources = snapshot.resources ?? [];
|
|
221
|
+
for (const r of resources) {
|
|
222
|
+
const g = RESOURCE_NAME_TO_GROUP[r.name];
|
|
223
|
+
if (g) groups.add(g);
|
|
224
|
+
}
|
|
225
|
+
// Hyperlinks live inline in cell.p (Stage 5) — the hyperlink plugin
|
|
226
|
+
// still owns click handling / context-menu actions, so eager-load
|
|
227
|
+
// when any cell has a HYPERLINK customRange.
|
|
228
|
+
if (snapshotHasHyperlinks(snapshot)) groups.add('hyperlink');
|
|
229
|
+
await Promise.all(Array.from(groups).map((g) => ensurePlugin(univer, g)));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Idle-load every remaining lazy group AFTER Univer is mounted. The
|
|
234
|
+
* bundle split (each group ships as its own chunk) is the persistent
|
|
235
|
+
* boot-time win — the initial paint doesn't pay for them. This
|
|
236
|
+
* idle-load just ensures they're all eventually registered so a user
|
|
237
|
+
* clicking "Insert > Table" doesn't hit a no-op.
|
|
238
|
+
*
|
|
239
|
+
* Loads in parallel via `requestIdleCallback` so they don't compete
|
|
240
|
+
* with the first paint, falling back to `setTimeout(0)` in browsers
|
|
241
|
+
* without rIC (Safari).
|
|
242
|
+
*/
|
|
243
|
+
export function idleLoadAll(univer: Univer): void {
|
|
244
|
+
const groups = Object.keys(LOADERS) as LazyPluginGroup[];
|
|
245
|
+
schedule(() => {
|
|
246
|
+
for (const g of groups) {
|
|
247
|
+
void ensurePlugin(univer, g);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function schedule(fn: () => void): void {
|
|
253
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
254
|
+
const ric = (globalThis as any).requestIdleCallback as
|
|
255
|
+
| ((cb: () => void, opts?: { timeout: number }) => number)
|
|
256
|
+
| undefined;
|
|
257
|
+
if (ric) ric(fn, { timeout: 500 });
|
|
258
|
+
else setTimeout(fn, 0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function snapshotHasHyperlinks(snapshot: IWorkbookData): boolean {
|
|
262
|
+
const sheetOrder = snapshot.sheetOrder ?? [];
|
|
263
|
+
for (const sid of sheetOrder) {
|
|
264
|
+
const sheet = snapshot.sheets?.[sid];
|
|
265
|
+
if (!sheet?.cellData) continue;
|
|
266
|
+
const cellData = sheet.cellData as Record<
|
|
267
|
+
string,
|
|
268
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
269
|
+
Record<string, { p?: any }>
|
|
270
|
+
>;
|
|
271
|
+
for (const r of Object.keys(cellData)) {
|
|
272
|
+
const row = cellData[r];
|
|
273
|
+
for (const c of Object.keys(row)) {
|
|
274
|
+
const ranges = row[c]?.p?.body?.customRanges ?? [];
|
|
275
|
+
if (ranges.some((cr: { rangeType?: number }) => cr.rangeType === 0)) return true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny perf shim — drop-in replacement for apps/web's `../perf` so the
|
|
3
|
+
* SDK doesn't have to pull in a User Timing dependency. The host can
|
|
4
|
+
* wrap individual SDK calls with its own timing if it wants spans;
|
|
5
|
+
* inside the SDK they're cheap no-op forwards.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function timeIt<T>(_label: string, fn: () => T): T {
|
|
9
|
+
return fn();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function timeItAsync<T>(_label: string, fn: () => Promise<T>): Promise<T> {
|
|
13
|
+
return fn();
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot defaults pulled out of apps/web/src/snapshot.ts so the
|
|
3
|
+
* SDK's xlsx parser doesn't have to import that whole module. Keep
|
|
4
|
+
* these in sync with the host snapshot module if the defaults drift.
|
|
5
|
+
*
|
|
6
|
+
* UNIVER_VERSION must match the runtime Univer the host boots — the
|
|
7
|
+
* appVersion field on the IWorkbookData snapshot is checked at unit
|
|
8
|
+
* mount and a mismatch warns in dev. Sheet apps' `../snapshot` reads
|
|
9
|
+
* the version from the workspace's @univerjs/core dep; we hardcode
|
|
10
|
+
* the same minor here because the SDK declares @univerjs/* as
|
|
11
|
+
* `^0.24.0` peer.
|
|
12
|
+
*/
|
|
13
|
+
export const INITIAL_ROWS = 1024;
|
|
14
|
+
export const INITIAL_COLUMNS = 26;
|
|
15
|
+
export const UNIVER_VERSION = '0.24.0';
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type ExcelJS from 'exceljs';
|
|
2
|
+
import type { IWorkbookData } from '@univerjs/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* xlsx-native `cell.note` ⇄ Univer `thread-comment` resource bridge.
|
|
6
|
+
*
|
|
7
|
+
* Comments in xlsx live on the cell itself (`xl/comments<N>.xml`).
|
|
8
|
+
* Univer's thread-comment plugin stores them in a workbook-level
|
|
9
|
+
* resource keyed by sheet id under the name below — same
|
|
10
|
+
* registration pattern as defined-names. Each side of the round-trip
|
|
11
|
+
* has to translate to the shape the other one expects:
|
|
12
|
+
*
|
|
13
|
+
* xlsx cell.note (string | { texts: [{ text }] })
|
|
14
|
+
* ⇅
|
|
15
|
+
* { dataStream: "<text>\r\n" } (Univer IDocumentBody minimal form)
|
|
16
|
+
*
|
|
17
|
+
* Why this exists separately from the parser/exporter modules:
|
|
18
|
+
* 1. Two-sided code lives next to itself so the inverse stays
|
|
19
|
+
* obvious. The audit test fails the moment one side drifts.
|
|
20
|
+
* 2. The `IThreadComment` row builder is non-trivial and reused
|
|
21
|
+
* verbatim on both sides — extracting it keeps parse-impl and
|
|
22
|
+
* export-impl from carrying near-duplicate shape definitions.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Mirrors vendor/univer/packages/thread-comment/src/controllers/tc-resource.controller.ts:25
|
|
26
|
+
// — the resource registration name the plugin's controller subscribes
|
|
27
|
+
// to at workbook load. Hard-coding instead of importing avoids pulling
|
|
28
|
+
// the plugin into the xlsx worker bundle (thread-comment ships UI deps
|
|
29
|
+
// that don't tree-shake cleanly in a Worker context).
|
|
30
|
+
export const THREAD_COMMENT_RESOURCE = 'SHEET_UNIVER_THREAD_COMMENT_PLUGIN';
|
|
31
|
+
|
|
32
|
+
// Minimal subset of Univer's `IThreadComment` we synthesise. The
|
|
33
|
+
// plugin populates the rest (children, mentions, resolved) on
|
|
34
|
+
// subsequent edits; xlsx-native comments don't carry any of them.
|
|
35
|
+
export type SynthComment = {
|
|
36
|
+
id: string;
|
|
37
|
+
threadId: string;
|
|
38
|
+
ref: string; // "B2" style
|
|
39
|
+
dT: string; // ISO date
|
|
40
|
+
personId: string;
|
|
41
|
+
text: { dataStream: string };
|
|
42
|
+
unitId: string;
|
|
43
|
+
subUnitId: string;
|
|
44
|
+
// children: [] on import — Univer's controller tolerates this
|
|
45
|
+
// because `addComment` of a root with no children is the normal
|
|
46
|
+
// create path.
|
|
47
|
+
children?: never[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const PERSON_FROM_XLSX = 'imported';
|
|
51
|
+
|
|
52
|
+
function colToLetters(n: number): string {
|
|
53
|
+
let out = '';
|
|
54
|
+
let v = n;
|
|
55
|
+
while (v >= 0) {
|
|
56
|
+
out = String.fromCharCode(65 + (v % 26)) + out;
|
|
57
|
+
v = Math.floor(v / 26) - 1;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function refOf(row: number, column: number): string {
|
|
63
|
+
return `${colToLetters(column)}${row + 1}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function noteToString(note: unknown): string | null {
|
|
67
|
+
if (typeof note === 'string') return note.trim() ? note : null;
|
|
68
|
+
if (note && typeof note === 'object') {
|
|
69
|
+
// ExcelJS's expanded shape: { texts: [{ text }] }. Fall back to
|
|
70
|
+
// `.text` if a single-string variant came back through some
|
|
71
|
+
// odder code path.
|
|
72
|
+
const texts = (note as { texts?: Array<{ text?: string }> }).texts;
|
|
73
|
+
if (Array.isArray(texts)) {
|
|
74
|
+
const joined = texts.map((t) => t?.text ?? '').join('');
|
|
75
|
+
return joined.trim() ? joined : null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Walk every worksheet and collect xlsx-native `cell.note` entries
|
|
83
|
+
* into a Univer thread-comment resource payload. The synthesised IDs
|
|
84
|
+
* and dates aren't stable — re-opening the same file produces new
|
|
85
|
+
* ones — which is fine for the use cases we care about (comments
|
|
86
|
+
* survive the round-trip; nobody is referring to them by id).
|
|
87
|
+
*/
|
|
88
|
+
export function readCommentsFromXlsx(
|
|
89
|
+
wb: ExcelJS.Workbook,
|
|
90
|
+
unitId: string,
|
|
91
|
+
sheetIdForExcel: (excelId: number) => string,
|
|
92
|
+
): Record<string, SynthComment[]> {
|
|
93
|
+
const out: Record<string, SynthComment[]> = {};
|
|
94
|
+
let seq = 0;
|
|
95
|
+
const nowIso = new Date().toISOString();
|
|
96
|
+
|
|
97
|
+
for (const ws of wb.worksheets) {
|
|
98
|
+
const subUnitId = sheetIdForExcel(ws.id);
|
|
99
|
+
const bucket: SynthComment[] = [];
|
|
100
|
+
ws.eachRow({ includeEmpty: false }, (row, rowNumber) => {
|
|
101
|
+
row.eachCell({ includeEmpty: false }, (cell, colNumber) => {
|
|
102
|
+
// ExcelJS exposes notes on cells with comments — the field is
|
|
103
|
+
// missing entirely on the vast majority of cells, so iterating
|
|
104
|
+
// every cell is cheap.
|
|
105
|
+
const note = (cell as unknown as { note?: unknown }).note;
|
|
106
|
+
const text = noteToString(note);
|
|
107
|
+
if (!text) return;
|
|
108
|
+
const id = `xc-${unitId}-${seq++}`;
|
|
109
|
+
bucket.push({
|
|
110
|
+
id,
|
|
111
|
+
threadId: id, // single-comment thread; thread id = root id
|
|
112
|
+
ref: refOf(rowNumber - 1, colNumber - 1),
|
|
113
|
+
dT: nowIso,
|
|
114
|
+
personId: PERSON_FROM_XLSX,
|
|
115
|
+
// Univer's body convention terminates with `\r\n` (one paragraph).
|
|
116
|
+
// Without it the cursor in the panel jumps a column on first
|
|
117
|
+
// open of the comment.
|
|
118
|
+
text: { dataStream: `${text}\r\n` },
|
|
119
|
+
unitId,
|
|
120
|
+
subUnitId,
|
|
121
|
+
children: [],
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
if (bucket.length > 0) out[subUnitId] = bucket;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Merge a synthesised comment payload into the workbook's `resources`
|
|
133
|
+
* array. Skipped when the workbook already carries a thread-comment
|
|
134
|
+
* resource from our hidden sidecar (`__casual_sheets_resources__`) —
|
|
135
|
+
* that one has the full plugin shape; we shouldn't clobber it with a
|
|
136
|
+
* lossy xlsx-native re-derivation.
|
|
137
|
+
*/
|
|
138
|
+
export function mergeCommentsIntoResources(
|
|
139
|
+
resources: IWorkbookData['resources'],
|
|
140
|
+
comments: Record<string, SynthComment[]>,
|
|
141
|
+
): IWorkbookData['resources'] {
|
|
142
|
+
if (Object.keys(comments).length === 0) return resources;
|
|
143
|
+
const existing = resources?.find((r) => r.name === THREAD_COMMENT_RESOURCE);
|
|
144
|
+
if (existing) return resources;
|
|
145
|
+
const next = [...(resources ?? [])];
|
|
146
|
+
next.push({
|
|
147
|
+
name: THREAD_COMMENT_RESOURCE,
|
|
148
|
+
data: JSON.stringify(comments),
|
|
149
|
+
});
|
|
150
|
+
return next;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Read the thread-comment resource off a snapshot. Tolerant of older
|
|
155
|
+
* / missing / malformed payloads — those cases return `{}` so the
|
|
156
|
+
* exporter just skips writing notes rather than throwing on save.
|
|
157
|
+
*/
|
|
158
|
+
export function readCommentsFromSnapshot(
|
|
159
|
+
data: IWorkbookData,
|
|
160
|
+
): Record<string, SynthComment[]> {
|
|
161
|
+
const entry = data.resources?.find((r) => r.name === THREAD_COMMENT_RESOURCE);
|
|
162
|
+
if (!entry?.data) return {};
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(entry.data) as Record<string, unknown>;
|
|
165
|
+
if (!parsed || typeof parsed !== 'object') return {};
|
|
166
|
+
const out: Record<string, SynthComment[]> = {};
|
|
167
|
+
for (const [sheetId, value] of Object.entries(parsed)) {
|
|
168
|
+
if (!Array.isArray(value)) continue;
|
|
169
|
+
const bucket: SynthComment[] = [];
|
|
170
|
+
for (const c of value) {
|
|
171
|
+
if (!c || typeof c !== 'object') continue;
|
|
172
|
+
const obj = c as Record<string, unknown>;
|
|
173
|
+
const text = (obj.text as { dataStream?: string } | undefined)?.dataStream;
|
|
174
|
+
const ref = typeof obj.ref === 'string' ? obj.ref : null;
|
|
175
|
+
if (!ref || typeof text !== 'string') continue;
|
|
176
|
+
bucket.push(c as SynthComment);
|
|
177
|
+
}
|
|
178
|
+
if (bucket.length > 0) out[sheetId] = bucket;
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
} catch {
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Extract the plain string from a Univer comment body. Strips the
|
|
188
|
+
* trailing `\r\n` (or `\n`) that the body convention appends so the
|
|
189
|
+
* xlsx-side note doesn't show as a blank line below the text in
|
|
190
|
+
* Excel's pop-up.
|
|
191
|
+
*/
|
|
192
|
+
export function commentBodyToString(body: SynthComment['text']): string {
|
|
193
|
+
const s = body?.dataStream ?? '';
|
|
194
|
+
return s.replace(/[\r\n]+$/, '');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Parse an "A1"-style ref back to a zero-based (row, col). Falls back
|
|
199
|
+
* to (-1, -1) on malformed input — exporter callers should skip in
|
|
200
|
+
* that case.
|
|
201
|
+
*/
|
|
202
|
+
export function refToRowCol(ref: string): { row: number; column: number } {
|
|
203
|
+
const m = /^([A-Z]+)(\d+)$/.exec(ref);
|
|
204
|
+
if (!m) return { row: -1, column: -1 };
|
|
205
|
+
const letters = m[1];
|
|
206
|
+
let col = 0;
|
|
207
|
+
for (let i = 0; i < letters.length; i++) col = col * 26 + (letters.charCodeAt(i) - 64);
|
|
208
|
+
return { row: Number(m[2]) - 1, column: col - 1 };
|
|
209
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name of a hidden worksheet we use to stash JSON we can't represent
|
|
3
|
+
* natively in xlsx (Univer plugin state — e.g. table definitions, outline
|
|
4
|
+
* groups). On open we recognize and consume it, never showing it to the
|
|
5
|
+
* user. Defined in its own module so both the parser worker and the
|
|
6
|
+
* exporter worker can import it without dragging the other one's
|
|
7
|
+
* ExcelJS code into their bundle.
|
|
8
|
+
*/
|
|
9
|
+
export const RESOURCES_SHEET = '__casual_sheets_resources__';
|