@openpresentation/opf-editor 0.12.1 → 0.14.2
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/README.md +71 -60
- package/dist/annotations.d.ts +16 -16
- package/dist/annotations.js +71 -79
- package/dist/assets.d.ts +7 -7
- package/dist/assets.js +15 -15
- package/dist/background-options.d.ts +2 -2
- package/dist/background-options.js +6 -6
- package/dist/block-controls.js +7 -7
- package/dist/block-convert.d.ts +7 -7
- package/dist/block-convert.js +22 -21
- package/dist/blocks.d.ts +8 -8
- package/dist/blocks.js +41 -40
- package/dist/canvas-fields.js +35 -12
- package/dist/canvas.d.ts +22 -30
- package/dist/canvas.js +57 -64
- package/dist/chart-data.d.ts +10 -10
- package/dist/chart-data.js +21 -21
- package/dist/chart-options-panel.d.ts +2 -2
- package/dist/chart-options-panel.js +82 -8
- package/dist/chart-options.d.ts +35 -9
- package/dist/chart-options.js +133 -40
- package/dist/checks.js +21 -0
- package/dist/color-authoring.js +1 -1
- package/dist/content-actions.d.ts +15 -15
- package/dist/content-actions.js +71 -70
- package/dist/content-controls.js +12 -12
- package/dist/data-grid.js +7 -8
- package/dist/data.d.ts +4 -4
- package/dist/data.js +5 -5
- package/dist/dataset-refs.d.ts +2 -2
- package/dist/dataset-refs.js +5 -5
- package/dist/design-controls.js +61 -44
- package/dist/design-options.d.ts +16 -14
- package/dist/design-options.js +86 -74
- package/dist/edit-helpers.js +8 -7
- package/dist/exact-source.js +5 -5
- package/dist/export.d.ts +5 -7
- package/dist/export.js +40 -14
- package/dist/find-panel.js +5 -5
- package/dist/find-replace.d.ts +3 -3
- package/dist/find-replace.js +23 -20
- package/dist/font-gate.js +49 -58
- package/dist/galleries.js +36 -52
- package/dist/grid-model.d.ts +25 -27
- package/dist/grid-model.js +92 -102
- package/dist/image-crop.d.ts +4 -4
- package/dist/image-crop.js +20 -20
- package/dist/image-cropper.js +3 -3
- package/dist/index.d.ts +28 -28
- package/dist/index.js +74 -90
- package/dist/json-options.js +17 -16
- package/dist/layout-handles.js +5 -5
- package/dist/layout-placeholders.js +4 -4
- package/dist/layout.d.ts +2 -2
- package/dist/layout.js +8 -7
- package/dist/numbering-panel.js +3 -5
- package/dist/numbering.d.ts +3 -5
- package/dist/numbering.js +14 -26
- package/dist/outline-view.js +24 -24
- package/dist/outline.d.ts +12 -12
- package/dist/outline.js +52 -50
- package/dist/persistence.d.ts +1 -1
- package/dist/persistence.js +19 -18
- package/dist/presentation-ids.js +2 -2
- package/dist/react.js +3 -3
- package/dist/review-panel.d.ts +30 -13
- package/dist/review-panel.js +154 -53
- package/dist/review.d.ts +36 -43
- package/dist/review.js +92 -53
- package/dist/rich-text-input.js +4 -3
- package/dist/rich-text-toolbar.js +5 -3
- package/dist/rich-text.js +5 -2
- package/dist/schema-inspector.d.ts +2 -2
- package/dist/schema-inspector.js +16 -11
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +25 -5
- package/dist/slide-manager.d.ts +2 -2
- package/dist/slide-manager.js +18 -18
- package/dist/slides.d.ts +20 -20
- package/dist/slides.js +99 -97
- package/dist/svelte.js +1 -1
- package/dist/switches.d.ts +10 -10
- package/dist/switches.js +124 -114
- package/dist/table-options.d.ts +8 -8
- package/dist/table-options.js +28 -27
- package/dist/table-structure.d.ts +10 -10
- package/dist/table-structure.js +21 -21
- package/dist/template-panel.d.ts +3 -3
- package/dist/template-panel.js +37 -13
- package/dist/templates.d.ts +22 -11
- package/dist/templates.js +48 -55
- package/dist/transfer.d.ts +4 -4
- package/dist/transfer.js +49 -59
- package/package.json +8 -6
- package/dist/font-defaults.js +0 -25
package/dist/chart-options.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
export declare function
|
|
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 };
|
package/dist/chart-options.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
// Chart options (RR-35): axis titles, legend position and data labels
|
|
2
|
-
// `{ type, data, axisTitles?, legend?, dataLabels? }`. These helpers read the
|
|
1
|
+
// Chart options (RR-35): axis titles, legend position and data labels, plus (FA-09) the chart's text alternative `alt` and (FA-14) the
|
|
2
|
+
// highlight. A chart is `{ type, data, alt?, axisTitles?, legend?, dataLabels?, highlight? }`. These helpers read the fields as form
|
|
3
3
|
// state and edit them as one validated, undoable patch (a few add / replace / remove operations
|
|
4
4
|
// on the chart, one undo step), so the canvas redraws the legend, titles and labels and Undo
|
|
5
5
|
// restores the chart. Which fields a chart offers follows core's support table
|
|
6
6
|
// (`chartOptionSupport`): a pie has no axis titles, area and radar labels have no position
|
|
7
|
-
// choice, and only a pie or doughnut can show percent.
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
// choice, and only a pie or doughnut can show percent. A highlight names plotted series and category labels (the
|
|
8
|
+
// choices come from the chart's resolved data, so a dataset chart offers its dataset's columns and rows); a pie
|
|
9
|
+
// highlights slices (categories), an area only series. Nothing here invents text. A combo chart (FA-15) also offers which series are lines (`line`), which lines use the secondary value
|
|
10
|
+
// axis (`secondaryAxis`) and that axis's title.
|
|
11
|
+
import { resolveChartData } from "@openpresentation/opf";
|
|
12
|
+
import { chartOptionSupport, chartOptionTarget } from "@openpresentation/opf/composition";
|
|
13
|
+
import { getValueAtPath, opfPathToJsonPointer, splitOpfPath } from "./index.js";
|
|
10
14
|
import { checkedDocument, fail, same } from "./edit-helpers.js";
|
|
15
|
+
import { checkFormat } from "./checks.js";
|
|
11
16
|
|
|
12
17
|
export const CHART_LEGEND_POSITIONS = Object.freeze(["default", "none", "top", "bottom", "left", "right"]);
|
|
13
18
|
export const CHART_LABEL_CONTENT = Object.freeze(["category", "value", "percent"]);
|
|
@@ -16,11 +21,6 @@ const isObject = (value) => Boolean(value) && typeof value === "object" && !Arra
|
|
|
16
21
|
const CONTENT_ORDER = CHART_LABEL_CONTENT;
|
|
17
22
|
const POSITIONS = new Set(["center", "inside-end", "inside-base", "outside-end", "above", "below", "left", "right"]);
|
|
18
23
|
|
|
19
|
-
/** True when the installed core knows the chart option fields; an older core leaves the panel out. */
|
|
20
|
-
export function chartOptionsAvailable() {
|
|
21
|
-
return typeof core.chartOptionSupport === "function" && typeof core.chartOptionTarget === "function" && typeof core.resolveChartOptions === "function";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
24
|
/** The chart path a selection path points at (`…chart`, `…chart.data.rows.0.1`), or undefined. */
|
|
25
25
|
export function parseChartPath(path) {
|
|
26
26
|
let parts;
|
|
@@ -33,38 +33,64 @@ export function parseChartPath(path) {
|
|
|
33
33
|
return index >= 1 ? parts.slice(0, index + 1).join(".") : undefined;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function chartAt(
|
|
36
|
+
function chartAt(presentation, chartPath) {
|
|
37
37
|
const parts = splitOpfPath(chartPath);
|
|
38
|
-
const chart = getValueAtPath(
|
|
38
|
+
const chart = getValueAtPath(presentation, parts);
|
|
39
39
|
if (!isObject(chart) || typeof chart.type !== "string") throw fail("chart-not-found", "Choose a chart (a path ending in .chart).", { chartPath });
|
|
40
40
|
return { parts, chart };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// The names a highlight can take, from the chart's resolved data: the plotted series (the columns after the category and, on a
|
|
44
|
+
// scatter chart, the X column) and the distinct non-empty row labels. Empty when the data does not resolve.
|
|
45
|
+
function highlightChoices(chart, document) {
|
|
46
|
+
const none = { series: [], categories: [] };
|
|
47
|
+
let resolved;
|
|
48
|
+
try { resolved = resolveChartData(chart, document); } catch { return none; }
|
|
49
|
+
if (!resolved?.ok || resolved.columns.length < 2) return none;
|
|
50
|
+
const first = resolved.hasX ? 2 : 1;
|
|
51
|
+
const label = (cell) => (cell === null || cell === undefined ? "" : String(cell));
|
|
52
|
+
return { series: resolved.columns.slice(first), categories: [...new Set(resolved.rows.map((row) => label(row[0])).filter(Boolean))] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const names = (value) => (Array.isArray(value) ? [...new Set(value.filter((entry) => typeof entry === "string"))] : []);
|
|
56
|
+
|
|
43
57
|
/**
|
|
44
|
-
* What the chart options offer for `chart`, and the form state of the
|
|
58
|
+
* What the chart options offer for `chart`, and the form state of the fields. `presentation` (optional) resolves a dataset-backed
|
|
59
|
+
* chart's columns and rows for the highlight choices.
|
|
45
60
|
* `fields.axisTitles.category` and `.value` say whether the type has that axis; `fields.legend` whether it has a legend;
|
|
46
61
|
* `fields.dataLabels` the contents and positions it accepts (`positions` is empty when labels have no position choice).
|
|
62
|
+
* `fields.axisTitles.secondary` is true on a combo chart, whose `combo.series` lists the plotted series ({ name, role, axis });
|
|
63
|
+
* pass the `presentation` for a dataset-backed chart.
|
|
47
64
|
*/
|
|
48
|
-
export function readChartOptions(chart) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
axisTitles: { category: true, value: true }, legend: true,
|
|
65
|
+
export function readChartOptions(chart, presentation) {
|
|
66
|
+
const target = chartOptionTarget(chart?.type);
|
|
67
|
+
// A chart type outside the catalog is never restricted: every field is offered (a secondary axis exists only on combo charts).
|
|
68
|
+
const support = target ? chartOptionSupport(target) : {
|
|
69
|
+
axisTitles: { category: true, value: true, secondary: false }, legend: true,
|
|
54
70
|
dataLabels: { supported: true, content: CONTENT_ORDER, positions: [...POSITIONS], defaultPosition: null, defaultOn: false },
|
|
71
|
+
highlight: { series: true, categories: true },
|
|
55
72
|
};
|
|
56
73
|
const titles = isObject(chart?.axisTitles) ? chart.axisTitles : {};
|
|
57
74
|
const labels = chart?.dataLabels;
|
|
58
75
|
const labelObject = isObject(labels) ? labels : {};
|
|
76
|
+
const combo = target?.kind === "combo" ? comboSeries(chart, presentation) : undefined;
|
|
59
77
|
return {
|
|
60
78
|
target,
|
|
61
79
|
fields: {
|
|
62
|
-
axisTitles: { ...support.axisTitles },
|
|
80
|
+
axisTitles: { ...support.axisTitles, secondary: support.axisTitles.secondary === true },
|
|
63
81
|
legend: support.legend,
|
|
64
82
|
dataLabels: { ...support.dataLabels, content: [...support.dataLabels.content], positions: [...support.dataLabels.positions] },
|
|
83
|
+
highlight: { ...support.highlight },
|
|
65
84
|
},
|
|
85
|
+
// FA-15: a combo chart's plotted series, each drawn as columns ("bar") or as a line, on the primary or secondary value axis.
|
|
86
|
+
...(combo ? { combo: { series: combo } } : {}),
|
|
87
|
+
choices: highlightChoices(chart, presentation),
|
|
66
88
|
state: {
|
|
67
|
-
|
|
89
|
+
// FA-09: the text alternative. `decorative` is the empty alt, a reviewed choice, not a missing one.
|
|
90
|
+
alt: typeof chart?.alt === "string" ? chart.alt : "",
|
|
91
|
+
decorative: chart?.alt === "",
|
|
92
|
+
highlight: { series: names(chart?.highlight?.series), categories: names(chart?.highlight?.categories) },
|
|
93
|
+
axisTitles: { category: typeof titles.category === "string" ? titles.category : "", value: typeof titles.value === "string" ? titles.value : "", secondary: typeof titles.secondary === "string" ? titles.secondary : "" },
|
|
68
94
|
legend: typeof chart?.legend === "string" ? chart.legend : "default",
|
|
69
95
|
dataLabels: {
|
|
70
96
|
// Funnel and treemap label their marks by default, so `dataLabels: false` is what switches them off.
|
|
@@ -78,18 +104,64 @@ export function readChartOptions(chart) {
|
|
|
78
104
|
};
|
|
79
105
|
}
|
|
80
106
|
|
|
107
|
+
/**
|
|
108
|
+
* FA-15: a combo chart's plotted series ({ name, role, axis }) in the data's plotted order (the mapping, else the columns), with
|
|
109
|
+
* the role and axis core resolves for each; [] when the data does not resolve.
|
|
110
|
+
*/
|
|
111
|
+
function comboSeries(chart, document) {
|
|
112
|
+
const resolved = resolveChartData(chart, document);
|
|
113
|
+
if (!resolved.ok || !Array.isArray(resolved.combo)) return [];
|
|
114
|
+
const plan = new Map(resolved.columns.slice(1).map((name, index) => [name, resolved.combo[index]]));
|
|
115
|
+
const { line: _line, secondaryAxis: _secondary, ...plain } = chart;
|
|
116
|
+
const plotted = resolveChartData({ ...plain, type: "column" }, document);
|
|
117
|
+
const names = plotted.ok ? plotted.columns.slice(1) : [...plan.keys()];
|
|
118
|
+
return names.filter((name) => plan.has(name)).map((name) => ({ name, role: plan.get(name)?.role ?? "bar", axis: plan.get(name)?.axis ?? "primary" }));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// FA-15: `line` and `secondaryAxis` for a combo chart from the wanted line and secondary-axis names. `line` is written only when
|
|
122
|
+
// it differs from the default (the last plotted series), names keep the data's order, and only a line can use the secondary axis.
|
|
123
|
+
function comboFields(chart, change, document) {
|
|
124
|
+
const series = comboSeries(chart, document);
|
|
125
|
+
const plotted = series.map((entry) => entry.name);
|
|
126
|
+
const currentLines = series.filter((entry) => entry.role === "line").map((entry) => entry.name);
|
|
127
|
+
const currentSecondary = series.filter((entry) => entry.axis === "secondary").map((entry) => entry.name);
|
|
128
|
+
const wanted = (value, current, field) => {
|
|
129
|
+
if (value === undefined) return current;
|
|
130
|
+
if (!Array.isArray(value) || value.some((name) => typeof name !== "string")) throw fail("invalid-chart-option", `${field} is a list of series names.`, { [field]: value });
|
|
131
|
+
const unknown = value.filter((name) => !plotted.includes(name));
|
|
132
|
+
if (unknown.length) throw fail("invalid-chart-option", `${unknown.map((name) => `'${name}'`).join(", ")} ${unknown.length === 1 ? "is not a plotted series" : "are not plotted series"} of this chart.`, { [field]: value });
|
|
133
|
+
return value;
|
|
134
|
+
};
|
|
135
|
+
const lines = wanted(change.line, currentLines, "line");
|
|
136
|
+
if (!lines.length) throw fail("invalid-chart-option", "A combo chart draws at least one series as a line; choose a column chart for columns only.", { line: change.line });
|
|
137
|
+
if (lines.length >= plotted.length) throw fail("invalid-chart-option", "A combo chart keeps at least one series as columns.", { line: change.line });
|
|
138
|
+
const ordered = plotted.filter((name) => lines.includes(name));
|
|
139
|
+
const secondary = plotted.filter((name) => ordered.includes(name) && wanted(change.secondaryAxis, currentSecondary, "secondaryAxis").includes(name));
|
|
140
|
+
const defaultLine = ordered.length === 1 && ordered[0] === plotted[plotted.length - 1];
|
|
141
|
+
return { line: defaultLine ? undefined : ordered, secondaryAxis: secondary.length ? secondary : undefined };
|
|
142
|
+
}
|
|
143
|
+
|
|
81
144
|
// The desired value of each field (undefined removes it), from the chart and a patch.
|
|
82
|
-
function desired(chart, change, support) {
|
|
145
|
+
function desired(chart, change, support, choices, current, document) {
|
|
83
146
|
const out = {};
|
|
84
|
-
const
|
|
85
|
-
if (change.
|
|
147
|
+
const combo = support.axisTitles.secondary === true;
|
|
148
|
+
if (change.line !== undefined || change.secondaryAxis !== undefined) {
|
|
149
|
+
// Only a combo chart has line series; elsewhere the fields are removed.
|
|
150
|
+
Object.assign(out, combo ? comboFields(chart, change, document) : { line: undefined, secondaryAxis: undefined });
|
|
151
|
+
}
|
|
152
|
+
// The secondary axis title exists only while a series uses the secondary axis.
|
|
153
|
+
const hasSecondary = combo && (Object.hasOwn(out, "secondaryAxis") ? out.secondaryAxis !== undefined : Array.isArray(chart.secondaryAxis) && chart.secondaryAxis.length > 0);
|
|
154
|
+
if (change.axisTitles !== undefined || (current.axisTitles.secondary && !hasSecondary)) {
|
|
86
155
|
const titles = {};
|
|
87
|
-
for (const axis of ["category", "value"]) {
|
|
156
|
+
for (const axis of ["category", "value", "secondary"]) {
|
|
88
157
|
const value = change.axisTitles?.[axis] !== undefined ? change.axisTitles[axis] : current.axisTitles[axis];
|
|
89
|
-
if (typeof value === "string" && value.trim() && support.axisTitles[axis]) titles[axis] = value.trim();
|
|
158
|
+
if (typeof value === "string" && value.trim() && support.axisTitles[axis] && (axis !== "secondary" || hasSecondary)) titles[axis] = value.trim();
|
|
90
159
|
}
|
|
91
160
|
out.axisTitles = Object.keys(titles).length ? titles : undefined;
|
|
92
161
|
}
|
|
162
|
+
if (change.decorative === true) out.alt = "";
|
|
163
|
+
else if (change.alt !== undefined) out.alt = typeof change.alt === "string" && change.alt.trim() ? change.alt.trim() : undefined;
|
|
164
|
+
else if (change.decorative === false && chart.alt === "") out.alt = undefined;
|
|
93
165
|
if (change.legend !== undefined) {
|
|
94
166
|
if (change.legend !== "default" && !CHART_LEGEND_POSITIONS.includes(change.legend)) throw fail("invalid-chart-option", `'${change.legend}' is not a legend position.`, { legend: change.legend });
|
|
95
167
|
out.legend = change.legend === "default" || !support.legend ? undefined : change.legend;
|
|
@@ -112,22 +184,43 @@ function desired(chart, change, support) {
|
|
|
112
184
|
out.dataLabels = Object.keys(next).length ? next : true;
|
|
113
185
|
}
|
|
114
186
|
}
|
|
187
|
+
if (change.highlight !== undefined) {
|
|
188
|
+
if (change.highlight === null) out.highlight = undefined;
|
|
189
|
+
else {
|
|
190
|
+
const next = {};
|
|
191
|
+
for (const part of ["series", "categories"]) {
|
|
192
|
+
const requested = change.highlight[part] !== undefined ? change.highlight[part] : current.highlight[part];
|
|
193
|
+
if (!Array.isArray(requested) || requested.some((entry) => typeof entry !== "string")) throw fail("invalid-chart-option", `Highlight ${part} must be a list of names.`, { part });
|
|
194
|
+
const listed = names(requested);
|
|
195
|
+
// A part the chart type cannot highlight is never written (a pie highlights slices, not series).
|
|
196
|
+
if (!support.highlight[part] || !listed.length) continue;
|
|
197
|
+
const unknown = choices[part].length ? listed.find((entry) => !choices[part].includes(entry)) : undefined;
|
|
198
|
+
if (unknown !== undefined) throw fail("invalid-chart-option", `'${unknown}' is not a ${part === "series" ? "plotted series" : "category label"} of this chart.`, { part, name: unknown });
|
|
199
|
+
next[part] = listed;
|
|
200
|
+
}
|
|
201
|
+
out.highlight = Object.keys(next).length ? next : undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
115
204
|
return out;
|
|
116
205
|
}
|
|
117
206
|
|
|
118
207
|
/**
|
|
119
208
|
* Prepare one validated patch for a change to the chart's options. `change` is
|
|
120
|
-
* `{ axisTitles?: { category?, value? }, legend?, dataLabels? }`: `
|
|
209
|
+
* `{ alt?, decorative?, axisTitles?: { category?, value?, secondary? }, legend?, dataLabels?, highlight?, line?, secondaryAxis? }`: `alt` is the text alternative (trimmed; empty or `null`
|
|
210
|
+
* removes it), `decorative: true` writes the empty alt (and wins over `alt`), `decorative: false` removes an empty alt; `axisTitles` entries are strings (empty removes a title),
|
|
121
211
|
* `legend` is `"default"` (remove the field), `"none"`, `"top"`, `"bottom"`, `"left"` or `"right"`, and `dataLabels` is
|
|
122
|
-
* `true`, `false` (or `null`, which removes the field), or `{ content?, position?, separator? }` merged over the current labels
|
|
212
|
+
* `true`, `false` (or `null`, which removes the field), or `{ content?, position?, separator? }` merged over the current labels, and
|
|
213
|
+
* `highlight` is `null` (remove the field) or `{ series?, categories? }`: each list replaces that part (an empty list removes it, a name the
|
|
214
|
+
* chart does not have is refused) and the other part is kept. On a combo chart (FA-15), `line` lists the series drawn as lines and
|
|
215
|
+
* `secondaryAxis` the lines on the secondary value axis, by name; at least one series stays columns and at least one is a line, `line`
|
|
216
|
+
* is written only when it differs from the default (the last series), and the secondary axis title goes when no series uses that axis.
|
|
123
217
|
* Fields the chart type cannot show are never written. The document is not modified.
|
|
124
218
|
*/
|
|
125
|
-
export function prepareChartOptions(
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const wanted = desired(chart, change, support);
|
|
219
|
+
export function prepareChartOptions(presentation, chartPath, change) {
|
|
220
|
+
const before = checkFormat(presentation);
|
|
221
|
+
const { parts, chart } = chartAt(presentation, chartPath);
|
|
222
|
+
const read = readChartOptions(chart, presentation);
|
|
223
|
+
const wanted = desired(chart, change, read.fields, read.choices, read.state, presentation);
|
|
131
224
|
const patches = [];
|
|
132
225
|
for (const [key, value] of Object.entries(wanted)) {
|
|
133
226
|
const path = opfPathToJsonPointer([...parts, key]);
|
|
@@ -137,8 +230,8 @@ export function prepareChartOptions(document, chartPath, change) {
|
|
|
137
230
|
} else if (!present) patches.push({ op: "add", path, value: structuredClone(value) });
|
|
138
231
|
else if (!same(chart[key], value)) patches.push({ op: "replace", path, value: structuredClone(value) });
|
|
139
232
|
}
|
|
140
|
-
const next = checkedDocument(
|
|
141
|
-
return { action: "chart-options", chartPath, changed: patches.length > 0, patches,
|
|
233
|
+
const next = checkedDocument(presentation, patches, before);
|
|
234
|
+
return { action: "chart-options", chartPath, changed: patches.length > 0, patches, presentation: next };
|
|
142
235
|
}
|
|
143
236
|
|
|
144
237
|
function checkEditor(editor) {
|
|
@@ -148,10 +241,10 @@ function checkEditor(editor) {
|
|
|
148
241
|
/** Apply a chart option change to an editor session as one undoable edit. Returns the editor change. */
|
|
149
242
|
export function setChartOptions(editor, chartPath, change, meta = {}) {
|
|
150
243
|
checkEditor(editor);
|
|
151
|
-
const prepared = prepareChartOptions(editor.
|
|
152
|
-
const {
|
|
153
|
-
void
|
|
154
|
-
if (!prepared.changed) return { ...summary,
|
|
244
|
+
const prepared = prepareChartOptions(editor.presentation, chartPath, change);
|
|
245
|
+
const { presentation, patches, ...summary } = prepared;
|
|
246
|
+
void presentation;
|
|
247
|
+
if (!prepared.changed) return { ...summary, presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
|
|
155
248
|
const applied = editor.applyPatch(patches, { ...meta, source: meta.source ?? "chart-option", action: prepared.action, path: chartPath });
|
|
156
249
|
return { ...applied, ...summary };
|
|
157
250
|
}
|
package/dist/checks.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The editor's one use of core's checker. Every edit is validated with `validate(presentation, { only: ["format"] })`: the
|
|
2
|
+
// schema and semantic OPF rules, with no layout built, so it is cheap enough to run on every keystroke and undo. The full
|
|
3
|
+
// report (accessibility, layout, content) belongs to the Review panel, which runs it on idle (`./review.js`).
|
|
4
|
+
import { validate } from "@openpresentation/opf";
|
|
5
|
+
|
|
6
|
+
const FORMAT_ONLY = Object.freeze({ only: Object.freeze(["format"]) });
|
|
7
|
+
|
|
8
|
+
/** Core's `ValidationReport` for the `format` category: `valid`, `findings`, `counts`, `schemaValid`. */
|
|
9
|
+
export function checkFormat(presentation) {
|
|
10
|
+
return validate(presentation, FORMAT_ONLY);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The error-severity findings of a report: what makes it invalid. */
|
|
14
|
+
export function errorFindings(report) {
|
|
15
|
+
return (report?.findings ?? []).filter((finding) => finding.severity === "error");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The message of the first error finding, or `fallback` when the report has none. */
|
|
19
|
+
export function firstErrorMessage(report, fallback) {
|
|
20
|
+
return errorFindings(report)[0]?.message ?? fallback;
|
|
21
|
+
}
|
package/dist/color-authoring.js
CHANGED
|
@@ -6,7 +6,7 @@ const SCHEME_SLOTS = new Set([
|
|
|
6
6
|
]);
|
|
7
7
|
|
|
8
8
|
const ROLE_KEYS = new Set([
|
|
9
|
-
"primary", "secondary", "accent", "background", "surface", "text", "textSecondary",
|
|
9
|
+
"primary", "secondary", "accent", "background", "surface", "surfaceAlt", "text", "textSecondary",
|
|
10
10
|
]);
|
|
11
11
|
|
|
12
12
|
/** Whether a toolbar-authored text color matches documented ColorRef / TextRun forms. */
|
|
@@ -7,7 +7,7 @@ export type SlidePages = readonly (PaginatedPage & { sourceSlideIndex: number })
|
|
|
7
7
|
|
|
8
8
|
/** What every prepare function returns without touching a session. A refusal throws `content-action-refused`. */
|
|
9
9
|
export interface PreparedContentAction {
|
|
10
|
-
|
|
10
|
+
presentation: unknown;
|
|
11
11
|
/** A `test` of what was read, then `replace`, or per-slide `remove` and `add`. Empty when nothing changed. */
|
|
12
12
|
patches: JsonPatchOperation[];
|
|
13
13
|
/** The path of the block or slide the change touched: select it again after applying. */
|
|
@@ -27,8 +27,8 @@ export interface PreparedContentAction {
|
|
|
27
27
|
pages?: (PaginatedPage & { sourceSlideIndex: number })[];
|
|
28
28
|
}
|
|
29
29
|
/** The session change of an applied action, with the same report fields. One undo step. */
|
|
30
|
-
export interface ContentActionChange extends Omit<EditorChange, "
|
|
31
|
-
|
|
30
|
+
export interface ContentActionChange extends Omit<EditorChange, "presentation" | "patches"> {
|
|
31
|
+
presentation: unknown;
|
|
32
32
|
patches: JsonPatchOperation[];
|
|
33
33
|
changed: boolean;
|
|
34
34
|
lossless: boolean;
|
|
@@ -53,17 +53,17 @@ export interface ListShiftOptions extends DryRunOptions {
|
|
|
53
53
|
|
|
54
54
|
/** The index of the list item a selection points at (`slides.0.blocks.1.items.2.text` gives 2), or undefined. */
|
|
55
55
|
export declare function listItemIndexForSelection(selectedPath: string, blockPath: string): number | undefined;
|
|
56
|
-
export declare function prepareListShift(
|
|
56
|
+
export declare function prepareListShift(presentation: unknown, blockPath: string, indices: number[], delta: number, options?: ListShiftOptions): PreparedContentAction;
|
|
57
57
|
/** Nest (`delta` 1) or un-nest (`delta` -1) list items as one undoable step. */
|
|
58
58
|
export declare function shiftListItems(editor: EditorSession, blockPath: string, indices: number[], delta: number, options?: ListShiftOptions, meta?: Record<string, unknown>): ContentActionChange;
|
|
59
59
|
|
|
60
|
-
export declare function prepareGroupBlocks(
|
|
61
|
-
export declare function prepareUngroupBlock(
|
|
62
|
-
export declare function prepareBlocksToRegions(
|
|
63
|
-
export declare function prepareRegionsToBlocks(
|
|
64
|
-
export declare function prepareMoveRegion(
|
|
65
|
-
export declare function prepareImageToDesign(
|
|
66
|
-
export declare function prepareImageToContent(
|
|
60
|
+
export declare function prepareGroupBlocks(presentation: unknown, containerPath: string, indices: number[], options?: DryRunOptions & { composition?: Record<string, unknown> }): PreparedContentAction;
|
|
61
|
+
export declare function prepareUngroupBlock(presentation: unknown, groupPath: string, options?: DryRunOptions): PreparedContentAction;
|
|
62
|
+
export declare function prepareBlocksToRegions(presentation: unknown, slideIndex: number, regions: string[], options?: DryRunOptions): PreparedContentAction;
|
|
63
|
+
export declare function prepareRegionsToBlocks(presentation: unknown, slideIndex: number, options?: DryRunOptions): PreparedContentAction;
|
|
64
|
+
export declare function prepareMoveRegion(presentation: unknown, slideIndex: number, from: string, to: string, options?: DryRunOptions & { swap?: boolean }): PreparedContentAction;
|
|
65
|
+
export declare function prepareImageToDesign(presentation: unknown, blockPath: string, target: ImageTarget, options?: DryRunOptions & PromoteImageOptions): PreparedContentAction;
|
|
66
|
+
export declare function prepareImageToContent(presentation: unknown, slideIndex: number, source: ImageTarget, options?: DryRunOptions & { index?: number; region?: string }): PreparedContentAction;
|
|
67
67
|
|
|
68
68
|
/** Wrap the blocks at `indices` of a slide, group or region group in a new group. */
|
|
69
69
|
export declare function groupBlocks(editor: EditorSession, containerPath: string, indices: number[], options?: { composition?: Record<string, unknown> }, meta?: Record<string, unknown>): ContentActionChange;
|
|
@@ -77,10 +77,10 @@ export declare function moveImageToDesign(editor: EditorSession, blockPath: stri
|
|
|
77
77
|
/** Move a slide's own slide image, background image or watermark back into its content as an image block. */
|
|
78
78
|
export declare function moveImageToContent(editor: EditorSession, slideIndex: number, source: ImageTarget, options?: { index?: number; region?: string }, meta?: Record<string, unknown>): ContentActionChange;
|
|
79
79
|
|
|
80
|
-
export declare function prepareSplitSlide(
|
|
81
|
-
export declare function prepareSplitSlideOnOverflow(
|
|
82
|
-
export declare function prepareMergeSlides(
|
|
83
|
-
export declare function prepareUnpaginate(
|
|
80
|
+
export declare function prepareSplitSlide(presentation: unknown, slideIndex: number, options?: { at?: number[]; each?: boolean; repeatHeadings?: boolean }): PreparedContentAction;
|
|
81
|
+
export declare function prepareSplitSlideOnOverflow(presentation: unknown, slideIndex: number, options?: PresentationPaginationOptions): PreparedContentAction;
|
|
82
|
+
export declare function prepareMergeSlides(presentation: unknown, start: number, count?: number): PreparedContentAction;
|
|
83
|
+
export declare function prepareUnpaginate(presentation: unknown, pages: SlidePages, options?: { sourceSlideIndex?: number }): PreparedContentAction;
|
|
84
84
|
/** Split a slide by its blocks into several slides, as one undoable step. */
|
|
85
85
|
export declare function splitSlideByBlocks(editor: EditorSession, slideIndex: number, options?: { at?: number[]; each?: boolean; repeatHeadings?: boolean }, meta?: Record<string, unknown>): ContentActionChange;
|
|
86
86
|
/** Split a slide that overflows with the existing pagination; the change carries `pages` for `unpaginateSlides`. */
|