@casualoffice/sheets 0.9.0 → 0.10.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/dist/api-BI2VLYQ6.d.cts +62 -0
- package/dist/api-BI2VLYQ6.d.ts +62 -0
- package/dist/chrome.cjs +2569 -0
- package/dist/chrome.cjs.map +1 -0
- package/dist/chrome.d.cts +96 -0
- package/dist/chrome.d.ts +96 -0
- package/dist/chrome.js +2556 -0
- package/dist/chrome.js.map +1 -0
- package/dist/collab.cjs +770 -0
- package/dist/collab.cjs.map +1 -0
- package/dist/collab.d.cts +248 -0
- package/dist/collab.d.ts +248 -0
- package/dist/collab.js +737 -0
- package/dist/collab.js.map +1 -0
- package/dist/embed/embed-runtime.js +128 -128
- package/dist/embed.cjs +166 -0
- package/dist/embed.cjs.map +1 -1
- package/dist/embed.d.cts +78 -3
- package/dist/embed.d.ts +78 -3
- package/dist/embed.js +166 -0
- package/dist/embed.js.map +1 -1
- package/dist/index.cjs +262 -165
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +271 -168
- package/dist/index.js.map +1 -1
- package/dist/{protocol--KyBQUjU.d.cts → protocol-Cq4Cdoyi.d.cts} +19 -2
- package/dist/{protocol-cEzy7S0i.d.ts → protocol-DqDaG2yG.d.ts} +19 -2
- package/dist/sheets.cjs +102 -16
- package/dist/sheets.cjs.map +1 -1
- package/dist/sheets.d.cts +49 -63
- package/dist/sheets.d.ts +49 -63
- package/dist/sheets.js +111 -19
- package/dist/sheets.js.map +1 -1
- package/package.json +28 -3
- package/src/chrome/AutoSumPicker.tsx +176 -0
- package/src/chrome/BordersPicker.tsx +171 -0
- package/src/chrome/ChromeBottom.tsx +18 -0
- package/src/chrome/ChromeTop.tsx +21 -0
- package/src/chrome/ColorPicker.tsx +220 -0
- package/src/chrome/FindReplace.tsx +370 -0
- package/src/chrome/FormulaBar.tsx +378 -0
- package/src/chrome/Icon.tsx +43 -0
- package/src/chrome/MenuBar.tsx +336 -0
- package/src/chrome/NameBox.tsx +347 -0
- package/src/chrome/SheetTabs.tsx +346 -0
- package/src/chrome/StatusBar.tsx +232 -0
- package/src/chrome/Toolbar.tsx +401 -0
- package/src/chrome/fonts.ts +42 -0
- package/src/chrome/index.ts +24 -0
- package/src/collab/attachCollab.ts +151 -0
- package/src/collab/bridge-helpers.ts +97 -0
- package/src/collab/bridge.ts +885 -0
- package/src/collab/bridge.unit.test.ts +160 -0
- package/src/collab/index.ts +38 -0
- package/src/collab/replay-retry.ts +137 -0
- package/src/collab/replay-retry.unit.test.ts +223 -0
- package/src/collab/ws-url.ts +20 -0
- package/src/collab/ws-url.unit.test.ts +35 -0
- package/src/embed/EmbedHostTransport.ts +16 -1
- package/src/embed/EmbedTransport.ts +16 -0
- package/src/embed/EmbedTransport.unit.test.ts +88 -2
- package/src/embed/index.ts +7 -0
- package/src/embed/protocol.ts +34 -0
- package/src/embed-runtime/index.tsx +20 -0
- package/src/sheets/CasualSheets.tsx +204 -33
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FindReplace — a custom find/replace dialog for `<CasualSheets chrome>`.
|
|
3
|
+
*
|
|
4
|
+
* Univer's own find-replace dialog doesn't render in the SDK's headless mount
|
|
5
|
+
* (it needs Univer's UI/overlay layer, which is dormant when header/toolbar/
|
|
6
|
+
* footer are off), so the chrome ships its own — driven purely through the
|
|
7
|
+
* facade: search reads the active sheet's `cellData` from `getSnapshot()`,
|
|
8
|
+
* navigation `activate()`s the matching cell, and replace `setValue()`s it.
|
|
9
|
+
*
|
|
10
|
+
* Opens on Ctrl/Cmd+F (find) or Ctrl/Cmd+H (replace) while the editor has focus;
|
|
11
|
+
* Escape closes. Self-managing — just mount it in the chrome with the live api.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
useEffect,
|
|
16
|
+
useMemo,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
type CSSProperties,
|
|
20
|
+
type KeyboardEvent,
|
|
21
|
+
} from 'react';
|
|
22
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
23
|
+
import { Icon } from './Icon';
|
|
24
|
+
|
|
25
|
+
interface Match {
|
|
26
|
+
row: number;
|
|
27
|
+
col: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
type AnySnapshot = any;
|
|
32
|
+
|
|
33
|
+
/** Collect matches on the active sheet, in row-major order. Searches each
|
|
34
|
+
* cell's value (`v`) and formula (`f`) text. */
|
|
35
|
+
function findMatches(api: CasualSheetsAPI, query: string, matchCase: boolean): Match[] {
|
|
36
|
+
if (!query) return [];
|
|
37
|
+
const snap = api.getSnapshot() as AnySnapshot;
|
|
38
|
+
const sheetId =
|
|
39
|
+
api.getSelection()?.sheetId ?? api.univer.getActiveWorkbook()?.getActiveSheet()?.getSheetId();
|
|
40
|
+
const sheet = sheetId ? snap?.sheets?.[sheetId] : undefined;
|
|
41
|
+
const cellData = sheet?.cellData as Record<string, Record<string, AnySnapshot>> | undefined;
|
|
42
|
+
if (!cellData) return [];
|
|
43
|
+
const needle = matchCase ? query : query.toLowerCase();
|
|
44
|
+
const out: Match[] = [];
|
|
45
|
+
for (const rk of Object.keys(cellData).sort((a, b) => Number(a) - Number(b))) {
|
|
46
|
+
const rowObj = cellData[rk];
|
|
47
|
+
if (!rowObj) continue;
|
|
48
|
+
for (const ck of Object.keys(rowObj).sort((a, b) => Number(a) - Number(b))) {
|
|
49
|
+
const cell = rowObj[ck];
|
|
50
|
+
if (!cell) continue;
|
|
51
|
+
const hay = `${cell.v ?? ''}${cell.f ? ` ${cell.f}` : ''}`;
|
|
52
|
+
const cmp = matchCase ? hay : hay.toLowerCase();
|
|
53
|
+
if (cmp.includes(needle)) out.push({ row: Number(rk), col: Number(ck) });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const WRAP_STYLE: CSSProperties = {
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
top: 8,
|
|
62
|
+
right: 12,
|
|
63
|
+
zIndex: 1100,
|
|
64
|
+
width: 320,
|
|
65
|
+
padding: 10,
|
|
66
|
+
borderRadius: 8,
|
|
67
|
+
border: '1px solid var(--cs-chrome-border, #e6e9ee)',
|
|
68
|
+
background: 'var(--cs-chrome-input-bg, #fff)',
|
|
69
|
+
boxShadow: '0 8px 28px rgba(0,0,0,0.18)',
|
|
70
|
+
font: 'inherit',
|
|
71
|
+
fontSize: 13,
|
|
72
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const ROW_STYLE: CSSProperties = { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 };
|
|
76
|
+
|
|
77
|
+
const INPUT_STYLE: CSSProperties = {
|
|
78
|
+
flex: '1 1 auto',
|
|
79
|
+
height: 26,
|
|
80
|
+
padding: '0 8px',
|
|
81
|
+
border: '1px solid var(--cs-chrome-border, #cdd3db)',
|
|
82
|
+
borderRadius: 4,
|
|
83
|
+
background: 'var(--cs-chrome-input-bg, #fff)',
|
|
84
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
85
|
+
font: 'inherit',
|
|
86
|
+
fontSize: 13,
|
|
87
|
+
boxSizing: 'border-box',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const BTN_STYLE: CSSProperties = {
|
|
91
|
+
height: 26,
|
|
92
|
+
padding: '0 10px',
|
|
93
|
+
border: '1px solid var(--cs-chrome-border, #cdd3db)',
|
|
94
|
+
borderRadius: 4,
|
|
95
|
+
background: 'var(--cs-chrome-bg, #f3f5f8)',
|
|
96
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
97
|
+
font: 'inherit',
|
|
98
|
+
fontSize: 12,
|
|
99
|
+
cursor: 'pointer',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const ICON_BTN_STYLE: CSSProperties = {
|
|
103
|
+
width: 26,
|
|
104
|
+
height: 26,
|
|
105
|
+
display: 'inline-flex',
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
justifyContent: 'center',
|
|
108
|
+
border: 'none',
|
|
109
|
+
borderRadius: 4,
|
|
110
|
+
background: 'transparent',
|
|
111
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
112
|
+
cursor: 'pointer',
|
|
113
|
+
padding: 0,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export interface FindReplaceProps {
|
|
117
|
+
/** Live API, or `null` until the editor is ready. */
|
|
118
|
+
api: CasualSheetsAPI | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function FindReplace({ api }: FindReplaceProps) {
|
|
122
|
+
const [open, setOpen] = useState(false);
|
|
123
|
+
const [showReplace, setShowReplace] = useState(false);
|
|
124
|
+
const [query, setQuery] = useState('');
|
|
125
|
+
const [replaceText, setReplaceText] = useState('');
|
|
126
|
+
const [matchCase, setMatchCase] = useState(false);
|
|
127
|
+
const [idx, setIdx] = useState(0);
|
|
128
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
129
|
+
|
|
130
|
+
// Open on Ctrl/Cmd+F (find) / Ctrl/Cmd+H (replace), close on Escape.
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const onKey = (e: globalThis.KeyboardEvent) => {
|
|
133
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
134
|
+
if (mod && (e.key === 'f' || e.key === 'F')) {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
setShowReplace(false);
|
|
137
|
+
setOpen(true);
|
|
138
|
+
requestAnimationFrame(() => inputRef.current?.focus());
|
|
139
|
+
} else if (mod && (e.key === 'h' || e.key === 'H')) {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
setShowReplace(true);
|
|
142
|
+
setOpen(true);
|
|
143
|
+
requestAnimationFrame(() => inputRef.current?.focus());
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
document.addEventListener('keydown', onKey);
|
|
147
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
const matches = useMemo(
|
|
151
|
+
() => (api && open && query ? findMatches(api, query, matchCase) : []),
|
|
152
|
+
[api, open, query, matchCase],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Keep idx in range as matches change.
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
setIdx((i) => (matches.length === 0 ? 0 : Math.min(i, matches.length - 1)));
|
|
158
|
+
}, [matches.length]);
|
|
159
|
+
|
|
160
|
+
if (!open || !api) return null;
|
|
161
|
+
|
|
162
|
+
const go = (delta: number) => {
|
|
163
|
+
if (matches.length === 0) return;
|
|
164
|
+
const next = (idx + delta + matches.length) % matches.length;
|
|
165
|
+
setIdx(next);
|
|
166
|
+
activate(matches[next]);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const activate = (m: Match) => {
|
|
170
|
+
const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
|
+
(sheet?.getRange(m.row, m.col) as any)?.activate?.();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const replaceOne = () => {
|
|
176
|
+
if (matches.length === 0) return;
|
|
177
|
+
const m = matches[Math.min(idx, matches.length - 1)];
|
|
178
|
+
replaceAt(m);
|
|
179
|
+
// Matches recompute via the query memo on next render; advance stays put.
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const replaceAt = (m: Match) => {
|
|
183
|
+
const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
|
|
184
|
+
if (!sheet) return;
|
|
185
|
+
const cur = String(sheet.getRange(m.row, m.col).getValue?.() ?? '');
|
|
186
|
+
const next = replaceInString(cur, query, replaceText, matchCase);
|
|
187
|
+
if (next !== cur) sheet.getRange(m.row, m.col).setValue(next);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const replaceAll = () => {
|
|
191
|
+
const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
|
|
192
|
+
if (!sheet) return;
|
|
193
|
+
for (const m of matches) {
|
|
194
|
+
const cur = String(sheet.getRange(m.row, m.col).getValue?.() ?? '');
|
|
195
|
+
const next = replaceInString(cur, query, replaceText, matchCase);
|
|
196
|
+
if (next !== cur) sheet.getRange(m.row, m.col).setValue(next);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const onInputKey = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
201
|
+
if (e.key === 'Enter') {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
go(e.shiftKey ? -1 : 1);
|
|
204
|
+
} else if (e.key === 'Escape') {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
setOpen(false);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const count = matches.length;
|
|
211
|
+
const position = count === 0 ? 0 : Math.min(idx, count - 1) + 1;
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div
|
|
215
|
+
style={WRAP_STYLE}
|
|
216
|
+
data-testid="cs-find-replace"
|
|
217
|
+
role="dialog"
|
|
218
|
+
aria-label="Find and replace"
|
|
219
|
+
>
|
|
220
|
+
<div style={ROW_STYLE}>
|
|
221
|
+
<input
|
|
222
|
+
ref={inputRef}
|
|
223
|
+
type="text"
|
|
224
|
+
aria-label="Find"
|
|
225
|
+
placeholder="Find"
|
|
226
|
+
data-testid="cs-find-input"
|
|
227
|
+
style={INPUT_STYLE}
|
|
228
|
+
value={query}
|
|
229
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
230
|
+
onKeyDown={onInputKey}
|
|
231
|
+
/>
|
|
232
|
+
<span
|
|
233
|
+
style={{ minWidth: 54, textAlign: 'center', color: 'var(--cs-chrome-muted, #6b7280)' }}
|
|
234
|
+
data-testid="cs-find-count"
|
|
235
|
+
>
|
|
236
|
+
{position}/{count}
|
|
237
|
+
</span>
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
style={ICON_BTN_STYLE}
|
|
241
|
+
aria-label="Previous match"
|
|
242
|
+
data-testid="cs-find-prev"
|
|
243
|
+
onMouseDown={(e) => {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
go(-1);
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
<Icon name="keyboard_arrow_up" size={18} />
|
|
249
|
+
</button>
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
style={ICON_BTN_STYLE}
|
|
253
|
+
aria-label="Next match"
|
|
254
|
+
data-testid="cs-find-next"
|
|
255
|
+
onMouseDown={(e) => {
|
|
256
|
+
e.preventDefault();
|
|
257
|
+
go(1);
|
|
258
|
+
}}
|
|
259
|
+
>
|
|
260
|
+
<Icon name="keyboard_arrow_down" size={18} />
|
|
261
|
+
</button>
|
|
262
|
+
<button
|
|
263
|
+
type="button"
|
|
264
|
+
style={ICON_BTN_STYLE}
|
|
265
|
+
aria-label="Close"
|
|
266
|
+
data-testid="cs-find-close"
|
|
267
|
+
onMouseDown={(e) => {
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
setOpen(false);
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
<Icon name="close" size={18} />
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{showReplace && (
|
|
277
|
+
<div style={ROW_STYLE}>
|
|
278
|
+
<input
|
|
279
|
+
type="text"
|
|
280
|
+
aria-label="Replace with"
|
|
281
|
+
placeholder="Replace with"
|
|
282
|
+
data-testid="cs-replace-input"
|
|
283
|
+
style={INPUT_STYLE}
|
|
284
|
+
value={replaceText}
|
|
285
|
+
onChange={(e) => setReplaceText(e.target.value)}
|
|
286
|
+
/>
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
style={BTN_STYLE}
|
|
290
|
+
data-testid="cs-replace-one"
|
|
291
|
+
onMouseDown={(e) => {
|
|
292
|
+
e.preventDefault();
|
|
293
|
+
replaceOne();
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
Replace
|
|
297
|
+
</button>
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
style={BTN_STYLE}
|
|
301
|
+
data-testid="cs-replace-all"
|
|
302
|
+
onMouseDown={(e) => {
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
replaceAll();
|
|
305
|
+
}}
|
|
306
|
+
>
|
|
307
|
+
All
|
|
308
|
+
</button>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
313
|
+
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
|
|
314
|
+
<input
|
|
315
|
+
type="checkbox"
|
|
316
|
+
data-testid="cs-find-match-case"
|
|
317
|
+
checked={matchCase}
|
|
318
|
+
onChange={(e) => setMatchCase(e.target.checked)}
|
|
319
|
+
/>
|
|
320
|
+
Match case
|
|
321
|
+
</label>
|
|
322
|
+
{!showReplace && (
|
|
323
|
+
<button
|
|
324
|
+
type="button"
|
|
325
|
+
style={{
|
|
326
|
+
...BTN_STYLE,
|
|
327
|
+
marginLeft: 'auto',
|
|
328
|
+
border: 'none',
|
|
329
|
+
background: 'transparent',
|
|
330
|
+
color: 'var(--cs-chrome-active-fg, #0e7490)',
|
|
331
|
+
}}
|
|
332
|
+
data-testid="cs-find-toggle-replace"
|
|
333
|
+
onMouseDown={(e) => {
|
|
334
|
+
e.preventDefault();
|
|
335
|
+
setShowReplace(true);
|
|
336
|
+
}}
|
|
337
|
+
>
|
|
338
|
+
Replace…
|
|
339
|
+
</button>
|
|
340
|
+
)}
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Replace all occurrences of `query` in `text` (case-(in)sensitive). */
|
|
347
|
+
function replaceInString(
|
|
348
|
+
text: string,
|
|
349
|
+
query: string,
|
|
350
|
+
replacement: string,
|
|
351
|
+
matchCase: boolean,
|
|
352
|
+
): string {
|
|
353
|
+
if (!query) return text;
|
|
354
|
+
if (matchCase) return text.split(query).join(replacement);
|
|
355
|
+
// Case-insensitive: walk and rebuild preserving non-matched casing.
|
|
356
|
+
const lower = text.toLowerCase();
|
|
357
|
+
const needle = query.toLowerCase();
|
|
358
|
+
let out = '';
|
|
359
|
+
let i = 0;
|
|
360
|
+
while (i < text.length) {
|
|
361
|
+
if (lower.startsWith(needle, i)) {
|
|
362
|
+
out += replacement;
|
|
363
|
+
i += needle.length;
|
|
364
|
+
} else {
|
|
365
|
+
out += text[i];
|
|
366
|
+
i += 1;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|