@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,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MenuBar — the built-in dropdown menu row for `<CasualSheets chrome>`.
|
|
3
|
+
*
|
|
4
|
+
* Office-style horizontal menu strip (no logo, no title — the host frames the
|
|
5
|
+
* editor with its own bar): Edit · Insert · Format · Data. Each button opens a
|
|
6
|
+
* dropdown of items that dispatch a single verified Univer command through
|
|
7
|
+
* `CasualSheetsAPI.executeCommand`, then close the menu.
|
|
8
|
+
*
|
|
9
|
+
* Self-contained: only one menu is open at a time; Escape and an outside
|
|
10
|
+
* pointerdown close it. Every command id below was verified against the fork
|
|
11
|
+
* (`vendor/univer-revamp/packages`); items whose command can't be verified are
|
|
12
|
+
* omitted rather than dispatched blindly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useEffect, useRef, useState, type CSSProperties } from 'react';
|
|
16
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
17
|
+
import { Icon } from './Icon';
|
|
18
|
+
import { ensureChromeFonts } from './fonts';
|
|
19
|
+
|
|
20
|
+
type MenuId = 'edit' | 'insert' | 'format' | 'data' | 'view';
|
|
21
|
+
|
|
22
|
+
interface MenuItem {
|
|
23
|
+
/** Stable id — drives the per-item testid `cs-menuitem-<id>`. */
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
command: string;
|
|
27
|
+
icon?: string;
|
|
28
|
+
params?: object;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Menu {
|
|
32
|
+
id: MenuId;
|
|
33
|
+
label: string;
|
|
34
|
+
items: MenuItem[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// All command ids below verified via:
|
|
38
|
+
// grep -rhoE "id: '(sheet\.command\.[a-z.-]+)'" vendor/univer-revamp/packages | sort -u
|
|
39
|
+
// (undo/redo: univer.command.undo / .redo in packages/core undoredo)
|
|
40
|
+
const MENUS: Menu[] = [
|
|
41
|
+
{
|
|
42
|
+
id: 'edit',
|
|
43
|
+
label: 'Edit',
|
|
44
|
+
items: [
|
|
45
|
+
{ id: 'undo', label: 'Undo', command: 'univer.command.undo', icon: 'undo' },
|
|
46
|
+
{ id: 'redo', label: 'Redo', command: 'univer.command.redo', icon: 'redo' },
|
|
47
|
+
{ id: 'cut', label: 'Cut', command: 'univer.command.cut', icon: 'content_cut' },
|
|
48
|
+
{ id: 'copy', label: 'Copy', command: 'univer.command.copy', icon: 'content_copy' },
|
|
49
|
+
{ id: 'paste', label: 'Paste', command: 'univer.command.paste', icon: 'content_paste' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'insert',
|
|
54
|
+
label: 'Insert',
|
|
55
|
+
items: [
|
|
56
|
+
{
|
|
57
|
+
id: 'insert-row',
|
|
58
|
+
label: 'Insert row above',
|
|
59
|
+
command: 'sheet.command.insert-row-before',
|
|
60
|
+
icon: 'add_row_above',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'insert-col',
|
|
64
|
+
label: 'Insert column left',
|
|
65
|
+
command: 'sheet.command.insert-col-before',
|
|
66
|
+
icon: 'add_column_left',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'delete-row',
|
|
70
|
+
label: 'Delete row',
|
|
71
|
+
command: 'sheet.command.remove-row',
|
|
72
|
+
icon: 'delete',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'delete-col',
|
|
76
|
+
label: 'Delete column',
|
|
77
|
+
command: 'sheet.command.remove-col',
|
|
78
|
+
icon: 'delete',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'format',
|
|
84
|
+
label: 'Format',
|
|
85
|
+
items: [
|
|
86
|
+
{ id: 'bold', label: 'Bold', command: 'sheet.command.set-range-bold', icon: 'format_bold' },
|
|
87
|
+
{
|
|
88
|
+
id: 'italic',
|
|
89
|
+
label: 'Italic',
|
|
90
|
+
command: 'sheet.command.set-range-italic',
|
|
91
|
+
icon: 'format_italic',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'underline',
|
|
95
|
+
label: 'Underline',
|
|
96
|
+
command: 'sheet.command.set-range-underline',
|
|
97
|
+
icon: 'format_underlined',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'wrap-text',
|
|
101
|
+
label: 'Wrap text',
|
|
102
|
+
command: 'sheet.command.set-text-wrap',
|
|
103
|
+
icon: 'wrap_text',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'clear-format',
|
|
107
|
+
label: 'Clear formatting',
|
|
108
|
+
command: 'sheet.command.clear-selection-format',
|
|
109
|
+
icon: 'format_clear',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'data',
|
|
115
|
+
label: 'Data',
|
|
116
|
+
items: [
|
|
117
|
+
{
|
|
118
|
+
id: 'sort-asc',
|
|
119
|
+
label: 'Sort ascending',
|
|
120
|
+
command: 'sheet.command.sort-range-asc',
|
|
121
|
+
icon: 'arrow_upward',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'sort-desc',
|
|
125
|
+
label: 'Sort descending',
|
|
126
|
+
command: 'sheet.command.sort-range-desc',
|
|
127
|
+
icon: 'arrow_downward',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'toggle-filter',
|
|
131
|
+
label: 'Toggle filter',
|
|
132
|
+
command: 'sheet.command.smart-toggle-filter',
|
|
133
|
+
icon: 'filter_alt',
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'view',
|
|
139
|
+
label: 'View',
|
|
140
|
+
items: [
|
|
141
|
+
{
|
|
142
|
+
id: 'freeze',
|
|
143
|
+
label: 'Freeze panes',
|
|
144
|
+
command: 'sheet.command.set-selection-frozen',
|
|
145
|
+
icon: 'table_view',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'unfreeze',
|
|
149
|
+
label: 'Unfreeze panes',
|
|
150
|
+
command: 'sheet.command.cancel-frozen',
|
|
151
|
+
icon: 'grid_off',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'toggle-gridlines',
|
|
155
|
+
label: 'Toggle gridlines',
|
|
156
|
+
command: 'sheet.command.toggle-gridlines',
|
|
157
|
+
icon: 'grid_on',
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const BAR_STYLE: CSSProperties = {
|
|
164
|
+
position: 'relative',
|
|
165
|
+
display: 'flex',
|
|
166
|
+
alignItems: 'center',
|
|
167
|
+
gap: 2,
|
|
168
|
+
padding: '2px 6px',
|
|
169
|
+
borderBottom: '1px solid var(--cs-chrome-border, #e6e9ee)',
|
|
170
|
+
background: 'var(--cs-chrome-bg, #eef1f5)',
|
|
171
|
+
flex: '0 0 auto',
|
|
172
|
+
userSelect: 'none',
|
|
173
|
+
font: 'inherit',
|
|
174
|
+
fontSize: 13,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const MENU_BTN_STYLE: CSSProperties = {
|
|
178
|
+
height: 26,
|
|
179
|
+
display: 'inline-flex',
|
|
180
|
+
alignItems: 'center',
|
|
181
|
+
padding: '0 10px',
|
|
182
|
+
border: 'none',
|
|
183
|
+
borderRadius: 6,
|
|
184
|
+
background: 'transparent',
|
|
185
|
+
cursor: 'pointer',
|
|
186
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
187
|
+
font: 'inherit',
|
|
188
|
+
fontSize: 13,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const DROPDOWN_STYLE: CSSProperties = {
|
|
192
|
+
position: 'absolute',
|
|
193
|
+
top: '100%',
|
|
194
|
+
minWidth: 200,
|
|
195
|
+
marginTop: 2,
|
|
196
|
+
padding: 4,
|
|
197
|
+
display: 'flex',
|
|
198
|
+
flexDirection: 'column',
|
|
199
|
+
border: '1px solid var(--cs-chrome-border, #e6e9ee)',
|
|
200
|
+
borderRadius: 8,
|
|
201
|
+
background: 'var(--cs-chrome-input-bg, #fff)',
|
|
202
|
+
boxShadow: '0 6px 20px rgba(0,0,0,0.16)',
|
|
203
|
+
zIndex: 1000,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const ITEM_STYLE: CSSProperties = {
|
|
207
|
+
display: 'flex',
|
|
208
|
+
alignItems: 'center',
|
|
209
|
+
gap: 10,
|
|
210
|
+
width: '100%',
|
|
211
|
+
height: 30,
|
|
212
|
+
padding: '0 10px',
|
|
213
|
+
border: 'none',
|
|
214
|
+
borderRadius: 6,
|
|
215
|
+
background: 'transparent',
|
|
216
|
+
cursor: 'pointer',
|
|
217
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
218
|
+
font: 'inherit',
|
|
219
|
+
fontSize: 13,
|
|
220
|
+
textAlign: 'left',
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export interface MenuBarProps {
|
|
224
|
+
/** Live API, or `null` until the editor is ready. */
|
|
225
|
+
api: CasualSheetsAPI | null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function MenuBar({ api }: MenuBarProps) {
|
|
229
|
+
const [open, setOpen] = useState<MenuId | null>(null);
|
|
230
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
ensureChromeFonts();
|
|
234
|
+
}, []);
|
|
235
|
+
|
|
236
|
+
// Close on Escape + on a pointerdown outside the menu bar.
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (open === null) return;
|
|
239
|
+
const onKey = (e: KeyboardEvent) => {
|
|
240
|
+
if (e.key === 'Escape') setOpen(null);
|
|
241
|
+
};
|
|
242
|
+
const onDown = (e: PointerEvent) => {
|
|
243
|
+
if (!rootRef.current?.contains(e.target as Node)) setOpen(null);
|
|
244
|
+
};
|
|
245
|
+
document.addEventListener('keydown', onKey);
|
|
246
|
+
document.addEventListener('pointerdown', onDown, true);
|
|
247
|
+
return () => {
|
|
248
|
+
document.removeEventListener('keydown', onKey);
|
|
249
|
+
document.removeEventListener('pointerdown', onDown, true);
|
|
250
|
+
};
|
|
251
|
+
}, [open]);
|
|
252
|
+
|
|
253
|
+
const run = (item: MenuItem) => {
|
|
254
|
+
setOpen(null);
|
|
255
|
+
void api?.executeCommand(item.command, item.params);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div
|
|
260
|
+
ref={rootRef}
|
|
261
|
+
style={BAR_STYLE}
|
|
262
|
+
data-testid="cs-menubar"
|
|
263
|
+
role="menubar"
|
|
264
|
+
aria-label="Menu bar"
|
|
265
|
+
>
|
|
266
|
+
{MENUS.map((menu) => {
|
|
267
|
+
const isOpen = open === menu.id;
|
|
268
|
+
return (
|
|
269
|
+
<div key={menu.id} style={{ position: 'relative' }}>
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
data-menu={menu.id}
|
|
273
|
+
aria-haspopup="menu"
|
|
274
|
+
aria-expanded={isOpen}
|
|
275
|
+
style={{
|
|
276
|
+
...MENU_BTN_STYLE,
|
|
277
|
+
background: isOpen ? 'var(--cs-chrome-active, #e6f3f7)' : 'transparent',
|
|
278
|
+
color: isOpen ? 'var(--cs-chrome-active-fg, #0e7490)' : MENU_BTN_STYLE.color,
|
|
279
|
+
}}
|
|
280
|
+
// mousedown (not click) so the grid selection isn't lost first;
|
|
281
|
+
// toggle the menu open/closed.
|
|
282
|
+
onMouseDown={(e) => {
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
setOpen((cur) => (cur === menu.id ? null : menu.id));
|
|
285
|
+
}}
|
|
286
|
+
onMouseEnter={(e) => {
|
|
287
|
+
// Hover-to-switch once a menu is already open (Office behaviour).
|
|
288
|
+
if (open !== null && open !== menu.id) setOpen(menu.id);
|
|
289
|
+
else if (!isOpen)
|
|
290
|
+
e.currentTarget.style.background = 'var(--cs-chrome-hover, rgba(0,0,0,0.06))';
|
|
291
|
+
}}
|
|
292
|
+
onMouseLeave={(e) => {
|
|
293
|
+
e.currentTarget.style.background = isOpen
|
|
294
|
+
? 'var(--cs-chrome-active, #e6f3f7)'
|
|
295
|
+
: 'transparent';
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
{menu.label}
|
|
299
|
+
</button>
|
|
300
|
+
{isOpen && (
|
|
301
|
+
<div style={DROPDOWN_STYLE} role="menu" aria-label={menu.label}>
|
|
302
|
+
{menu.items.map((item) => (
|
|
303
|
+
<button
|
|
304
|
+
key={item.id}
|
|
305
|
+
type="button"
|
|
306
|
+
role="menuitem"
|
|
307
|
+
data-testid={`cs-menuitem-${item.id}`}
|
|
308
|
+
disabled={!api}
|
|
309
|
+
style={{ ...ITEM_STYLE, opacity: api ? 1 : 0.5 }}
|
|
310
|
+
onMouseDown={(e) => {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
run(item);
|
|
313
|
+
}}
|
|
314
|
+
onMouseEnter={(e) => {
|
|
315
|
+
e.currentTarget.style.background = 'var(--cs-chrome-hover, rgba(0,0,0,0.06))';
|
|
316
|
+
}}
|
|
317
|
+
onMouseLeave={(e) => {
|
|
318
|
+
e.currentTarget.style.background = 'transparent';
|
|
319
|
+
}}
|
|
320
|
+
>
|
|
321
|
+
{item.icon ? (
|
|
322
|
+
<Icon name={item.icon} size={18} />
|
|
323
|
+
) : (
|
|
324
|
+
<span style={{ width: 18 }} aria-hidden />
|
|
325
|
+
)}
|
|
326
|
+
<span>{item.label}</span>
|
|
327
|
+
</button>
|
|
328
|
+
))}
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
})}
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NameBox — the Excel "Name Box" for `<CasualSheets chrome>`.
|
|
3
|
+
*
|
|
4
|
+
* A small editable box that mirrors the active cell's A1 reference live, and
|
|
5
|
+
* lets you navigate by typing a reference (e.g. `B5` or `A1:C3`) + Enter. Self-
|
|
6
|
+
* contained: reads the active selection through `CasualSheetsAPI` and navigates
|
|
7
|
+
* through the FUniver facade (`getRange(a1).activate()`) — no app context, no
|
|
8
|
+
* named-range support (defined names land with the rich name-box later).
|
|
9
|
+
*
|
|
10
|
+
* The orchestrator uses this to replace the static name-box span currently
|
|
11
|
+
* inside FormulaBar.tsx; this component does NOT edit FormulaBar.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect, useRef, useState, type CSSProperties, type KeyboardEvent } from 'react';
|
|
15
|
+
import { ICommandService } from '@univerjs/core';
|
|
16
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
17
|
+
import { Icon } from './Icon';
|
|
18
|
+
|
|
19
|
+
/** A defined name as surfaced by the FWorkbook facade. */
|
|
20
|
+
interface DefinedNameEntry {
|
|
21
|
+
name: string;
|
|
22
|
+
ref: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read the active workbook's defined names through the FWorkbook facade
|
|
27
|
+
* (`getDefinedNames()` → `FDefinedName[]`, each with `getName()` +
|
|
28
|
+
* `getFormulaOrRefString()`). Returns [] when the facade/API is unavailable —
|
|
29
|
+
* never invents an API.
|
|
30
|
+
*/
|
|
31
|
+
function readDefinedNames(api: CasualSheetsAPI | null): DefinedNameEntry[] {
|
|
32
|
+
if (!api) return [];
|
|
33
|
+
try {
|
|
34
|
+
const wb = api.univer.getActiveWorkbook() as unknown as {
|
|
35
|
+
getDefinedNames?: () => Array<{
|
|
36
|
+
getName?: () => string;
|
|
37
|
+
getFormulaOrRefString?: () => string;
|
|
38
|
+
}>;
|
|
39
|
+
} | null;
|
|
40
|
+
const names = wb?.getDefinedNames?.();
|
|
41
|
+
if (!names) return [];
|
|
42
|
+
return names
|
|
43
|
+
.map((dn) => ({
|
|
44
|
+
name: dn.getName?.() ?? '',
|
|
45
|
+
ref: dn.getFormulaOrRefString?.() ?? '',
|
|
46
|
+
}))
|
|
47
|
+
.filter((e) => e.name !== '');
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A1 column letters from a 0-based column index (0→A, 26→AA). */
|
|
54
|
+
function colToLetters(col: number): string {
|
|
55
|
+
let s = '';
|
|
56
|
+
let n = col;
|
|
57
|
+
do {
|
|
58
|
+
s = String.fromCharCode(65 + (n % 26)) + s;
|
|
59
|
+
n = Math.floor(n / 26) - 1;
|
|
60
|
+
} while (n >= 0);
|
|
61
|
+
return s;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The active selection's top-left A1 reference, or '' when unavailable. */
|
|
65
|
+
function readActiveRef(api: CasualSheetsAPI): string {
|
|
66
|
+
const sel = api.getSelection();
|
|
67
|
+
if (!sel) return '';
|
|
68
|
+
const { startRow, startColumn } = sel.range;
|
|
69
|
+
return colToLetters(startColumn) + (startRow + 1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const BOX_STYLE: CSSProperties = {
|
|
73
|
+
flex: '0 0 auto',
|
|
74
|
+
width: 80,
|
|
75
|
+
height: 24,
|
|
76
|
+
padding: '0 8px',
|
|
77
|
+
border: '1px solid var(--cs-chrome-border, #e6e9ee)',
|
|
78
|
+
borderRadius: 4,
|
|
79
|
+
background: 'var(--cs-chrome-input-bg, #fff)',
|
|
80
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
81
|
+
font: 'inherit',
|
|
82
|
+
fontSize: 13,
|
|
83
|
+
textAlign: 'center',
|
|
84
|
+
boxSizing: 'border-box',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const ROOT_STYLE: CSSProperties = {
|
|
88
|
+
flex: '0 0 auto',
|
|
89
|
+
display: 'inline-flex',
|
|
90
|
+
alignItems: 'center',
|
|
91
|
+
position: 'relative',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const DROPDOWN_BTN_STYLE: CSSProperties = {
|
|
95
|
+
flex: '0 0 auto',
|
|
96
|
+
width: 20,
|
|
97
|
+
height: 24,
|
|
98
|
+
marginLeft: 2,
|
|
99
|
+
display: 'inline-flex',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
border: 'none',
|
|
103
|
+
borderRadius: 4,
|
|
104
|
+
background: 'transparent',
|
|
105
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
106
|
+
cursor: 'pointer',
|
|
107
|
+
padding: 0,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const MENU_STYLE: CSSProperties = {
|
|
111
|
+
position: 'absolute',
|
|
112
|
+
top: '100%',
|
|
113
|
+
left: 0,
|
|
114
|
+
marginTop: 4,
|
|
115
|
+
zIndex: 1000,
|
|
116
|
+
minWidth: 160,
|
|
117
|
+
maxHeight: 280,
|
|
118
|
+
overflowY: 'auto',
|
|
119
|
+
padding: 4,
|
|
120
|
+
borderRadius: 8,
|
|
121
|
+
border: '1px solid var(--cs-chrome-border, #e6e9ee)',
|
|
122
|
+
background: 'var(--cs-chrome-input-bg, #fff)',
|
|
123
|
+
boxShadow: '0 6px 20px rgba(0,0,0,0.18)',
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const MENU_ITEM_STYLE: CSSProperties = {
|
|
127
|
+
display: 'flex',
|
|
128
|
+
alignItems: 'baseline',
|
|
129
|
+
justifyContent: 'space-between',
|
|
130
|
+
gap: 12,
|
|
131
|
+
width: '100%',
|
|
132
|
+
height: 28,
|
|
133
|
+
padding: '0 8px',
|
|
134
|
+
border: 'none',
|
|
135
|
+
borderRadius: 6,
|
|
136
|
+
background: 'transparent',
|
|
137
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
138
|
+
font: 'inherit',
|
|
139
|
+
fontSize: 13,
|
|
140
|
+
textAlign: 'left',
|
|
141
|
+
cursor: 'pointer',
|
|
142
|
+
boxSizing: 'border-box',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const MENU_EMPTY_STYLE: CSSProperties = {
|
|
146
|
+
padding: '6px 8px',
|
|
147
|
+
color: 'var(--cs-chrome-muted, #8a8886)',
|
|
148
|
+
fontSize: 12,
|
|
149
|
+
whiteSpace: 'nowrap',
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const ITEM_REF_STYLE: CSSProperties = {
|
|
153
|
+
color: 'var(--cs-chrome-muted, #8a8886)',
|
|
154
|
+
fontSize: 11,
|
|
155
|
+
flex: '0 0 auto',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export interface NameBoxProps {
|
|
159
|
+
/** Live API, or `null` until the editor is ready. */
|
|
160
|
+
api: CasualSheetsAPI | null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function NameBox({ api }: NameBoxProps) {
|
|
164
|
+
// The A1 reference of the active cell.
|
|
165
|
+
const [ref, setRef] = useState('');
|
|
166
|
+
// null = mirror the active cell; a string = the user is typing.
|
|
167
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
168
|
+
const draftRef = useRef<string | null>(null);
|
|
169
|
+
draftRef.current = draft;
|
|
170
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
171
|
+
// The defined-names dropdown: open state, current entries, and a root ref for
|
|
172
|
+
// outside-pointerdown detection (mirrors ColorPicker's popover pattern).
|
|
173
|
+
const [open, setOpen] = useState(false);
|
|
174
|
+
const [names, setNames] = useState<DefinedNameEntry[]>([]);
|
|
175
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
176
|
+
|
|
177
|
+
// Reflect the active cell: subscribe to command activity (covers selection
|
|
178
|
+
// moves) and re-read the reference — unless the user is mid-edit.
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!api) return;
|
|
181
|
+
const refresh = () => {
|
|
182
|
+
if (draftRef.current !== null) return;
|
|
183
|
+
setRef(readActiveRef(api));
|
|
184
|
+
};
|
|
185
|
+
refresh();
|
|
186
|
+
const injector = (api.univer as unknown as { _injector?: { get(t: unknown): unknown } })
|
|
187
|
+
._injector;
|
|
188
|
+
const cmd = injector?.get(ICommandService) as
|
|
189
|
+
| { onCommandExecuted: (cb: () => void) => { dispose: () => void } }
|
|
190
|
+
| undefined;
|
|
191
|
+
const sub = cmd?.onCommandExecuted(() => refresh());
|
|
192
|
+
return () => sub?.dispose();
|
|
193
|
+
}, [api]);
|
|
194
|
+
|
|
195
|
+
// While the dropdown is open, close it on Escape or any outside pointerdown.
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (!open) return;
|
|
198
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
199
|
+
if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
|
|
200
|
+
};
|
|
201
|
+
const onKeyDown = (e: globalThis.KeyboardEvent) => {
|
|
202
|
+
if (e.key === 'Escape') setOpen(false);
|
|
203
|
+
};
|
|
204
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
205
|
+
document.addEventListener('keydown', onKeyDown);
|
|
206
|
+
return () => {
|
|
207
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
208
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
209
|
+
};
|
|
210
|
+
}, [open]);
|
|
211
|
+
|
|
212
|
+
/** Navigate to the typed A1 reference / range. Invalid input is ignored. */
|
|
213
|
+
const navigate = (text: string) => {
|
|
214
|
+
const t = text.trim();
|
|
215
|
+
setDraft(null);
|
|
216
|
+
if (!api || t === '') {
|
|
217
|
+
setRef(readActiveRef(api ?? ({} as CasualSheetsAPI)));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
|
|
221
|
+
if (!sheet) return;
|
|
222
|
+
try {
|
|
223
|
+
// FRange.getRange parses an A1 string ("B5") or range ("A1:C3").
|
|
224
|
+
const range = (sheet as unknown as { getRange(a1: string): { activate(): void } }).getRange(
|
|
225
|
+
t,
|
|
226
|
+
);
|
|
227
|
+
range.activate();
|
|
228
|
+
} catch {
|
|
229
|
+
// Invalid reference — fall back to the current selection.
|
|
230
|
+
setRef(readActiveRef(api));
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Open the dropdown: refresh the defined-name list from the live workbook,
|
|
235
|
+
// then toggle. mousedown (not click) so the grid selection isn't lost first.
|
|
236
|
+
const toggleDropdown = () => {
|
|
237
|
+
setOpen((cur) => {
|
|
238
|
+
const next = !cur;
|
|
239
|
+
if (next) setNames(readDefinedNames(api));
|
|
240
|
+
return next;
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Navigate to a defined name's range. The ref may carry a sheet prefix
|
|
245
|
+
// ("Sheet1!$A$1") and absolute `$` markers — strip both, then reuse the same
|
|
246
|
+
// getRange(...).activate() path the typed-reference navigation uses.
|
|
247
|
+
const navigateName = (entry: DefinedNameEntry) => {
|
|
248
|
+
setOpen(false);
|
|
249
|
+
const raw = entry.ref.trim();
|
|
250
|
+
if (raw === '') return;
|
|
251
|
+
const afterSheet = raw.includes('!') ? raw.slice(raw.lastIndexOf('!') + 1) : raw;
|
|
252
|
+
const a1 = afterSheet.replace(/\$/g, '');
|
|
253
|
+
navigate(a1);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
257
|
+
if (e.key === 'Enter') {
|
|
258
|
+
e.preventDefault();
|
|
259
|
+
navigate((e.target as HTMLInputElement).value);
|
|
260
|
+
(e.target as HTMLInputElement).blur();
|
|
261
|
+
} else if (e.key === 'Escape') {
|
|
262
|
+
e.preventDefault();
|
|
263
|
+
setDraft(null);
|
|
264
|
+
setRef(api ? readActiveRef(api) : '');
|
|
265
|
+
(e.target as HTMLInputElement).blur();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const shown = draft ?? ref;
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div ref={rootRef} style={ROOT_STYLE}>
|
|
273
|
+
<input
|
|
274
|
+
ref={inputRef}
|
|
275
|
+
type="text"
|
|
276
|
+
aria-label="Name box"
|
|
277
|
+
title="Name box"
|
|
278
|
+
data-testid="cs-namebox-input"
|
|
279
|
+
style={BOX_STYLE}
|
|
280
|
+
value={shown}
|
|
281
|
+
disabled={!api}
|
|
282
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
283
|
+
onKeyDown={onKeyDown}
|
|
284
|
+
onFocus={(e) => e.currentTarget.select()}
|
|
285
|
+
onBlur={() => {
|
|
286
|
+
// Abandon any uncommitted edit without navigating.
|
|
287
|
+
if (draftRef.current !== null) {
|
|
288
|
+
setDraft(null);
|
|
289
|
+
setRef(api ? readActiveRef(api) : '');
|
|
290
|
+
}
|
|
291
|
+
}}
|
|
292
|
+
/>
|
|
293
|
+
<button
|
|
294
|
+
type="button"
|
|
295
|
+
title="Defined names"
|
|
296
|
+
aria-label="Defined names"
|
|
297
|
+
aria-haspopup="true"
|
|
298
|
+
aria-expanded={open}
|
|
299
|
+
data-testid="cs-namebox-dropdown"
|
|
300
|
+
style={{
|
|
301
|
+
...DROPDOWN_BTN_STYLE,
|
|
302
|
+
background: open ? 'var(--cs-chrome-active, #e6f3f7)' : 'transparent',
|
|
303
|
+
color: open ? 'var(--cs-chrome-active-fg, #0e7490)' : DROPDOWN_BTN_STYLE.color,
|
|
304
|
+
}}
|
|
305
|
+
disabled={!api}
|
|
306
|
+
// mousedown (not click) so the grid's selection isn't lost first.
|
|
307
|
+
onMouseDown={(e) => {
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
toggleDropdown();
|
|
310
|
+
}}
|
|
311
|
+
>
|
|
312
|
+
<Icon name="expand_more" size={18} />
|
|
313
|
+
</button>
|
|
314
|
+
{open && (
|
|
315
|
+
<div style={MENU_STYLE} role="menu" data-testid="cs-namebox-menu">
|
|
316
|
+
{names.length === 0 ? (
|
|
317
|
+
<div style={MENU_EMPTY_STYLE}>No names</div>
|
|
318
|
+
) : (
|
|
319
|
+
names.map((entry, i) => (
|
|
320
|
+
<button
|
|
321
|
+
key={entry.name}
|
|
322
|
+
type="button"
|
|
323
|
+
role="menuitem"
|
|
324
|
+
title={entry.ref || entry.name}
|
|
325
|
+
data-testid={`cs-namebox-name-${i}`}
|
|
326
|
+
style={MENU_ITEM_STYLE}
|
|
327
|
+
onMouseDown={(e) => {
|
|
328
|
+
e.preventDefault();
|
|
329
|
+
navigateName(entry);
|
|
330
|
+
}}
|
|
331
|
+
onMouseEnter={(e) => {
|
|
332
|
+
e.currentTarget.style.background = 'var(--cs-chrome-hover, rgba(0,0,0,0.06))';
|
|
333
|
+
}}
|
|
334
|
+
onMouseLeave={(e) => {
|
|
335
|
+
e.currentTarget.style.background = 'transparent';
|
|
336
|
+
}}
|
|
337
|
+
>
|
|
338
|
+
<span>{entry.name}</span>
|
|
339
|
+
{entry.ref && <span style={ITEM_REF_STYLE}>{entry.ref}</span>}
|
|
340
|
+
</button>
|
|
341
|
+
))
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
}
|