@openpresentation/opf-editor 0.12.1 → 0.15.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.
Files changed (103) hide show
  1. package/README.md +122 -80
  2. package/dist/annotations.d.ts +16 -16
  3. package/dist/annotations.js +71 -79
  4. package/dist/assets.d.ts +7 -7
  5. package/dist/assets.js +16 -16
  6. package/dist/background-options.d.ts +12 -5
  7. package/dist/background-options.js +44 -19
  8. package/dist/block-controls.js +7 -7
  9. package/dist/block-convert.d.ts +7 -7
  10. package/dist/block-convert.js +22 -21
  11. package/dist/blocks.d.ts +8 -8
  12. package/dist/blocks.js +41 -40
  13. package/dist/canvas-fields.js +35 -12
  14. package/dist/canvas.d.ts +30 -31
  15. package/dist/canvas.js +64 -68
  16. package/dist/catalogs.d.ts +48 -0
  17. package/dist/catalogs.js +217 -0
  18. package/dist/chart-data.d.ts +10 -10
  19. package/dist/chart-data.js +21 -21
  20. package/dist/chart-options-panel.d.ts +2 -2
  21. package/dist/chart-options-panel.js +82 -8
  22. package/dist/chart-options.d.ts +35 -9
  23. package/dist/chart-options.js +133 -40
  24. package/dist/checks.js +23 -0
  25. package/dist/color-authoring.js +1 -1
  26. package/dist/content-actions.d.ts +17 -17
  27. package/dist/content-actions.js +74 -73
  28. package/dist/content-controls.js +21 -21
  29. package/dist/data-grid.js +7 -8
  30. package/dist/data.d.ts +4 -4
  31. package/dist/data.js +5 -5
  32. package/dist/dataset-refs.d.ts +2 -2
  33. package/dist/dataset-refs.js +5 -5
  34. package/dist/design-controls.d.ts +12 -5
  35. package/dist/design-controls.js +312 -111
  36. package/dist/design-options.d.ts +20 -18
  37. package/dist/design-options.js +91 -99
  38. package/dist/edit-helpers.js +8 -7
  39. package/dist/exact-source.js +5 -5
  40. package/dist/export.d.ts +7 -7
  41. package/dist/export.js +48 -17
  42. package/dist/find-panel.js +5 -5
  43. package/dist/find-replace.d.ts +3 -3
  44. package/dist/find-replace.js +23 -20
  45. package/dist/font-gate.js +49 -58
  46. package/dist/galleries.js +50 -101
  47. package/dist/grid-model.d.ts +25 -27
  48. package/dist/grid-model.js +92 -102
  49. package/dist/image-crop.d.ts +4 -4
  50. package/dist/image-crop.js +28 -29
  51. package/dist/image-cropper.js +3 -3
  52. package/dist/image-options.d.ts +45 -0
  53. package/dist/image-options.js +195 -0
  54. package/dist/index.d.ts +48 -31
  55. package/dist/index.js +154 -126
  56. package/dist/json-editor.js +2 -2
  57. package/dist/json-menu.js +2 -2
  58. package/dist/json-options.d.ts +3 -3
  59. package/dist/json-options.js +44 -54
  60. package/dist/layout-handles.js +5 -5
  61. package/dist/layout-placeholders.js +4 -4
  62. package/dist/layout.d.ts +2 -2
  63. package/dist/layout.js +8 -7
  64. package/dist/numbering-panel.js +3 -5
  65. package/dist/numbering.d.ts +3 -5
  66. package/dist/numbering.js +14 -26
  67. package/dist/outline-view.js +24 -24
  68. package/dist/outline.d.ts +12 -12
  69. package/dist/outline.js +52 -50
  70. package/dist/persistence.d.ts +1 -1
  71. package/dist/persistence.js +19 -18
  72. package/dist/presentation-ids.js +2 -2
  73. package/dist/react.js +3 -3
  74. package/dist/review-panel.d.ts +30 -13
  75. package/dist/review-panel.js +160 -53
  76. package/dist/review.d.ts +36 -43
  77. package/dist/review.js +92 -53
  78. package/dist/rich-text-input.js +4 -3
  79. package/dist/rich-text-toolbar.js +5 -3
  80. package/dist/rich-text.js +5 -2
  81. package/dist/schema-inspector.d.ts +2 -2
  82. package/dist/schema-inspector.js +16 -11
  83. package/dist/schema.d.ts +1 -1
  84. package/dist/schema.js +26 -6
  85. package/dist/slide-manager.d.ts +2 -2
  86. package/dist/slide-manager.js +19 -19
  87. package/dist/slides.d.ts +21 -21
  88. package/dist/slides.js +104 -101
  89. package/dist/svelte.js +2 -3
  90. package/dist/switches.d.ts +37 -22
  91. package/dist/switches.js +282 -171
  92. package/dist/table-options.d.ts +8 -8
  93. package/dist/table-options.js +28 -27
  94. package/dist/table-structure.d.ts +10 -10
  95. package/dist/table-structure.js +21 -21
  96. package/dist/template-panel.d.ts +3 -3
  97. package/dist/template-panel.js +37 -13
  98. package/dist/templates.d.ts +22 -11
  99. package/dist/templates.js +48 -55
  100. package/dist/transfer.d.ts +15 -4
  101. package/dist/transfer.js +107 -161
  102. package/package.json +12 -6
  103. package/dist/font-defaults.js +0 -25
