@casualoffice/sheets 0.17.0 → 0.19.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/chrome.cjs +3339 -244
- package/dist/chrome.cjs.map +1 -1
- package/dist/chrome.js +3298 -203
- package/dist/chrome.js.map +1 -1
- package/dist/embed/embed-runtime.js +203 -157
- package/dist/index.cjs +5971 -1572
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5961 -1548
- package/dist/index.js.map +1 -1
- package/dist/sheets.cjs +4937 -538
- package/dist/sheets.cjs.map +1 -1
- package/dist/sheets.js +4945 -532
- package/dist/sheets.js.map +1 -1
- package/package.json +8 -7
- package/src/charts/ChartContextMenu.tsx +264 -0
- package/src/charts/ChartLayer.tsx +333 -0
- package/src/charts/ChartOverlay.tsx +293 -0
- package/src/charts/ChartsPanel.tsx +211 -0
- package/src/charts/FormatChartDialog.tsx +419 -0
- package/src/charts/InsertChartDialog.tsx +478 -0
- package/src/charts/build-option.ts +476 -0
- package/src/charts/charts-context.tsx +205 -0
- package/src/charts/echarts-init.ts +58 -0
- package/src/charts/hit-test.ts +130 -0
- package/src/charts/insert-chart.ts +106 -0
- package/src/charts/naming.ts +38 -0
- package/src/charts/render-to-png.ts +117 -0
- package/src/charts/resources.ts +108 -0
- package/src/charts/types.ts +239 -0
- package/src/charts/univer-dom.ts +102 -0
- package/src/chrome/CommentsPanel.tsx +427 -0
- package/src/chrome/ConditionalFormattingDialog.tsx +534 -0
- package/src/chrome/CustomSortDialog.tsx +357 -0
- package/src/chrome/DataValidationDialog.tsx +536 -0
- package/src/chrome/DeleteCellsDialog.tsx +183 -0
- package/src/chrome/GoalSeekDialog.tsx +370 -0
- package/src/chrome/HistoryPanel.tsx +319 -0
- package/src/chrome/InsertCellsDialog.tsx +185 -0
- package/src/chrome/InsertChartDialog.tsx +490 -0
- package/src/chrome/InsertFunctionDialog.tsx +493 -0
- package/src/chrome/InsertPivotDialog.tsx +488 -0
- package/src/chrome/InsertSparklineDialog.tsx +344 -0
- package/src/chrome/NameManagerDialog.tsx +378 -0
- package/src/chrome/PanelHost.tsx +55 -0
- package/src/chrome/PanelRail.tsx +90 -0
- package/src/chrome/PasteSpecialDialog.tsx +286 -0
- package/src/chrome/PivotFieldsPanel.tsx +1052 -0
- package/src/chrome/TablesPanel.tsx +301 -0
- package/src/chrome/dialog-context.tsx +24 -0
- package/src/chrome/panel-context.tsx +55 -0
- package/src/chrome/panel-registry.ts +48 -0
- package/src/sheets/CasualSheets.tsx +60 -34
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* InsertChartDialog — the SDK chrome's built-in Insert Chart modal.
|
|
19
|
+
*
|
|
20
|
+
* Reads the active A1 selection off the FUniver facade, lets the user pick a
|
|
21
|
+
* chart type (column / bar / line / pie) and confirm the source range, then
|
|
22
|
+
* inserts a chart over the grid.
|
|
23
|
+
*
|
|
24
|
+
* Facade grounding / capability note
|
|
25
|
+
* ----------------------------------
|
|
26
|
+
* Univer 0.25 (the version pinned in this SDK's peerDependencies) ships NO
|
|
27
|
+
* `@univerjs/*chart*` package — there is no `FUniver.newChart()` builder and no
|
|
28
|
+
* `FRange.insertChart()` the way `@univerjs/sheets-data-validation` gives us
|
|
29
|
+
* `newDataValidation()` / `setDataValidation()`. (Verified: no chart package
|
|
30
|
+
* under node_modules/@univerjs, and none listed in the SDK peerDependencies.)
|
|
31
|
+
*
|
|
32
|
+
* The closest REAL, installed facade capability for placing a positioned visual
|
|
33
|
+
* artifact over the grid is the float-DOM API contributed by
|
|
34
|
+
* `@univerjs/sheets-drawing-ui/facade` — the same mechanism Univer's own chart
|
|
35
|
+
* and image plugins use to anchor content over cells:
|
|
36
|
+
* - `univerAPI.registerComponent(key, Component)` (@univerjs/ui f-univer.d.ts L390)
|
|
37
|
+
* - `FWorksheet.addFloatDomToRange(range, { componentKey, data }, domLayout, id)`
|
|
38
|
+
* (@univerjs/sheets-drawing-ui/lib/types/facade/f-worksheet.d.ts L305; the
|
|
39
|
+
* `declare module '@univerjs/sheets/facade' { interface FWorksheet … }`
|
|
40
|
+
* augmentation at L428 is what puts the method on the live sheet)
|
|
41
|
+
*
|
|
42
|
+
* So on Apply we: register a small self-contained SVG chart-preview component
|
|
43
|
+
* (once), read the selected range's numeric values off the SDK snapshot, and
|
|
44
|
+
* insert a float DOM over the selection rendering that preview for the chosen
|
|
45
|
+
* type. This is a genuine facade insertion (real, disposable, positioned),
|
|
46
|
+
* reads the real selection data, and closes.
|
|
47
|
+
*
|
|
48
|
+
* LIMITATION (recorded): without a chart engine in the SDK's dep tree, the
|
|
49
|
+
* inserted artifact is a static, computed SVG preview of the selected values —
|
|
50
|
+
* not a live, re-calculating, fully-styled chart series. When a host installs a
|
|
51
|
+
* real Univer chart plugin it can OVERRIDE this dialog by registering its own
|
|
52
|
+
* `insert-chart` extension (extensions.dialogs['insert-chart']); the built-in is
|
|
53
|
+
* the always-available default.
|
|
54
|
+
*
|
|
55
|
+
* Mounted by `<DialogHost>` when `openDialog('insert-chart')` is called and no
|
|
56
|
+
* host override is registered.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import { useMemo, useState, type CSSProperties, type ReactNode } from 'react';
|
|
60
|
+
// Side-effect import: installs `FWorksheet.addFloatDomToRange` (+ the other
|
|
61
|
+
// float-dom methods) on the facade prototype AND augments
|
|
62
|
+
// `@univerjs/sheets/facade`'s FWorksheet at type-check time. Without it, the
|
|
63
|
+
// method is undefined at runtime. Mirrors DataValidationDialog's
|
|
64
|
+
// `@univerjs/sheets-data-validation/facade` side-effect import.
|
|
65
|
+
import '@univerjs/sheets-drawing-ui/facade';
|
|
66
|
+
import type { DialogComponentProps } from './extensions';
|
|
67
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
68
|
+
import { Dialog } from './Dialog';
|
|
69
|
+
import {
|
|
70
|
+
DIALOG_BTN_PRIMARY_STYLE,
|
|
71
|
+
DIALOG_BTN_SECONDARY_STYLE,
|
|
72
|
+
DIALOG_FIELD_STYLE,
|
|
73
|
+
DIALOG_INPUT_STYLE,
|
|
74
|
+
DIALOG_LABEL_STYLE,
|
|
75
|
+
} from './dialog-styles';
|
|
76
|
+
|
|
77
|
+
/** Chart families the dialog can insert. */
|
|
78
|
+
type ChartType = 'column' | 'bar' | 'line' | 'pie';
|
|
79
|
+
|
|
80
|
+
const CHART_TYPE_OPTIONS: Array<{ value: ChartType; label: string }> = [
|
|
81
|
+
{ value: 'column', label: 'Column' },
|
|
82
|
+
{ value: 'bar', label: 'Bar' },
|
|
83
|
+
{ value: 'line', label: 'Line' },
|
|
84
|
+
{ value: 'pie', label: 'Pie' },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/** Stable component key we register the preview under (once per FUniver). */
|
|
88
|
+
const CHART_COMPONENT_KEY = 'cs-builtin-chart-preview';
|
|
89
|
+
|
|
90
|
+
/** Palette for the preview series (kept small + colour-blind-friendly-ish). */
|
|
91
|
+
const PALETTE = ['#0e7490', '#f59e0b', '#10b981', '#ef4444', '#6366f1', '#ec4899'];
|
|
92
|
+
|
|
93
|
+
interface DialogState {
|
|
94
|
+
chartType: ChartType;
|
|
95
|
+
/** A1 range the chart reads from (seeded from the live selection). */
|
|
96
|
+
rangeA1: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The active FRange, or null when there is no selection. */
|
|
100
|
+
function activeRange(api: CasualSheetsAPI) {
|
|
101
|
+
return api.univer.getActiveWorkbook()?.getActiveSheet()?.getActiveRange() ?? null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The active FWorksheet, or null. */
|
|
105
|
+
function activeSheet(api: CasualSheetsAPI) {
|
|
106
|
+
return api.univer.getActiveWorkbook()?.getActiveSheet() ?? null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Pull the numeric values out of the selected range off the SDK snapshot (the
|
|
111
|
+
* same read path FormatCellsDialog uses to seed itself). Flattened + coerced;
|
|
112
|
+
* non-numeric cells drop out. Returns [] when nothing usable is present.
|
|
113
|
+
*/
|
|
114
|
+
function readRangeNumbers(api: CasualSheetsAPI): number[] {
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
const snap = api.getSnapshot() as any;
|
|
117
|
+
const sel = api.getSelection();
|
|
118
|
+
if (!snap || !sel) return [];
|
|
119
|
+
const sheet = snap.sheets?.[sel.sheetId];
|
|
120
|
+
if (!sheet) return [];
|
|
121
|
+
const { startRow, endRow, startColumn, endColumn } = sel.range;
|
|
122
|
+
const out: number[] = [];
|
|
123
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
124
|
+
for (let c = startColumn; c <= endColumn; c++) {
|
|
125
|
+
const raw = sheet.cellData?.[r]?.[c]?.v;
|
|
126
|
+
const n = typeof raw === 'number' ? raw : Number(raw);
|
|
127
|
+
if (Number.isFinite(n)) out.push(n);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Register the SVG chart-preview component on the FUniver facade if it hasn't
|
|
135
|
+
* been registered yet. Idempotent per FUniver instance: we stash a flag on the
|
|
136
|
+
* facade object so re-opening the dialog doesn't re-register (registerComponent
|
|
137
|
+
* returns a disposable, but we intentionally keep the registration alive for the
|
|
138
|
+
* lifetime of the editor so previously-inserted charts keep rendering).
|
|
139
|
+
*/
|
|
140
|
+
function ensureChartComponent(api: CasualSheetsAPI): void {
|
|
141
|
+
const uni = api.univer as unknown as {
|
|
142
|
+
registerComponent?: (key: string, comp: unknown) => { dispose(): void };
|
|
143
|
+
__csChartRegistered?: boolean;
|
|
144
|
+
};
|
|
145
|
+
if (uni.__csChartRegistered || typeof uni.registerComponent !== 'function') return;
|
|
146
|
+
uni.registerComponent(CHART_COMPONENT_KEY, ChartPreview);
|
|
147
|
+
uni.__csChartRegistered = true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Insert the chart over the current selection via the drawing-ui float-DOM
|
|
152
|
+
* facade. Returns false when there's no selection or the facade method is
|
|
153
|
+
* missing (host didn't register the drawing-ui plugin). Reads the range's
|
|
154
|
+
* numbers and hands them to the preview component as `data`.
|
|
155
|
+
*/
|
|
156
|
+
function insertChart(api: CasualSheetsAPI, s: DialogState): boolean {
|
|
157
|
+
const range = activeRange(api);
|
|
158
|
+
const sheet = activeSheet(api);
|
|
159
|
+
if (!range || !sheet) return false;
|
|
160
|
+
|
|
161
|
+
ensureChartComponent(api);
|
|
162
|
+
|
|
163
|
+
const values = readRangeNumbers(api);
|
|
164
|
+
|
|
165
|
+
// `addFloatDomToRange` is contributed by @univerjs/sheets-drawing-ui/facade
|
|
166
|
+
// (f-worksheet.d.ts L305). Typed loosely at the call site so the SDK doesn't
|
|
167
|
+
// hard-depend on the ambient FWorksheet augmentation here.
|
|
168
|
+
const domSheet = sheet as unknown as {
|
|
169
|
+
addFloatDomToRange?: (
|
|
170
|
+
range: unknown,
|
|
171
|
+
layer: { componentKey: string; data?: unknown; allowTransform?: boolean },
|
|
172
|
+
domLayout: {
|
|
173
|
+
width?: number;
|
|
174
|
+
height?: number;
|
|
175
|
+
marginX?: number | string;
|
|
176
|
+
marginY?: number | string;
|
|
177
|
+
},
|
|
178
|
+
id?: string,
|
|
179
|
+
) => { id: string; dispose: () => void } | null;
|
|
180
|
+
};
|
|
181
|
+
if (typeof domSheet.addFloatDomToRange !== 'function') return false;
|
|
182
|
+
|
|
183
|
+
const result = domSheet.addFloatDomToRange(
|
|
184
|
+
range,
|
|
185
|
+
{
|
|
186
|
+
componentKey: CHART_COMPONENT_KEY,
|
|
187
|
+
allowTransform: true,
|
|
188
|
+
data: { type: s.chartType, values, rangeA1: s.rangeA1 },
|
|
189
|
+
},
|
|
190
|
+
{ width: 360, height: 240, marginX: 0, marginY: 0 },
|
|
191
|
+
`cs-chart-${Date.now()}`,
|
|
192
|
+
);
|
|
193
|
+
return result != null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Self-contained SVG chart preview rendered inside the float DOM. Univer passes
|
|
198
|
+
* the `layer.data` object through as the component's `data` prop. Deliberately
|
|
199
|
+
* dependency-free (no echarts) so the SDK bundle stays lean and the built-in
|
|
200
|
+
* works without any host chart engine.
|
|
201
|
+
*/
|
|
202
|
+
function ChartPreview({
|
|
203
|
+
data,
|
|
204
|
+
}: {
|
|
205
|
+
data?: { type?: ChartType; values?: number[]; rangeA1?: string };
|
|
206
|
+
}) {
|
|
207
|
+
const type = data?.type ?? 'column';
|
|
208
|
+
const values = (data?.values ?? []).slice(0, 24);
|
|
209
|
+
const label = data?.rangeA1 ?? '';
|
|
210
|
+
|
|
211
|
+
const wrapStyle: CSSProperties = {
|
|
212
|
+
width: '100%',
|
|
213
|
+
height: '100%',
|
|
214
|
+
boxSizing: 'border-box',
|
|
215
|
+
background: '#ffffff',
|
|
216
|
+
border: '1px solid #cdd3db',
|
|
217
|
+
borderRadius: 6,
|
|
218
|
+
padding: 8,
|
|
219
|
+
display: 'flex',
|
|
220
|
+
flexDirection: 'column',
|
|
221
|
+
font: '12px system-ui, sans-serif',
|
|
222
|
+
color: '#201f1e',
|
|
223
|
+
overflow: 'hidden',
|
|
224
|
+
};
|
|
225
|
+
const titleStyle: CSSProperties = {
|
|
226
|
+
fontWeight: 600,
|
|
227
|
+
marginBottom: 4,
|
|
228
|
+
whiteSpace: 'nowrap',
|
|
229
|
+
overflow: 'hidden',
|
|
230
|
+
textOverflow: 'ellipsis',
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const W = 340;
|
|
234
|
+
const H = 176;
|
|
235
|
+
const max = values.length ? Math.max(...values, 0) : 0;
|
|
236
|
+
const min = values.length ? Math.min(...values, 0) : 0;
|
|
237
|
+
const span = max - min || 1;
|
|
238
|
+
|
|
239
|
+
let body: ReactNode;
|
|
240
|
+
if (values.length === 0) {
|
|
241
|
+
body = (
|
|
242
|
+
<text x={W / 2} y={H / 2} textAnchor="middle" fill="#605e5c">
|
|
243
|
+
No numeric data in range
|
|
244
|
+
</text>
|
|
245
|
+
);
|
|
246
|
+
} else if (type === 'pie') {
|
|
247
|
+
const total = values.reduce((a, v) => a + Math.abs(v), 0) || 1;
|
|
248
|
+
const cx = W / 2;
|
|
249
|
+
const cy = H / 2;
|
|
250
|
+
const rad = Math.min(W, H) / 2 - 6;
|
|
251
|
+
let angle = -Math.PI / 2;
|
|
252
|
+
body = (
|
|
253
|
+
<>
|
|
254
|
+
{values.map((v, i) => {
|
|
255
|
+
const frac = Math.abs(v) / total;
|
|
256
|
+
const next = angle + frac * Math.PI * 2;
|
|
257
|
+
const x1 = cx + rad * Math.cos(angle);
|
|
258
|
+
const y1 = cy + rad * Math.sin(angle);
|
|
259
|
+
const x2 = cx + rad * Math.cos(next);
|
|
260
|
+
const y2 = cy + rad * Math.sin(next);
|
|
261
|
+
const large = frac > 0.5 ? 1 : 0;
|
|
262
|
+
const d = `M ${cx} ${cy} L ${x1} ${y1} A ${rad} ${rad} 0 ${large} 1 ${x2} ${y2} Z`;
|
|
263
|
+
angle = next;
|
|
264
|
+
return <path key={i} d={d} fill={PALETTE[i % PALETTE.length]} />;
|
|
265
|
+
})}
|
|
266
|
+
</>
|
|
267
|
+
);
|
|
268
|
+
} else if (type === 'line') {
|
|
269
|
+
const step = values.length > 1 ? W / (values.length - 1) : W;
|
|
270
|
+
const pts = values.map((v, i) => `${i * step},${H - ((v - min) / span) * H}`).join(' ');
|
|
271
|
+
body = <polyline points={pts} fill="none" stroke={PALETTE[0]} strokeWidth={2} />;
|
|
272
|
+
} else {
|
|
273
|
+
// column (vertical) or bar (horizontal)
|
|
274
|
+
const horizontal = type === 'bar';
|
|
275
|
+
const n = values.length;
|
|
276
|
+
const gap = 4;
|
|
277
|
+
body = (
|
|
278
|
+
<>
|
|
279
|
+
{values.map((v, i) => {
|
|
280
|
+
const fill = PALETTE[i % PALETTE.length];
|
|
281
|
+
if (horizontal) {
|
|
282
|
+
const bh = H / n - gap;
|
|
283
|
+
const bw = ((v - min) / span) * W;
|
|
284
|
+
return (
|
|
285
|
+
<rect
|
|
286
|
+
key={i}
|
|
287
|
+
x={0}
|
|
288
|
+
y={i * (bh + gap)}
|
|
289
|
+
width={Math.max(bw, 1)}
|
|
290
|
+
height={bh}
|
|
291
|
+
fill={fill}
|
|
292
|
+
/>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const bw = W / n - gap;
|
|
296
|
+
const bh = ((v - min) / span) * H;
|
|
297
|
+
return (
|
|
298
|
+
<rect
|
|
299
|
+
key={i}
|
|
300
|
+
x={i * (bw + gap)}
|
|
301
|
+
y={H - bh}
|
|
302
|
+
width={bw}
|
|
303
|
+
height={Math.max(bh, 1)}
|
|
304
|
+
fill={fill}
|
|
305
|
+
/>
|
|
306
|
+
);
|
|
307
|
+
})}
|
|
308
|
+
</>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div style={wrapStyle}>
|
|
314
|
+
{label && (
|
|
315
|
+
<div style={titleStyle}>{`${type[0].toUpperCase()}${type.slice(1)} chart · ${label}`}</div>
|
|
316
|
+
)}
|
|
317
|
+
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
|
|
318
|
+
{body}
|
|
319
|
+
</svg>
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const RANGE_NOTE_STYLE: CSSProperties = {
|
|
325
|
+
fontSize: 12,
|
|
326
|
+
color: 'var(--cs-chrome-muted, #605e5c)',
|
|
327
|
+
marginBottom: 12,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const TYPE_ROW_STYLE: CSSProperties = {
|
|
331
|
+
display: 'grid',
|
|
332
|
+
gridTemplateColumns: 'repeat(4, 1fr)',
|
|
333
|
+
gap: 8,
|
|
334
|
+
marginBottom: 12,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
function typeCardStyle(selected: boolean): CSSProperties {
|
|
338
|
+
return {
|
|
339
|
+
display: 'flex',
|
|
340
|
+
flexDirection: 'column',
|
|
341
|
+
alignItems: 'center',
|
|
342
|
+
gap: 6,
|
|
343
|
+
padding: '10px 4px',
|
|
344
|
+
border: `1px solid ${selected ? 'var(--cs-chrome-active-fg, #0e7490)' : 'var(--cs-chrome-border, #cdd3db)'}`,
|
|
345
|
+
borderRadius: 8,
|
|
346
|
+
background: selected
|
|
347
|
+
? 'var(--cs-chrome-active-bg, #ecfeff)'
|
|
348
|
+
: 'var(--cs-chrome-input-bg, #fff)',
|
|
349
|
+
color: selected ? 'var(--cs-chrome-active-fg, #0e7490)' : 'var(--cs-chrome-fg, #201f1e)',
|
|
350
|
+
font: 'inherit',
|
|
351
|
+
fontSize: 12,
|
|
352
|
+
cursor: 'pointer',
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Tiny inline SVG glyph per chart type — no icon-font dependency. */
|
|
357
|
+
function TypeGlyph({ type }: { type: ChartType }) {
|
|
358
|
+
const c = 'currentColor';
|
|
359
|
+
switch (type) {
|
|
360
|
+
case 'column':
|
|
361
|
+
return (
|
|
362
|
+
<svg width={22} height={22} viewBox="0 0 22 22" fill={c} aria-hidden="true">
|
|
363
|
+
<rect x={2} y={10} width={4} height={10} />
|
|
364
|
+
<rect x={9} y={5} width={4} height={15} />
|
|
365
|
+
<rect x={16} y={2} width={4} height={18} />
|
|
366
|
+
</svg>
|
|
367
|
+
);
|
|
368
|
+
case 'bar':
|
|
369
|
+
return (
|
|
370
|
+
<svg width={22} height={22} viewBox="0 0 22 22" fill={c} aria-hidden="true">
|
|
371
|
+
<rect x={2} y={2} width={10} height={4} />
|
|
372
|
+
<rect x={2} y={9} width={16} height={4} />
|
|
373
|
+
<rect x={2} y={16} width={7} height={4} />
|
|
374
|
+
</svg>
|
|
375
|
+
);
|
|
376
|
+
case 'line':
|
|
377
|
+
return (
|
|
378
|
+
<svg
|
|
379
|
+
width={22}
|
|
380
|
+
height={22}
|
|
381
|
+
viewBox="0 0 22 22"
|
|
382
|
+
fill="none"
|
|
383
|
+
stroke={c}
|
|
384
|
+
strokeWidth={2}
|
|
385
|
+
aria-hidden="true"
|
|
386
|
+
>
|
|
387
|
+
<polyline points="2,16 8,9 13,13 20,3" />
|
|
388
|
+
</svg>
|
|
389
|
+
);
|
|
390
|
+
case 'pie':
|
|
391
|
+
return (
|
|
392
|
+
<svg width={22} height={22} viewBox="0 0 22 22" fill={c} aria-hidden="true">
|
|
393
|
+
<path d="M11 11 L11 2 A9 9 0 0 1 20 11 Z" />
|
|
394
|
+
<circle cx={11} cy={11} r={9} fill="none" stroke={c} strokeWidth={1.5} />
|
|
395
|
+
</svg>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function InsertChartDialog({ api, onClose }: DialogComponentProps) {
|
|
401
|
+
// Read the selection once for the header hint + as the seed source range.
|
|
402
|
+
// `getA1Notation` off the live FRange (verified in @univerjs/sheets/facade
|
|
403
|
+
// f-range.d.ts) gives the user-facing A1 label, e.g. "A1:B5".
|
|
404
|
+
const rangeLabel = useMemo(() => {
|
|
405
|
+
const fRange = activeRange(api) as unknown as { getA1Notation?: () => string } | null;
|
|
406
|
+
return fRange?.getA1Notation?.() ?? null;
|
|
407
|
+
}, [api]);
|
|
408
|
+
|
|
409
|
+
const [state, setState] = useState<DialogState>(() => ({
|
|
410
|
+
chartType: 'column',
|
|
411
|
+
rangeA1: rangeLabel ?? '',
|
|
412
|
+
}));
|
|
413
|
+
|
|
414
|
+
const hasSelection = activeRange(api) !== null;
|
|
415
|
+
|
|
416
|
+
const update = <K extends keyof DialogState>(key: K, value: DialogState[K]) =>
|
|
417
|
+
setState((prev) => ({ ...prev, [key]: value }));
|
|
418
|
+
|
|
419
|
+
const apply = () => {
|
|
420
|
+
if (insertChart(api, state)) onClose();
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<Dialog
|
|
425
|
+
title="Insert chart"
|
|
426
|
+
onClose={onClose}
|
|
427
|
+
width={440}
|
|
428
|
+
data-testid="cs-insert-chart-dialog"
|
|
429
|
+
footer={
|
|
430
|
+
<>
|
|
431
|
+
<button type="button" style={DIALOG_BTN_SECONDARY_STYLE} onClick={onClose}>
|
|
432
|
+
Cancel
|
|
433
|
+
</button>
|
|
434
|
+
<button
|
|
435
|
+
type="button"
|
|
436
|
+
style={DIALOG_BTN_PRIMARY_STYLE}
|
|
437
|
+
data-testid="cs-insert-chart-apply"
|
|
438
|
+
disabled={!hasSelection}
|
|
439
|
+
onClick={apply}
|
|
440
|
+
>
|
|
441
|
+
Insert
|
|
442
|
+
</button>
|
|
443
|
+
</>
|
|
444
|
+
}
|
|
445
|
+
>
|
|
446
|
+
{hasSelection ? (
|
|
447
|
+
<div style={RANGE_NOTE_STYLE} data-testid="cs-insert-chart-range">
|
|
448
|
+
Chart from <strong>{rangeLabel ?? 'the current selection'}</strong>
|
|
449
|
+
</div>
|
|
450
|
+
) : (
|
|
451
|
+
<div style={RANGE_NOTE_STYLE} data-testid="cs-insert-chart-no-selection">
|
|
452
|
+
Select the data range first, then reopen this dialog.
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
<div style={DIALOG_LABEL_STYLE}>Chart type</div>
|
|
457
|
+
<div style={TYPE_ROW_STYLE} role="radiogroup" aria-label="Chart type">
|
|
458
|
+
{CHART_TYPE_OPTIONS.map((opt) => (
|
|
459
|
+
<button
|
|
460
|
+
key={opt.value}
|
|
461
|
+
type="button"
|
|
462
|
+
role="radio"
|
|
463
|
+
aria-checked={state.chartType === opt.value}
|
|
464
|
+
data-testid={`cs-insert-chart-type-${opt.value}`}
|
|
465
|
+
style={typeCardStyle(state.chartType === opt.value)}
|
|
466
|
+
onClick={() => update('chartType', opt.value)}
|
|
467
|
+
>
|
|
468
|
+
<TypeGlyph type={opt.value} />
|
|
469
|
+
<span>{opt.label}</span>
|
|
470
|
+
</button>
|
|
471
|
+
))}
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
475
|
+
<span style={DIALOG_LABEL_STYLE}>Data range</span>
|
|
476
|
+
<input
|
|
477
|
+
style={DIALOG_INPUT_STYLE}
|
|
478
|
+
data-testid="cs-insert-chart-range-input"
|
|
479
|
+
value={state.rangeA1}
|
|
480
|
+
placeholder="A1:B5"
|
|
481
|
+
onChange={(e) => update('rangeA1', e.target.value)}
|
|
482
|
+
/>
|
|
483
|
+
</label>
|
|
484
|
+
<div style={RANGE_NOTE_STYLE}>
|
|
485
|
+
The chart is placed over the current selection. Numeric cells in the range become the
|
|
486
|
+
series.
|
|
487
|
+
</div>
|
|
488
|
+
</Dialog>
|
|
489
|
+
);
|
|
490
|
+
}
|