@@ -0,0 +1,217 @@
1
+ // Catalogs (FA-23, OPF 0.15). The editor never ships or imports catalog data: the host registers its catalogs (`Catalog[]`,
2
+ // the shape core's `CatalogOptions.catalogs` takes, for example `defaultCatalog` from `@openpresentation/opf/catalog`), on the
3
+ // session (`createEditorSession(doc, { catalogs })`) or per call. `mergeCatalogs` is the one function that turns those into the
4
+ // one list the editor hands to core (resolution, validation, embedding, copying, updates), to the renderer and to the exporter.
5
+ // Pickers list core's `catalogRecords` and write the reference it gives (`id` or `name:id`); saving and exporting embed every
6
+ // referenced record with core's `embed`, so a saved document renders the same with no catalog registered.
7
+ import { catalogKinds, catalogRecords, embed, moveToCustom as coreMoveToCustom, updateFromCatalog } from "@openpresentation/opf";
8
+ import { OPFEditorError } from "./index.js";
9
+
10
+ const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
11
+
12
+ /**
13
+ * One catalog list from any number of lists (or single catalogs): the session's registration first, then each host list, in order.
14
+ * A catalog is identified by its `source`; the first catalog with a source wins and later ones with the same source are dropped,
15
+ * so the host default (the first entry) never moves. Entries that are not catalogs throw `invalid-catalogs` (core's
16
+ * `OPFCatalogsOptionError` code).
17
+ */
18
+ export function mergeCatalogs(...lists) {
19
+ const merged = [];
20
+ const sources = new Set();
21
+ for (const list of lists) {
22
+ if (list === undefined || list === null) continue;
23
+ for (const catalog of Array.isArray(list) ? list : [list]) {
24
+ if (catalog === undefined || catalog === null) continue;
25
+ if (!isObject(catalog) || typeof catalog.source !== "string" || catalog.source === "")
26
+ throw new OPFEditorError("invalid-catalogs", "A registered catalog is an object with a non-empty source, such as defaultCatalog from @openpresentation/opf/catalog.", { catalog });
27
+ if (sources.has(catalog.source)) continue;
28
+ sources.add(catalog.source);
29
+ merged.push(catalog);
30
+ }
31
+ }
32
+ return Object.freeze(merged);
33
+ }
34
+
35
+ /** The catalogs for one operation: the session's registered list (when `editor` is given), then `options.catalogs`. */
36
+ export function catalogsFor(editor, options = {}) {
37
+ return mergeCatalogs(editor?.catalogs, options?.catalogs);
38
+ }
39
+
40
+ /** The label a picker shows for a record: its name, then its gallery label, then the id. */
41
+ export function catalogRecordLabel(record, id) {
42
+ const gallery = isObject(record?.["x-gallery"]) ? record["x-gallery"] : {};
43
+ for (const value of [record?.name, record?.title, record?.label, gallery.label, gallery.name]) if (typeof value === "string" && value) return value;
44
+ return String(id);
45
+ }
46
+
47
+ /**
48
+ * Every record a picker can offer for `kind`, from core's `catalogRecords`: the document's embedded records first, then the
49
+ * registered catalogs'. Each entry is `{ id, reference, label, description, record, group, source, origin }`; `id` and
50
+ * `reference` are the reference to write (`id` or `name:id`). A reference is listed once.
51
+ */
52
+ export function listCatalogRecords(presentation, kind, options = {}) {
53
+ const seen = new Set();
54
+ const out = [];
55
+ for (const entry of catalogRecords(presentation ?? {}, kind, { catalogs: mergeCatalogs(options.catalogs) })) {
56
+ if (seen.has(entry.reference)) continue;
57
+ seen.add(entry.reference);
58
+ const record = isObject(entry.record) ? entry.record : {};
59
+ out.push({
60
+ id: entry.reference,
61
+ reference: entry.reference,
62
+ label: catalogRecordLabel(record, entry.id),
63
+ ...(typeof record.description === "string" ? { description: record.description } : {}),
64
+ record,
65
+ group: entry.group,
66
+ ...(entry.source !== undefined ? { source: entry.source } : {}),
67
+ origin: entry.origin,
68
+ });
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /**
74
+ * The document to save or hand on: core's `embed` adds every record the document references, once, under its catalog group
75
+ * (with the records those reference, `x-*` display metadata stripped), so it renders the same with no catalog registered.
76
+ * Returns `{ document, added, unresolved }`; the input is not changed.
77
+ */
78
+ export function prepareSave(presentation, options = {}) {
79
+ const result = embed(presentation, { catalogs: mergeCatalogs(options.catalogs) });
80
+ return { document: result.document, added: result.added ?? [], unresolved: result.unresolved ?? [] };
81
+ }
82
+
83
+ /** `prepareSave` for the session's document, with the session's catalogs and `options.catalogs`. The session is not changed. */
84
+ export function saveDocument(editor, options = {}) {
85
+ assertSession(editor);
86
+ return prepareSave(editor.presentation, { catalogs: catalogsFor(editor, options) });
87
+ }
88
+
89
+ /**
90
+ * The "Update from catalog" check: core's `updateFromCatalog` compares every record embedded under `default` or a named group
91
+ * (never `custom`) with the registered catalog's current record. Returns `{ changes, patch }`; nothing is applied. `refs` limits
92
+ * the check to some references (`[{ kind, reference }]`).
93
+ */
94
+ export function checkCatalogUpdates(presentation, options = {}) {
95
+ const update = updateFromCatalog(presentation, mergeCatalogs(options.catalogs), options.refs);
96
+ return { changes: update.changes ?? [], patch: update.patch ?? [] };
97
+ }
98
+
99
+ /**
100
+ * Apply an update the author approved, as ONE undoable step. `options.refs` names the approved changes (`[{ kind, reference }]`);
101
+ * the update is recomputed for exactly those against the session's current document, so a stale review never applies. Returns
102
+ * the session change plus `changes` (what was applied) and `changed`.
103
+ */
104
+ export function applyCatalogUpdate(editor, options = {}) {
105
+ assertSession(editor);
106
+ if (!Array.isArray(options.refs) || !options.refs.length) throw new OPFEditorError("no-approved-updates", "Choose the catalog updates to apply.");
107
+ const update = checkCatalogUpdates(editor.presentation, { catalogs: catalogsFor(editor, options), refs: options.refs });
108
+ if (!update.patch.length) return { changed: false, changes: [], presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
109
+ const change = editor.applyPatch(update.patch, { ...options.meta, source: options.meta?.source ?? "catalog-update", catalogRecordEdit: "in-place", catalogUpdate: update.changes.map(({ kind, reference }) => ({ kind, reference })) });
110
+ return { ...change, changed: true, changes: update.changes };
111
+ }
112
+
113
+ /**
114
+ * Core finding codes about catalog references and embedded records, which the editor surfaces next to its catalog controls:
115
+ * a reference that resolves nowhere, a prefix with no catalog group, and a record embedded under a catalog's group that the
116
+ * registered catalog does not publish (offer `moveToCustom`).
117
+ */
118
+ export const REFERENCE_FINDING_CODES = Object.freeze(["opf/unresolved-reference", "opf/undeclared-catalog", "opf/catalog-record-not-in-source"]);
119
+
120
+ /** The reference and embedded-record findings (`REFERENCE_FINDING_CODES`) of a validation report (the session's `validation`). */
121
+ export function referenceFindings(validation) {
122
+ return (validation?.findings ?? []).filter((finding) => REFERENCE_FINDING_CODES.includes(finding.ruleId ?? finding.code));
123
+ }
124
+
125
+ function assertSession(editor) {
126
+ if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function")
127
+ throw new OPFEditorError("invalid-editor", "Expected an editor session created by createEditorSession.");
128
+ }
129
+
130
+ /**
131
+ * The record an `opf/catalog-record-not-in-source` finding names, from its path (`/catalogs/<group>/<kind>/<id>` or the dotted
132
+ * form): `{ group, kind, id }`, or undefined for a path that names no embedded record.
133
+ */
134
+ export function catalogRecordAt(path) {
135
+ const parts = typeof path === "string" ? (path.startsWith("/") ? path.slice(1).split("/").map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~")) : path.split(".")) : [];
136
+ if (parts.length !== 4 || parts[0] !== "catalogs" || !catalogKinds.includes(parts[2])) return undefined;
137
+ return { group: parts[1], kind: parts[2], id: parts[3] };
138
+ }
139
+
140
+ const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
141
+
142
+ /**
143
+ * Move a record embedded under `default` or a named group into `catalogs.custom` (the fix the `opf/catalog-record-not-in-source`
144
+ * finding offers, and the fork an edit makes) with core's `moveToCustom`: the document owns it from then on. Every reference that
145
+ * resolved to it is rewritten to the custom id (`options.id`, else its id, `<id>-2` and up on a conflict), and references written
146
+ * inside it keep naming what they named. Returns `{ presentation, patches, changed, from, to, references }`.
147
+ */
148
+ export function prepareMoveToCustom(presentation, target, options = {}) {
149
+ const { group, kind, id } = target ?? {};
150
+ const groups = isObject(presentation?.catalogs) ? presentation.catalogs : {};
151
+ const record = isObject(groups[group]?.[kind]) ? groups[group][kind][id] : undefined;
152
+ if (group === "custom" || record === undefined) throw new OPFEditorError("not-a-catalog-record", "Choose a record embedded under catalogs.default or a named catalog group.", { group, kind, id });
153
+ // Core's moveToCustom is the one implementation: references follow, the record's own references keep naming what they named.
154
+ const reference = group === "default" ? id : `${group}:${id}`;
155
+ const moved = coreMoveToCustom(presentation, { kind, reference }, { catalogs: mergeCatalogs(options.catalogs), ...(options.id ? { id: options.id } : {}) });
156
+ const patches = [{ op: "test", path: "/catalogs", value: structuredClone(presentation.catalogs) }, ...moved.patch];
157
+ return { presentation: moved.document, patches, changed: true, from: { group, kind, id }, to: { group: "custom", kind, id: moved.to.id }, references: moved.references ?? [] };
158
+ }
159
+
160
+ /**
161
+ * `prepareMoveToCustom` on the session's document, as ONE undoable step (`meta.source: "catalog-move-to-custom"`). `options.id` names
162
+ * the custom record (default: the record's id, `<id>-2` on a conflict).
163
+ */
164
+ export function moveToCustom(editor, target, options = {}) {
165
+ assertSession(editor);
166
+ const prepared = prepareMoveToCustom(editor.presentation, target, { catalogs: catalogsFor(editor, options), ...(options.id ? { id: options.id } : {}) });
167
+ const change = editor.applyPatch(prepared.patches, { ...options.meta, source: options.meta?.source ?? "catalog-move-to-custom", catalogRecordEdit: "in-place" });
168
+ return { ...change, changed: true, from: prepared.from, to: prepared.to };
169
+ }
170
+
171
+ /** The record kinds a user edits, which an edit forks into `catalogs.custom` when they live under a catalog's group. */
172
+ export const FORKED_RECORD_KINDS = Object.freeze(["layouts", "themes", "colorSchemes", "fontSchemes"]);
173
+
174
+ /**
175
+ * Records under `catalogs.default` or a named group whose content an edit from `before` to `after` changed (present in both, not
176
+ * equal). An added or removed record is not an edit. Returns `[{ group, kind, id }]`.
177
+ */
178
+ export function editedCatalogRecords(before, after) {
179
+ const out = [];
180
+ const groupsBefore = isObject(before?.catalogs) ? before.catalogs : {};
181
+ const groupsAfter = isObject(after?.catalogs) ? after.catalogs : {};
182
+ for (const [group, value] of Object.entries(groupsAfter)) {
183
+ if (group === "custom" || !isObject(value) || !isObject(groupsBefore[group])) continue;
184
+ for (const kind of FORKED_RECORD_KINDS) {
185
+ const was = isObject(groupsBefore[group][kind]) ? groupsBefore[group][kind] : {};
186
+ const now = isObject(value[kind]) ? value[kind] : {};
187
+ for (const id of Object.keys(now)) if (Object.hasOwn(was, id) && !sameJson(was[id], now[id])) out.push({ group, kind, id });
188
+ }
189
+ }
190
+ return out;
191
+ }
192
+
193
+ /** The notice an editor shows after a fork: the record is the deck's own now and no longer receives catalog updates. */
194
+ export function forkNotice(fork) {
195
+ const label = { layouts: "layout", themes: "theme", colorSchemes: "color scheme", fontSchemes: "font scheme" }[fork.from.kind] ?? fork.from.kind;
196
+ return `The ${label} ${fork.from.id} is now this presentation's own (${fork.to.id}); it no longer receives catalog updates.`;
197
+ }
198
+
199
+ /**
200
+ * Owner rule (FA-23): editing a record that lives under `catalogs.default` or a named group forks it into `catalogs.custom` instead
201
+ * of changing the catalog's record in place. Given the document before and after an edit, returns the document after the forks
202
+ * (each edited record moved to custom as `<id>-custom`, or `forkIds[<group>:<kind>:<id>]`, every reference rewritten, the
203
+ * original dropped) and `forks: [{ from, to }]`. Nothing to fork returns `after` and `[]`.
204
+ */
205
+ export function forkEditedRecords(before, after, options = {}) {
206
+ let document = after;
207
+ const forks = [];
208
+ for (const target of editedCatalogRecords(before, after)) {
209
+ const custom = isObject(document.catalogs?.custom?.[target.kind]) ? document.catalogs.custom[target.kind] : {};
210
+ let id = options.forkIds?.[`${target.group}:${target.kind}:${target.id}`] ?? `${target.id}-custom`;
211
+ for (let n = 2; Object.hasOwn(custom, id); n += 1) id = `${target.id}-custom-${n}`;
212
+ const moved = prepareMoveToCustom(document, target, { catalogs: options.catalogs, id });
213
+ document = moved.presentation;
214
+ forks.push({ from: moved.from, to: moved.to });
215
+ }
216
+ return { document, forks };
217
+ }
@@ -2,29 +2,29 @@ import type { EditorSession } from "./index.js";
2
2
  import type { GridAddress, GridCellEdit, GridChange, GridOptions, GridSortOptions, PreparedGridChange } from "./grid-model.js";
3
3
 
4
4
  /** Set chart cells from text or typed values: names and categories are text, series cells are numbers read in the number format (blank is a gap, never 0). One patch; every problem is in `error.issues`. */
5
- export declare function prepareChartCells(document: unknown, chartPath: string, edits: GridCellEdit[], options?: GridOptions): PreparedGridChange;
5
+ export declare function prepareChartCells(presentation: unknown, chartPath: string, edits: GridCellEdit[], options?: GridOptions): PreparedGridChange;
6
6
  export declare function setChartCells(editor: EditorSession, chartPath: string, edits: GridCellEdit[], options?: GridOptions): GridChange;
7
- export declare function prepareChartPaste(document: unknown, chartPath: string, anchor: GridAddress, source: string | string[][], options?: GridOptions): PreparedGridChange;
7
+ export declare function prepareChartPaste(presentation: unknown, chartPath: string, anchor: GridAddress, source: string | string[][], options?: GridOptions): PreparedGridChange;
8
8
  export declare function pasteChartText(editor: EditorSession, chartPath: string, anchor: GridAddress, source: string | string[][], options?: GridOptions): GridChange;
9
9
  /** Insert `count` empty rows (categories) so the first is row `at`. */
10
- export declare function prepareChartInsertRows(document: unknown, chartPath: string, at: number, count?: number, options?: GridOptions): PreparedGridChange;
10
+ export declare function prepareChartInsertRows(presentation: unknown, chartPath: string, at: number, count?: number, options?: GridOptions): PreparedGridChange;
11
11
  export declare function insertChartRows(editor: EditorSession, chartPath: string, at: number, count?: number, options?: GridOptions): GridChange;
12
- export declare function prepareChartDeleteRows(document: unknown, chartPath: string, indices: number[], options?: GridOptions): PreparedGridChange;
12
+ export declare function prepareChartDeleteRows(presentation: unknown, chartPath: string, indices: number[], options?: GridOptions): PreparedGridChange;
13
13
  export declare function deleteChartRows(editor: EditorSession, chartPath: string, indices: number[], options?: GridOptions): GridChange;
14
- export declare function prepareChartMoveRows(document: unknown, chartPath: string, from: number, to: number, count?: number, options?: GridOptions): PreparedGridChange;
14
+ export declare function prepareChartMoveRows(presentation: unknown, chartPath: string, from: number, to: number, count?: number, options?: GridOptions): PreparedGridChange;
15
15
  export declare function moveChartRows(editor: EditorSession, chartPath: string, from: number, to: number, count?: number, options?: GridOptions): GridChange;
16
16
  /** Insert `count` unnamed series so the first is column `at`. */
17
- export declare function prepareChartInsertColumns(document: unknown, chartPath: string, at: number, count?: number, options?: GridOptions): PreparedGridChange;
17
+ export declare function prepareChartInsertColumns(presentation: unknown, chartPath: string, at: number, count?: number, options?: GridOptions): PreparedGridChange;
18
18
  export declare function insertChartColumns(editor: EditorSession, chartPath: string, at: number, count?: number, options?: GridOptions): GridChange;
19
- export declare function prepareChartDeleteColumns(document: unknown, chartPath: string, indices: number[], options?: GridOptions): PreparedGridChange;
19
+ export declare function prepareChartDeleteColumns(presentation: unknown, chartPath: string, indices: number[], options?: GridOptions): PreparedGridChange;
20
20
  export declare function deleteChartColumns(editor: EditorSession, chartPath: string, indices: number[], options?: GridOptions): GridChange;
21
- export declare function prepareChartMoveColumns(document: unknown, chartPath: string, from: number, to: number, count?: number, options?: GridOptions): PreparedGridChange;
21
+ export declare function prepareChartMoveColumns(presentation: unknown, chartPath: string, from: number, to: number, count?: number, options?: GridOptions): PreparedGridChange;
22
22
  export declare function moveChartColumns(editor: EditorSession, chartPath: string, from: number, to: number, count?: number, options?: GridOptions): GridChange;
23
23
  /** Sort the rows (categories) by a column, stably and by type. */
24
- export declare function prepareChartSort(document: unknown, chartPath: string, column: number, options?: GridSortOptions): PreparedGridChange;
24
+ export declare function prepareChartSort(presentation: unknown, chartPath: string, column: number, options?: GridSortOptions): PreparedGridChange;
25
25
  export declare function sortChartRows(editor: EditorSession, chartPath: string, column: number, options?: GridSortOptions): GridChange;
26
26
  /** Swap categories and series: the first column's values become the series names and each series becomes a row. Numeric categories become text. */
27
- export declare function prepareChartTranspose(document: unknown, chartPath: string): PreparedGridChange;
27
+ export declare function prepareChartTranspose(presentation: unknown, chartPath: string): PreparedGridChange;
28
28
  export declare function transposeChart(editor: EditorSession, chartPath: string, options?: { meta?: Record<string, unknown> }): GridChange;
29
29
  /** Rename a series: the label of data column `column` (1 or more). */
30
30
  export declare function renameChartSeries(editor: EditorSession, chartPath: string, column: number, name: string, options?: { meta?: Record<string, unknown> }): GridChange;
@@ -3,7 +3,7 @@
3
3
  // second). Cells are text for names and categories and numbers for values; a gap is null and stays a gap, never 0. Each operation is
4
4
  // one validated, undoable patch (grid-model.js has the implementation; grid-text.js documents number reading).
5
5
  //
6
- // `prepare*` computes `{ document, patches, changed }` without touching a session; the session forms apply it as one undo step.
6
+ // `prepare*` computes `{ presentation, patches, changed }` without touching a session; the session forms apply it as one undo step.
7
7
  import {
8
8
  prepareGridCells,
9
9
  preparePaste,
@@ -26,64 +26,64 @@ import {
26
26
  prepareTranspose,
27
27
  transposeGridData,
28
28
  } from "./grid-model.js";
29
- export function prepareChartCells(document, chartPath, edits, options = {}) {
30
- return prepareGridCells(document, chartPath, edits, { ...options, kind: "chart" });
29
+ export function prepareChartCells(presentation, chartPath, edits, options = {}) {
30
+ return prepareGridCells(presentation, chartPath, edits, { ...options, kind: "chart" });
31
31
  }
32
32
  export function setChartCells(editor, chartPath, edits, options = {}) {
33
33
  return setGridCells(editor, chartPath, edits, { ...options, kind: "chart" });
34
34
  }
35
- export function prepareChartPaste(document, chartPath, anchor, source, options = {}) {
36
- return preparePaste(document, chartPath, anchor, source, { ...options, kind: "chart" });
35
+ export function prepareChartPaste(presentation, chartPath, anchor, source, options = {}) {
36
+ return preparePaste(presentation, chartPath, anchor, source, { ...options, kind: "chart" });
37
37
  }
38
38
  export function pasteChartText(editor, chartPath, anchor, source, options = {}) {
39
39
  return pasteGridText(editor, chartPath, anchor, source, { ...options, kind: "chart" });
40
40
  }
41
- export function prepareChartInsertRows(document, chartPath, at, count = 1, options = {}) {
42
- return prepareInsertRows(document, chartPath, at, count, { ...options, kind: "chart" });
41
+ export function prepareChartInsertRows(presentation, chartPath, at, count = 1, options = {}) {
42
+ return prepareInsertRows(presentation, chartPath, at, count, { ...options, kind: "chart" });
43
43
  }
44
44
  export function insertChartRows(editor, chartPath, at, count = 1, options = {}) {
45
45
  return insertGridRows(editor, chartPath, at, count, { ...options, kind: "chart" });
46
46
  }
47
- export function prepareChartDeleteRows(document, chartPath, indices, options = {}) {
48
- return prepareDeleteRows(document, chartPath, indices, { ...options, kind: "chart" });
47
+ export function prepareChartDeleteRows(presentation, chartPath, indices, options = {}) {
48
+ return prepareDeleteRows(presentation, chartPath, indices, { ...options, kind: "chart" });
49
49
  }
50
50
  export function deleteChartRows(editor, chartPath, indices, options = {}) {
51
51
  return deleteGridRows(editor, chartPath, indices, { ...options, kind: "chart" });
52
52
  }
53
- export function prepareChartMoveRows(document, chartPath, from, to, count = 1, options = {}) {
54
- return prepareMoveRows(document, chartPath, from, to, count, { ...options, kind: "chart" });
53
+ export function prepareChartMoveRows(presentation, chartPath, from, to, count = 1, options = {}) {
54
+ return prepareMoveRows(presentation, chartPath, from, to, count, { ...options, kind: "chart" });
55
55
  }
56
56
  export function moveChartRows(editor, chartPath, from, to, count = 1, options = {}) {
57
57
  return moveGridRows(editor, chartPath, from, to, count, { ...options, kind: "chart" });
58
58
  }
59
- export function prepareChartInsertColumns(document, chartPath, at, count = 1, options = {}) {
60
- return prepareInsertColumns(document, chartPath, at, count, { ...options, kind: "chart" });
59
+ export function prepareChartInsertColumns(presentation, chartPath, at, count = 1, options = {}) {
60
+ return prepareInsertColumns(presentation, chartPath, at, count, { ...options, kind: "chart" });
61
61
  }
62
62
  export function insertChartColumns(editor, chartPath, at, count = 1, options = {}) {
63
63
  return insertGridColumns(editor, chartPath, at, count, { ...options, kind: "chart" });
64
64
  }
65
- export function prepareChartDeleteColumns(document, chartPath, indices, options = {}) {
66
- return prepareDeleteColumns(document, chartPath, indices, { ...options, kind: "chart" });
65
+ export function prepareChartDeleteColumns(presentation, chartPath, indices, options = {}) {
66
+ return prepareDeleteColumns(presentation, chartPath, indices, { ...options, kind: "chart" });
67
67
  }
68
68
  export function deleteChartColumns(editor, chartPath, indices, options = {}) {
69
69
  return deleteGridColumns(editor, chartPath, indices, { ...options, kind: "chart" });
70
70
  }
71
- export function prepareChartMoveColumns(document, chartPath, from, to, count = 1, options = {}) {
72
- return prepareMoveColumns(document, chartPath, from, to, count, { ...options, kind: "chart" });
71
+ export function prepareChartMoveColumns(presentation, chartPath, from, to, count = 1, options = {}) {
72
+ return prepareMoveColumns(presentation, chartPath, from, to, count, { ...options, kind: "chart" });
73
73
  }
74
74
  export function moveChartColumns(editor, chartPath, from, to, count = 1, options = {}) {
75
75
  return moveGridColumns(editor, chartPath, from, to, count, { ...options, kind: "chart" });
76
76
  }
77
- export function prepareChartSort(document, chartPath, column, options = {}) {
78
- return prepareSortRows(document, chartPath, column, { ...options, kind: "chart" });
77
+ export function prepareChartSort(presentation, chartPath, column, options = {}) {
78
+ return prepareSortRows(presentation, chartPath, column, { ...options, kind: "chart" });
79
79
  }
80
80
  export function sortChartRows(editor, chartPath, column, options = {}) {
81
81
  return sortGridRows(editor, chartPath, column, { ...options, kind: "chart" });
82
82
  }
83
83
 
84
84
  /** Swap categories and series: the first column's values become the series names and each series becomes a row. See {@link prepareTranspose}. */
85
- export function prepareChartTranspose(document, chartPath) {
86
- return prepareTranspose(document, chartPath);
85
+ export function prepareChartTranspose(presentation, chartPath) {
86
+ return prepareTranspose(presentation, chartPath);
87
87
  }
88
88
  export function transposeChart(editor, chartPath, options = {}) {
89
89
  return transposeGridData(editor, chartPath, options);
@@ -7,8 +7,8 @@ export interface ChartOptionsPanelOptions {
7
7
  onStatus?: (message: string) => void;
8
8
  }
9
9
  export interface ChartOptionsPanel {
10
- /** The panel's root element; undefined when the installed core has no chart option fields. */
11
- element: HTMLElement | undefined;
10
+ /** The panel's root element (hidden while no chart is selected). */
11
+ element: HTMLElement;
12
12
  /** Re-read the selection and the chart; the panel also refreshes on every editor change. */
13
13
  refresh(): void;
14
14
  destroy(): void;
@@ -1,14 +1,18 @@
1
- // The chart options panel (RR-35): axis titles, legend position and data labels for the selected chart.
1
+ // The chart options panel (RR-35): axis titles, legend position and data labels for the selected chart, plus (FA-09) its alt text and
2
+ // (FA-14) which series and categories to highlight.
2
3
  // It mounts into any host element over an editor session; every control commits one undoable change through
3
4
  // `setChartOptions` (src/chart-options.js), the canvas redraws, and Undo restores the chart. The panel shows only what
4
- // the selected chart type can show (core's support table), and hides itself when no chart is selected.
5
- import { CHART_LABEL_CONTENT, CHART_LEGEND_POSITIONS, chartOptionsAvailable, parseChartPath, readChartOptions, setChartOptions } from "./chart-options.js";
5
+ // the selected chart type can show (core's support table), and hides itself when no chart is selected. On a combo chart
6
+ // (FA-15) a "Series" group sets which series are lines and which lines use the secondary value axis, and a secondary axis
7
+ // title field appears while a line uses that axis.
8
+ import { CHART_LABEL_CONTENT, CHART_LEGEND_POSITIONS, parseChartPath, readChartOptions, setChartOptions } from "./chart-options.js";
6
9
 
7
10
  const LEGEND_LABELS = { default: "Default", none: "None", top: "Top", bottom: "Bottom", left: "Left", right: "Right" };
8
11
  const CONTENT_LABELS = { category: "Category", value: "Value", percent: "Percent" };
9
12
  const POSITION_LABELS = { auto: "Automatic", center: "Center", "inside-end": "Inside end", "inside-base": "Inside base", "outside-end": "Outside end", above: "Above", below: "Below", left: "Left", right: "Right" };
10
13
 
11
14
  let counter = 0;
15
+ const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
12
16
 
13
17
  /**
14
18
  * Mount the panel in `host`. `getSelectedPath()` returns the selected OPF path (any path inside a chart selects it);
@@ -16,7 +20,6 @@ let counter = 0;
16
20
  * call `refresh()` when the selection changes (the panel also refreshes on every editor change).
17
21
  */
18
22
  export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatus = () => {} }) {
19
- if (!chartOptionsAvailable()) return { refresh() {}, destroy() {}, element: undefined };
20
23
  const id = `opf-chart-options-${++counter}`;
21
24
  const root = document.createElement("section");
22
25
  root.className = "opf-chart-options";
@@ -24,10 +27,16 @@ export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatu
24
27
  root.hidden = true;
25
28
  root.innerHTML = `
26
29
  <h3 class="opf-co-title">Chart options</h3>
30
+ <div class="opf-co-group" data-group="alt">
31
+ <label class="opf-co-row"><span>Alt text</span><input type="text" id="${id}-alt" aria-label="Chart alt text" data-opf-chart-option="alt" autocomplete="off" placeholder="What the chart shows"></label>
32
+ <label class="opf-co-row opf-co-check"><input type="checkbox" id="${id}-decorative" aria-label="Decorative chart" data-opf-chart-option="alt.decorative"><span>Decorative (no alt text)</span></label>
33
+ </div>
27
34
  <div class="opf-co-group" data-group="axisTitles">
28
35
  <label class="opf-co-row" data-axis="category"><span>Category axis title</span><input type="text" id="${id}-category" aria-label="Category axis title" data-opf-chart-option="axisTitles.category" autocomplete="off"></label>
29
36
  <label class="opf-co-row" data-axis="value"><span>Value axis title</span><input type="text" id="${id}-value" aria-label="Value axis title" data-opf-chart-option="axisTitles.value" autocomplete="off"></label>
37
+ <label class="opf-co-row" data-axis="secondary"><span>Secondary axis title</span><input type="text" id="${id}-secondary" aria-label="Secondary axis title" data-opf-chart-option="axisTitles.secondary" autocomplete="off"></label>
30
38
  </div>
39
+ <fieldset class="opf-co-group opf-co-series" data-group="combo"><legend>Series</legend><div data-row="combo-series"></div></fieldset>
31
40
  <div class="opf-co-group" data-group="legend">
32
41
  <label class="opf-co-row"><span>Legend</span><select id="${id}-legend" aria-label="Legend" data-opf-chart-option="legend">${CHART_LEGEND_POSITIONS.map((value) => `<option value="${value}">${LEGEND_LABELS[value]}</option>`).join("")}</select></label>
33
42
  </div>
@@ -36,12 +45,18 @@ export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatu
36
45
  <fieldset class="opf-co-content"><legend>Label shows</legend>${CHART_LABEL_CONTENT.map((part) => `<label class="opf-co-check"><input type="checkbox" value="${part}" aria-label="Label shows ${CONTENT_LABELS[part].toLowerCase()}" data-opf-chart-option="dataLabels.content.${part}"><span>${CONTENT_LABELS[part]}</span></label>`).join("")}</fieldset>
37
46
  <label class="opf-co-row" data-row="position"><span>Label position</span><select id="${id}-position" aria-label="Label position" data-opf-chart-option="dataLabels.position"></select></label>
38
47
  <label class="opf-co-row" data-row="separator"><span>Separator</span><input type="text" id="${id}-separator" aria-label="Label separator" data-opf-chart-option="dataLabels.separator" autocomplete="off"></label>
48
+ </div>
49
+ <div class="opf-co-group" data-group="highlight">
50
+ <fieldset class="opf-co-highlight" data-part="series"><legend>Highlight series</legend><div class="opf-co-choices" data-choices="series"></div></fieldset>
51
+ <fieldset class="opf-co-highlight" data-part="categories"><legend>Highlight categories</legend><div class="opf-co-choices" data-choices="categories"></div></fieldset>
39
52
  </div>`;
40
53
  host.append(root);
41
54
  const $ = (selector) => root.querySelector(selector);
42
55
  const fields = {
43
- category: $(`#${id}-category`), value: $(`#${id}-value`), legend: $(`#${id}-legend`), labels: $(`#${id}-labels`),
56
+ alt: $(`#${id}-alt`), decorative: $(`#${id}-decorative`), category: $(`#${id}-category`), value: $(`#${id}-value`), secondary: $(`#${id}-secondary`), legend: $(`#${id}-legend`), labels: $(`#${id}-labels`),
57
+ series: $("[data-row='combo-series']"),
44
58
  position: $(`#${id}-position`), separator: $(`#${id}-separator`), contents: [...root.querySelectorAll("[data-opf-chart-option^='dataLabels.content.']")],
59
+ highlight: { series: $("[data-choices='series']"), categories: $("[data-choices='categories']") },
45
60
  };
46
61
  let chartPath;
47
62
 
@@ -55,13 +70,34 @@ export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatu
55
70
  return;
56
71
  }
57
72
  chartPath = path;
58
- const { fields: support, state } = readChartOptions(chart);
73
+ const { fields: support, state, choices, combo } = readChartOptions(chart, editor.presentation);
59
74
  root.hidden = false;
60
75
  const setValue = (input, value) => { if (input !== document.activeElement && input.value !== value) input.value = value; };
61
- for (const axis of ["category", "value"]) {
62
- $(`[data-axis="${axis}"]`).hidden = !support.axisTitles[axis];
76
+ setValue(fields.alt, state.alt);
77
+ fields.decorative.checked = state.decorative;
78
+ fields.alt.disabled = state.decorative;
79
+ const secondaryInUse = Boolean(combo?.series.some((entry) => entry.axis === "secondary"));
80
+ for (const axis of ["category", "value", "secondary"]) {
81
+ $(`[data-axis="${axis}"]`).hidden = !support.axisTitles[axis] || (axis === "secondary" && !secondaryInUse);
63
82
  setValue(fields[axis], state.axisTitles[axis]);
64
83
  }
84
+ // FA-15: one row per plotted series of a combo chart: "Line" and, for a line, "Secondary axis".
85
+ $("[data-group='combo']").hidden = !combo;
86
+ const series = combo?.series ?? [];
87
+ const signature = JSON.stringify(series.map((entry) => entry.name));
88
+ if (fields.series.dataset.series !== signature) {
89
+ fields.series.innerHTML = series.map((entry, index) => `<div class="opf-co-row opf-co-series-row" data-series="${index}"><span>${escapeHtml(entry.name)}</span>` +
90
+ `<label class="opf-co-check"><input type="checkbox" data-opf-chart-option="line" value="${escapeHtml(entry.name)}" aria-label="${escapeHtml(entry.name)} as a line"><span>Line</span></label>` +
91
+ `<label class="opf-co-check"><input type="checkbox" data-opf-chart-option="secondaryAxis" value="${escapeHtml(entry.name)}" aria-label="${escapeHtml(entry.name)} on the secondary axis"><span>Secondary axis</span></label></div>`).join("");
92
+ fields.series.dataset.series = signature;
93
+ }
94
+ series.forEach((entry, index) => {
95
+ const row = fields.series.querySelector(`[data-series="${index}"]`);
96
+ const line = row.querySelector("[data-opf-chart-option='line']"), secondary = row.querySelector("[data-opf-chart-option='secondaryAxis']");
97
+ line.checked = entry.role === "line";
98
+ secondary.checked = entry.axis === "secondary";
99
+ secondary.disabled = entry.role !== "line";
100
+ });
65
101
  $("[data-group='axisTitles']").hidden = !support.axisTitles.category && !support.axisTitles.value;
66
102
  $("[data-group='legend']").hidden = !support.legend;
67
103
  fields.legend.value = state.legend;
@@ -84,6 +120,31 @@ export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatu
84
120
  setValue(fields.separator, state.dataLabels.separator);
85
121
  fields.separator.disabled = off || state.dataLabels.content.length < 2;
86
122
  $("[data-row='separator']").hidden = support.dataLabels.content.length < 2;
123
+ // Highlight: one checkbox per plotted series and per category label the chart type can highlight.
124
+ for (const part of ["series", "categories"]) {
125
+ const offered = support.highlight[part] && choices[part].length > 0;
126
+ $(`[data-part='${part}']`).hidden = !offered;
127
+ const list = fields.highlight[part];
128
+ if (!offered) continue;
129
+ if (list.dataset.options !== JSON.stringify(choices[part])) {
130
+ list.replaceChildren(...choices[part].map((name) => {
131
+ const label = document.createElement("label");
132
+ label.className = "opf-co-check";
133
+ const input = document.createElement("input");
134
+ input.type = "checkbox";
135
+ input.value = name;
136
+ input.setAttribute("aria-label", `Highlight ${part === "series" ? "series" : "category"} ${name}`);
137
+ input.setAttribute("data-opf-chart-option", `highlight.${part}`);
138
+ const text = document.createElement("span");
139
+ text.textContent = name;
140
+ label.append(input, text);
141
+ return label;
142
+ }));
143
+ list.dataset.options = JSON.stringify(choices[part]);
144
+ }
145
+ for (const input of list.querySelectorAll("input")) input.checked = state.highlight[part].includes(input.value);
146
+ }
147
+ $("[data-group='highlight']").hidden = $("[data-part='series']").hidden && $("[data-part='categories']").hidden;
87
148
  }
88
149
 
89
150
  function commit(change, message) {
@@ -97,14 +158,27 @@ export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatu
97
158
  refresh();
98
159
  }
99
160
 
161
+ const checkedNames = (part) => [...fields.highlight[part].querySelectorAll("input:checked")].map((input) => input.value);
100
162
  const selectedContent = () => fields.contents.filter((input) => input.checked && !input.closest("label").hidden).map((input) => input.value);
101
163
  const listeners = [
164
+ [fields.alt, "change", () => commit({ alt: fields.alt.value }, "Changed the chart's alt text. Undo restores it.")],
165
+ [fields.decorative, "change", () => commit({ decorative: fields.decorative.checked }, fields.decorative.checked ? "Marked the chart decorative. Undo restores it." : "The chart is no longer decorative. Undo restores it.")],
102
166
  [fields.category, "change", () => commit({ axisTitles: { category: fields.category.value } }, "Changed the category axis title. Undo restores it.")],
103
167
  [fields.value, "change", () => commit({ axisTitles: { value: fields.value.value } }, "Changed the value axis title. Undo restores it.")],
168
+ [fields.secondary, "change", () => commit({ axisTitles: { secondary: fields.secondary.value } }, "Changed the secondary axis title. Undo restores it.")],
169
+ [fields.series, "change", () => {
170
+ const checked = (option) => [...fields.series.querySelectorAll(`[data-opf-chart-option='${option}']`)].filter((input) => input.checked).map((input) => input.value);
171
+ const line = checked("line");
172
+ commit({ line, secondaryAxis: checked("secondaryAxis").filter((name) => line.includes(name)) }, "Changed which series are lines. Undo restores them.");
173
+ }],
104
174
  [fields.legend, "change", () => commit({ legend: fields.legend.value }, "Changed the legend. Undo restores it.")],
105
175
  [fields.labels, "change", () => commit({ dataLabels: fields.labels.checked }, fields.labels.checked ? "Showing data labels. Undo restores the chart." : "Hid the data labels. Undo restores them.")],
106
176
  [fields.position, "change", () => commit({ dataLabels: { position: fields.position.value } }, "Moved the data labels. Undo restores them.")],
107
177
  [fields.separator, "change", () => commit({ dataLabels: { separator: fields.separator.value } }, "Changed the label separator. Undo restores it.")],
178
+ ...["series", "categories"].map((part) => [fields.highlight[part], "change", () => {
179
+ const names = checkedNames(part);
180
+ commit({ highlight: { [part]: names } }, names.length ? `Highlighted ${names.join(", ")}. Undo restores the colors.` : `Cleared the ${part} highlight. Undo restores it.`);
181
+ }]),
108
182
  ...fields.contents.map((input) => [input, "change", () => {
109
183
  const content = selectedContent();
110
184
  // A label always shows something: unticking the last part keeps it.
@@ -9,7 +9,8 @@ export declare const CHART_LABEL_CONTENT: readonly ["category", "value", "percen
9
9
 
10
10
  /** What a chart type can show (core's `chartOptionSupport`). */
11
11
  export interface ChartOptionFields {
12
- axisTitles: { category: boolean; value: boolean };
12
+ /** `secondary`: the type can title a secondary value axis (combo charts). */
13
+ axisTitles: { category: boolean; value: boolean; secondary: boolean };
13
14
  legend: boolean;
14
15
  dataLabels: {
15
16
  supported: boolean;
@@ -20,30 +21,55 @@ export interface ChartOptionFields {
20
21
  /** True for the constructs that label their marks by default (funnel, treemap). */
21
22
  defaultOn: boolean;
22
23
  };
24
+ /** What `chart.highlight` can name on this chart type: series, categories (one slice of a pie, one point of a line). */
25
+ highlight: { series: boolean; categories: boolean };
23
26
  }
24
- /** The three fields as form state. */
27
+ /** The names a highlight can take on a chart, from its resolved data (empty when the data does not resolve). */
28
+ export interface ChartHighlightChoices { series: string[]; categories: string[] }
29
+ /** A plotted series of a combo chart: drawn as columns ("bar") or as a line, on the primary or secondary value axis (FA-15). */
30
+ export interface ChartComboSeriesState {
31
+ name: string;
32
+ role: "bar" | "line";
33
+ axis: "primary" | "secondary";
34
+ }
35
+ /** The fields as form state. */
25
36
  export interface ChartOptionState {
26
- axisTitles: { category: string; value: string };
37
+ /** The chart's text alternative (empty when absent or decorative). */
38
+ alt: string;
39
+ /** True when the chart is marked decorative (alt is the empty string). */
40
+ decorative: boolean;
41
+ axisTitles: { category: string; value: string; secondary: string };
27
42
  legend: ChartLegendChoice;
28
43
  dataLabels: { on: boolean; explicit: boolean; content: ChartLabelContent[]; position: ChartLabelPosition | "auto"; separator: string };
44
+ /** The series and categories `chart.highlight` names (empty lists when it is absent). */
45
+ highlight: { series: string[]; categories: string[] };
29
46
  }
30
47
  export interface ChartOptionChange {
31
- axisTitles?: { category?: string; value?: string };
48
+ /** The text alternative: what the data shows. Trimmed; an empty string or null removes it. */
49
+ alt?: string | null;
50
+ /** true marks the chart decorative (alt ""); false removes an empty alt. */
51
+ decorative?: boolean;
52
+ axisTitles?: { category?: string; value?: string; secondary?: string };
53
+ /** Combo charts: the series drawn as lines, by name (at least one, and not every series). */
54
+ line?: string[];
55
+ /** Combo charts: the line series on the secondary value axis, by name. */
56
+ secondaryAxis?: string[];
32
57
  legend?: ChartLegendChoice;
33
58
  dataLabels?: boolean | null | { content?: ChartLabelContent[]; position?: ChartLabelPosition | "auto"; separator?: string };
59
+ /** `null` removes the highlight; a list replaces that part (an empty list removes it) and the other part is kept. */
60
+ highlight?: null | { series?: string[]; categories?: string[] };
34
61
  }
35
62
  export interface PreparedChartOptions {
36
63
  action: "chart-options";
37
64
  chartPath: string;
38
65
  changed: boolean;
39
66
  patches: JsonPatchOperation[];
40
- document: unknown;
67
+ presentation: unknown;
41
68
  }
42
69
 
43
- /** True when the installed core knows the chart option fields. */
44
- export declare function chartOptionsAvailable(): boolean;
45
70
  /** The chart path a selection path points at, or undefined. */
46
71
  export declare function parseChartPath(path: string): string | undefined;
47
- export declare function readChartOptions(chart: unknown): { target: unknown; fields: ChartOptionFields; state: ChartOptionState };
48
- export declare function prepareChartOptions(document: unknown, chartPath: string, change: ChartOptionChange): PreparedChartOptions;
72
+ /** `combo` is present on a combo chart: its plotted series in data order. Pass the `document` for a dataset-backed chart. */
73
+ export declare function readChartOptions(chart: unknown, presentation?: unknown): { target: unknown; fields: ChartOptionFields; choices: ChartHighlightChoices; state: ChartOptionState; combo?: { series: ChartComboSeriesState[] } };
74
+ export declare function prepareChartOptions(presentation: unknown, chartPath: string, change: ChartOptionChange): PreparedChartOptions;
49
75
  export declare function setChartOptions(editor: EditorSession, chartPath: string, change: ChartOptionChange, meta?: Record<string, unknown>): EditorChange & { action: "chart-options"; chartPath: string; changed: boolean };