@openpresentation/opf-editor 0.10.6 → 0.11.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 (76) hide show
  1. package/README.md +333 -8
  2. package/dist/annotations.d.ts +71 -0
  3. package/dist/annotations.js +281 -0
  4. package/dist/assets.d.ts +67 -0
  5. package/dist/assets.js +176 -0
  6. package/dist/background-options.d.ts +48 -0
  7. package/dist/background-options.js +134 -0
  8. package/dist/block-convert.d.ts +64 -0
  9. package/dist/block-convert.js +142 -0
  10. package/dist/canvas.d.ts +16 -0
  11. package/dist/canvas.js +82 -21
  12. package/dist/chart-data.d.ts +32 -0
  13. package/dist/chart-data.js +101 -0
  14. package/dist/chart-options-panel.d.ts +16 -0
  15. package/dist/chart-options-panel.js +127 -0
  16. package/dist/chart-options.d.ts +49 -0
  17. package/dist/chart-options.js +157 -0
  18. package/dist/content-actions.d.ts +91 -0
  19. package/dist/content-actions.js +207 -0
  20. package/dist/content-controls.js +326 -0
  21. package/dist/data-grid.d.ts +37 -0
  22. package/dist/data-grid.js +1035 -0
  23. package/dist/design-controls.d.ts +43 -0
  24. package/dist/design-controls.js +1077 -0
  25. package/dist/design-options.d.ts +108 -0
  26. package/dist/design-options.js +412 -0
  27. package/dist/edit-helpers.js +52 -0
  28. package/dist/export.d.ts +77 -0
  29. package/dist/export.js +216 -0
  30. package/dist/find-panel.d.ts +44 -0
  31. package/dist/find-panel.js +431 -0
  32. package/dist/find-replace.d.ts +100 -0
  33. package/dist/find-replace.js +374 -0
  34. package/dist/grid-model.d.ts +135 -0
  35. package/dist/grid-model.js +836 -0
  36. package/dist/grid-text.d.ts +33 -0
  37. package/dist/grid-text.js +251 -0
  38. package/dist/image-crop.d.ts +59 -0
  39. package/dist/image-crop.js +336 -0
  40. package/dist/image-cropper.d.ts +29 -0
  41. package/dist/image-cropper.js +519 -0
  42. package/dist/index.d.ts +11 -1
  43. package/dist/index.js +104 -171
  44. package/dist/numbering-panel.d.ts +21 -0
  45. package/dist/numbering-panel.js +200 -0
  46. package/dist/numbering.d.ts +62 -0
  47. package/dist/numbering.js +223 -0
  48. package/dist/outline-view.d.ts +17 -0
  49. package/dist/outline-view.js +278 -0
  50. package/dist/outline.d.ts +56 -0
  51. package/dist/outline.js +271 -0
  52. package/dist/persistence-ui.d.ts +24 -0
  53. package/dist/persistence-ui.js +81 -0
  54. package/dist/persistence.d.ts +105 -0
  55. package/dist/persistence.js +429 -0
  56. package/dist/review-panel.d.ts +44 -0
  57. package/dist/review-panel.js +359 -0
  58. package/dist/review.d.ts +75 -0
  59. package/dist/review.js +170 -0
  60. package/dist/slide-manager.d.ts +44 -0
  61. package/dist/slide-manager.js +695 -0
  62. package/dist/slides.d.ts +96 -0
  63. package/dist/slides.js +433 -0
  64. package/dist/switches.d.ts +26 -0
  65. package/dist/switches.js +127 -43
  66. package/dist/table-options.d.ts +80 -0
  67. package/dist/table-options.js +419 -0
  68. package/dist/table-structure.d.ts +30 -0
  69. package/dist/table-structure.js +92 -0
  70. package/dist/template-panel.d.ts +31 -0
  71. package/dist/template-panel.js +377 -0
  72. package/dist/templates.d.ts +126 -0
  73. package/dist/templates.js +331 -0
  74. package/dist/zip.d.ts +4 -0
  75. package/dist/zip.js +71 -0
  76. package/package.json +150 -10
@@ -0,0 +1,101 @@
1
+ // Chart data (RR-24): edit the inline data of a chart, `{ type, data: { columns, rows } }`. The first column holds the categories
2
+ // and every further column is a series named by its column label (a scatter chart with three or more columns reads x values from the
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
+ // one validated, undoable patch (grid-model.js has the implementation; grid-text.js documents number reading).
5
+ //
6
+ // `prepare*` computes `{ document, patches, changed }` without touching a session; the session forms apply it as one undo step.
7
+ import {
8
+ prepareGridCells,
9
+ preparePaste,
10
+ prepareInsertRows,
11
+ prepareDeleteRows,
12
+ prepareMoveRows,
13
+ prepareInsertColumns,
14
+ prepareDeleteColumns,
15
+ prepareMoveColumns,
16
+ prepareSortRows,
17
+ setGridCells,
18
+ pasteGridText,
19
+ insertGridRows,
20
+ deleteGridRows,
21
+ moveGridRows,
22
+ insertGridColumns,
23
+ deleteGridColumns,
24
+ moveGridColumns,
25
+ sortGridRows,
26
+ prepareTranspose,
27
+ transposeGridData,
28
+ } from "./grid-model.js";
29
+ export function prepareChartCells(document, chartPath, edits, options = {}) {
30
+ return prepareGridCells(document, chartPath, edits, { ...options, kind: "chart" });
31
+ }
32
+ export function setChartCells(editor, chartPath, edits, options = {}) {
33
+ return setGridCells(editor, chartPath, edits, { ...options, kind: "chart" });
34
+ }
35
+ export function prepareChartPaste(document, chartPath, anchor, source, options = {}) {
36
+ return preparePaste(document, chartPath, anchor, source, { ...options, kind: "chart" });
37
+ }
38
+ export function pasteChartText(editor, chartPath, anchor, source, options = {}) {
39
+ return pasteGridText(editor, chartPath, anchor, source, { ...options, kind: "chart" });
40
+ }
41
+ export function prepareChartInsertRows(document, chartPath, at, count = 1, options = {}) {
42
+ return prepareInsertRows(document, chartPath, at, count, { ...options, kind: "chart" });
43
+ }
44
+ export function insertChartRows(editor, chartPath, at, count = 1, options = {}) {
45
+ return insertGridRows(editor, chartPath, at, count, { ...options, kind: "chart" });
46
+ }
47
+ export function prepareChartDeleteRows(document, chartPath, indices, options = {}) {
48
+ return prepareDeleteRows(document, chartPath, indices, { ...options, kind: "chart" });
49
+ }
50
+ export function deleteChartRows(editor, chartPath, indices, options = {}) {
51
+ return deleteGridRows(editor, chartPath, indices, { ...options, kind: "chart" });
52
+ }
53
+ export function prepareChartMoveRows(document, chartPath, from, to, count = 1, options = {}) {
54
+ return prepareMoveRows(document, chartPath, from, to, count, { ...options, kind: "chart" });
55
+ }
56
+ export function moveChartRows(editor, chartPath, from, to, count = 1, options = {}) {
57
+ return moveGridRows(editor, chartPath, from, to, count, { ...options, kind: "chart" });
58
+ }
59
+ export function prepareChartInsertColumns(document, chartPath, at, count = 1, options = {}) {
60
+ return prepareInsertColumns(document, chartPath, at, count, { ...options, kind: "chart" });
61
+ }
62
+ export function insertChartColumns(editor, chartPath, at, count = 1, options = {}) {
63
+ return insertGridColumns(editor, chartPath, at, count, { ...options, kind: "chart" });
64
+ }
65
+ export function prepareChartDeleteColumns(document, chartPath, indices, options = {}) {
66
+ return prepareDeleteColumns(document, chartPath, indices, { ...options, kind: "chart" });
67
+ }
68
+ export function deleteChartColumns(editor, chartPath, indices, options = {}) {
69
+ return deleteGridColumns(editor, chartPath, indices, { ...options, kind: "chart" });
70
+ }
71
+ export function prepareChartMoveColumns(document, chartPath, from, to, count = 1, options = {}) {
72
+ return prepareMoveColumns(document, chartPath, from, to, count, { ...options, kind: "chart" });
73
+ }
74
+ export function moveChartColumns(editor, chartPath, from, to, count = 1, options = {}) {
75
+ return moveGridColumns(editor, chartPath, from, to, count, { ...options, kind: "chart" });
76
+ }
77
+ export function prepareChartSort(document, chartPath, column, options = {}) {
78
+ return prepareSortRows(document, chartPath, column, { ...options, kind: "chart" });
79
+ }
80
+ export function sortChartRows(editor, chartPath, column, options = {}) {
81
+ return sortGridRows(editor, chartPath, column, { ...options, kind: "chart" });
82
+ }
83
+
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);
87
+ }
88
+ export function transposeChart(editor, chartPath, options = {}) {
89
+ return transposeGridData(editor, chartPath, options);
90
+ }
91
+
92
+ /** Rename a series: the label of data column `column` (1 or more). */
93
+ export function renameChartSeries(editor, chartPath, column, name, options = {}) {
94
+ if (!Number.isInteger(column) || column < 1) throw new TypeError("A series is a column after the first.");
95
+ return setGridCells(editor, chartPath, [{ section: "header", column, value: String(name) }], { ...options, kind: "chart" });
96
+ }
97
+
98
+ /** Rename a category: the first-column label of body row `row`. An empty name leaves the category blank. */
99
+ export function renameChartCategory(editor, chartPath, row, name, options = {}) {
100
+ return setGridCells(editor, chartPath, [{ section: "body", row, column: 0, value: name === "" || name === null ? null : String(name) }], { ...options, kind: "chart" });
101
+ }
@@ -0,0 +1,16 @@
1
+ import type { EditorSession } from "./index.js";
2
+
3
+ export interface ChartOptionsPanelOptions {
4
+ editor: EditorSession;
5
+ /** The selected OPF path; any path inside a chart selects it. */
6
+ getSelectedPath: () => string | undefined;
7
+ onStatus?: (message: string) => void;
8
+ }
9
+ export interface ChartOptionsPanel {
10
+ /** The panel's root element; undefined when the installed core has no chart option fields. */
11
+ element: HTMLElement | undefined;
12
+ /** Re-read the selection and the chart; the panel also refreshes on every editor change. */
13
+ refresh(): void;
14
+ destroy(): void;
15
+ }
16
+ export declare function createChartOptionsPanel(host: HTMLElement, options: ChartOptionsPanelOptions): ChartOptionsPanel;
@@ -0,0 +1,127 @@
1
+ // The chart options panel (RR-35): axis titles, legend position and data labels for the selected chart.
2
+ // It mounts into any host element over an editor session; every control commits one undoable change through
3
+ // `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";
6
+
7
+ const LEGEND_LABELS = { default: "Default", none: "None", top: "Top", bottom: "Bottom", left: "Left", right: "Right" };
8
+ const CONTENT_LABELS = { category: "Category", value: "Value", percent: "Percent" };
9
+ 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
+
11
+ let counter = 0;
12
+
13
+ /**
14
+ * Mount the panel in `host`. `getSelectedPath()` returns the selected OPF path (any path inside a chart selects it);
15
+ * `onStatus(message)` receives a short message after each change. Returns `{ element, refresh, destroy }`;
16
+ * call `refresh()` when the selection changes (the panel also refreshes on every editor change).
17
+ */
18
+ export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatus = () => {} }) {
19
+ if (!chartOptionsAvailable()) return { refresh() {}, destroy() {}, element: undefined };
20
+ const id = `opf-chart-options-${++counter}`;
21
+ const root = document.createElement("section");
22
+ root.className = "opf-chart-options";
23
+ root.setAttribute("aria-label", "Chart options");
24
+ root.hidden = true;
25
+ root.innerHTML = `
26
+ <h3 class="opf-co-title">Chart options</h3>
27
+ <div class="opf-co-group" data-group="axisTitles">
28
+ <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
+ <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>
30
+ </div>
31
+ <div class="opf-co-group" data-group="legend">
32
+ <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
+ </div>
34
+ <div class="opf-co-group" data-group="dataLabels">
35
+ <label class="opf-co-row opf-co-check"><input type="checkbox" id="${id}-labels" aria-label="Data labels" data-opf-chart-option="dataLabels.on"><span>Data labels</span></label>
36
+ <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
+ <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
+ <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>
39
+ </div>`;
40
+ host.append(root);
41
+ const $ = (selector) => root.querySelector(selector);
42
+ const fields = {
43
+ category: $(`#${id}-category`), value: $(`#${id}-value`), legend: $(`#${id}-legend`), labels: $(`#${id}-labels`),
44
+ position: $(`#${id}-position`), separator: $(`#${id}-separator`), contents: [...root.querySelectorAll("[data-opf-chart-option^='dataLabels.content.']")],
45
+ };
46
+ let chartPath;
47
+
48
+ function refresh() {
49
+ const path = parseChartPath(getSelectedPath?.() ?? "");
50
+ let chart;
51
+ try { chart = path ? editor.get(path) : undefined; } catch { chart = undefined; }
52
+ if (!path || !chart || typeof chart !== "object" || typeof chart.type !== "string") {
53
+ root.hidden = true;
54
+ chartPath = undefined;
55
+ return;
56
+ }
57
+ chartPath = path;
58
+ const { fields: support, state } = readChartOptions(chart);
59
+ root.hidden = false;
60
+ 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];
63
+ setValue(fields[axis], state.axisTitles[axis]);
64
+ }
65
+ $("[data-group='axisTitles']").hidden = !support.axisTitles.category && !support.axisTitles.value;
66
+ $("[data-group='legend']").hidden = !support.legend;
67
+ fields.legend.value = state.legend;
68
+ $("[data-group='dataLabels']").hidden = !support.dataLabels.supported;
69
+ fields.labels.checked = state.dataLabels.on;
70
+ const off = !state.dataLabels.on;
71
+ for (const input of fields.contents) {
72
+ input.closest("label").hidden = !support.dataLabels.content.includes(input.value);
73
+ input.checked = state.dataLabels.content.includes(input.value);
74
+ input.disabled = off;
75
+ }
76
+ const positions = ["auto", ...support.dataLabels.positions];
77
+ $("[data-row='position']").hidden = support.dataLabels.positions.length === 0;
78
+ if (fields.position.dataset.options !== positions.join()) {
79
+ fields.position.innerHTML = positions.map((value) => `<option value="${value}">${POSITION_LABELS[value]}</option>`).join("");
80
+ fields.position.dataset.options = positions.join();
81
+ }
82
+ fields.position.value = positions.includes(state.dataLabels.position) ? state.dataLabels.position : "auto";
83
+ fields.position.disabled = off;
84
+ setValue(fields.separator, state.dataLabels.separator);
85
+ fields.separator.disabled = off || state.dataLabels.content.length < 2;
86
+ $("[data-row='separator']").hidden = support.dataLabels.content.length < 2;
87
+ }
88
+
89
+ function commit(change, message) {
90
+ if (!chartPath) return;
91
+ try {
92
+ const result = setChartOptions(editor, chartPath, change, { source: "chart-options-panel" });
93
+ if (result.changed) onStatus(message);
94
+ } catch (error) {
95
+ onStatus(error?.message ?? String(error));
96
+ }
97
+ refresh();
98
+ }
99
+
100
+ const selectedContent = () => fields.contents.filter((input) => input.checked && !input.closest("label").hidden).map((input) => input.value);
101
+ const listeners = [
102
+ [fields.category, "change", () => commit({ axisTitles: { category: fields.category.value } }, "Changed the category axis title. Undo restores it.")],
103
+ [fields.value, "change", () => commit({ axisTitles: { value: fields.value.value } }, "Changed the value axis title. Undo restores it.")],
104
+ [fields.legend, "change", () => commit({ legend: fields.legend.value }, "Changed the legend. Undo restores it.")],
105
+ [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
+ [fields.position, "change", () => commit({ dataLabels: { position: fields.position.value } }, "Moved the data labels. Undo restores them.")],
107
+ [fields.separator, "change", () => commit({ dataLabels: { separator: fields.separator.value } }, "Changed the label separator. Undo restores it.")],
108
+ ...fields.contents.map((input) => [input, "change", () => {
109
+ const content = selectedContent();
110
+ // A label always shows something: unticking the last part keeps it.
111
+ if (!content.length) { input.checked = true; return; }
112
+ commit({ dataLabels: { content } }, "Changed what the data labels show. Undo restores them.");
113
+ }]),
114
+ ];
115
+ for (const [element, type, handler] of listeners) element.addEventListener(type, handler);
116
+ const unsubscribe = editor.subscribe?.(refresh);
117
+ refresh();
118
+ return {
119
+ element: root,
120
+ refresh,
121
+ destroy() {
122
+ for (const [element, type, handler] of listeners) element.removeEventListener(type, handler);
123
+ if (typeof unsubscribe === "function") unsubscribe();
124
+ root.remove();
125
+ },
126
+ };
127
+ }
@@ -0,0 +1,49 @@
1
+ import type { EditorChange, EditorSession, JsonPatchOperation } from "./index.js";
2
+
3
+ export type ChartLegendChoice = "default" | "none" | "top" | "bottom" | "left" | "right";
4
+ export type ChartLabelContent = "category" | "value" | "percent";
5
+ export type ChartLabelPosition = "center" | "inside-end" | "inside-base" | "outside-end" | "above" | "below" | "left" | "right";
6
+
7
+ export declare const CHART_LEGEND_POSITIONS: readonly ["default", "none", "top", "bottom", "left", "right"];
8
+ export declare const CHART_LABEL_CONTENT: readonly ["category", "value", "percent"];
9
+
10
+ /** What a chart type can show (core's `chartOptionSupport`). */
11
+ export interface ChartOptionFields {
12
+ axisTitles: { category: boolean; value: boolean };
13
+ legend: boolean;
14
+ dataLabels: {
15
+ supported: boolean;
16
+ content: ChartLabelContent[];
17
+ /** Empty when labels have no position choice (area, doughnut, radar, funnel, treemap). */
18
+ positions: ChartLabelPosition[];
19
+ defaultPosition: ChartLabelPosition | null;
20
+ /** True for the constructs that label their marks by default (funnel, treemap). */
21
+ defaultOn: boolean;
22
+ };
23
+ }
24
+ /** The three fields as form state. */
25
+ export interface ChartOptionState {
26
+ axisTitles: { category: string; value: string };
27
+ legend: ChartLegendChoice;
28
+ dataLabels: { on: boolean; explicit: boolean; content: ChartLabelContent[]; position: ChartLabelPosition | "auto"; separator: string };
29
+ }
30
+ export interface ChartOptionChange {
31
+ axisTitles?: { category?: string; value?: string };
32
+ legend?: ChartLegendChoice;
33
+ dataLabels?: boolean | null | { content?: ChartLabelContent[]; position?: ChartLabelPosition | "auto"; separator?: string };
34
+ }
35
+ export interface PreparedChartOptions {
36
+ action: "chart-options";
37
+ chartPath: string;
38
+ changed: boolean;
39
+ patches: JsonPatchOperation[];
40
+ document: unknown;
41
+ }
42
+
43
+ /** True when the installed core knows the chart option fields. */
44
+ export declare function chartOptionsAvailable(): boolean;
45
+ /** The chart path a selection path points at, or undefined. */
46
+ 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;
49
+ export declare function setChartOptions(editor: EditorSession, chartPath: string, change: ChartOptionChange, meta?: Record<string, unknown>): EditorChange & { action: "chart-options"; chartPath: string; changed: boolean };
@@ -0,0 +1,157 @@
1
+ // Chart options (RR-35): axis titles, legend position and data labels. A chart is
2
+ // `{ type, data, axisTitles?, legend?, dataLabels? }`. These helpers read the three fields as form
3
+ // state and edit them as one validated, undoable patch (a few add / replace / remove operations
4
+ // on the chart, one undo step), so the canvas redraws the legend, titles and labels and Undo
5
+ // restores the chart. Which fields a chart offers follows core's support table
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. Nothing here invents text.
8
+ import * as core from "@openpresentation/opf";
9
+ import { getValueAtPath, opfPathToJsonPointer, splitOpfPath, validateOpfDocument } from "./index.js";
10
+ import { checkedDocument, fail, same } from "./edit-helpers.js";
11
+
12
+ export const CHART_LEGEND_POSITIONS = Object.freeze(["default", "none", "top", "bottom", "left", "right"]);
13
+ export const CHART_LABEL_CONTENT = Object.freeze(["category", "value", "percent"]);
14
+
15
+ const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
16
+ const CONTENT_ORDER = CHART_LABEL_CONTENT;
17
+ const POSITIONS = new Set(["center", "inside-end", "inside-base", "outside-end", "above", "below", "left", "right"]);
18
+
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
+ /** The chart path a selection path points at (`…chart`, `…chart.data.rows.0.1`), or undefined. */
25
+ export function parseChartPath(path) {
26
+ let parts;
27
+ try {
28
+ parts = splitOpfPath(path);
29
+ } catch {
30
+ return undefined;
31
+ }
32
+ const index = parts.lastIndexOf("chart");
33
+ return index >= 1 ? parts.slice(0, index + 1).join(".") : undefined;
34
+ }
35
+
36
+ function chartAt(document, chartPath) {
37
+ const parts = splitOpfPath(chartPath);
38
+ const chart = getValueAtPath(document, parts);
39
+ if (!isObject(chart) || typeof chart.type !== "string") throw fail("chart-not-found", "Choose a chart (a path ending in .chart).", { chartPath });
40
+ return { parts, chart };
41
+ }
42
+
43
+ /**
44
+ * What the chart options offer for `chart`, and the form state of the three fields.
45
+ * `fields.axisTitles.category` and `.value` say whether the type has that axis; `fields.legend` whether it has a legend;
46
+ * `fields.dataLabels` the contents and positions it accepts (`positions` is empty when labels have no position choice).
47
+ */
48
+ export function readChartOptions(chart) {
49
+ if (!chartOptionsAvailable()) throw fail("chart-options-unavailable", "The installed @openpresentation/opf does not know the chart option fields.");
50
+ const target = core.chartOptionTarget(chart?.type);
51
+ // A chart type outside the catalog is never restricted: every field is offered.
52
+ const support = target ? core.chartOptionSupport(target) : {
53
+ axisTitles: { category: true, value: true }, legend: true,
54
+ dataLabels: { supported: true, content: CONTENT_ORDER, positions: [...POSITIONS], defaultPosition: null, defaultOn: false },
55
+ };
56
+ const titles = isObject(chart?.axisTitles) ? chart.axisTitles : {};
57
+ const labels = chart?.dataLabels;
58
+ const labelObject = isObject(labels) ? labels : {};
59
+ return {
60
+ target,
61
+ fields: {
62
+ axisTitles: { ...support.axisTitles },
63
+ legend: support.legend,
64
+ dataLabels: { ...support.dataLabels, content: [...support.dataLabels.content], positions: [...support.dataLabels.positions] },
65
+ },
66
+ state: {
67
+ axisTitles: { category: typeof titles.category === "string" ? titles.category : "", value: typeof titles.value === "string" ? titles.value : "" },
68
+ legend: typeof chart?.legend === "string" ? chart.legend : "default",
69
+ dataLabels: {
70
+ // Funnel and treemap label their marks by default, so `dataLabels: false` is what switches them off.
71
+ on: labels === true || isObject(labels) || (labels === undefined && support.dataLabels.defaultOn),
72
+ explicit: labels !== undefined,
73
+ content: Array.isArray(labelObject.content) && labelObject.content.length ? CONTENT_ORDER.filter((part) => labelObject.content.includes(part)) : (labels === undefined && support.dataLabels.defaultOn ? [target?.kind === "treemap" ? "category" : "value"] : ["value"]),
74
+ position: typeof labelObject.position === "string" ? labelObject.position : "auto",
75
+ separator: typeof labelObject.separator === "string" ? labelObject.separator : ", ",
76
+ },
77
+ },
78
+ };
79
+ }
80
+
81
+ // The desired value of each field (undefined removes it), from the chart and a patch.
82
+ function desired(chart, change, support) {
83
+ const out = {};
84
+ const current = readChartOptions(chart).state;
85
+ if (change.axisTitles !== undefined) {
86
+ const titles = {};
87
+ for (const axis of ["category", "value"]) {
88
+ 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();
90
+ }
91
+ out.axisTitles = Object.keys(titles).length ? titles : undefined;
92
+ }
93
+ if (change.legend !== undefined) {
94
+ 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
+ out.legend = change.legend === "default" || !support.legend ? undefined : change.legend;
96
+ }
97
+ if (change.dataLabels !== undefined) {
98
+ const labels = change.dataLabels;
99
+ if (labels === null || labels === false || labels === "off") {
100
+ // false is only meaningful where a construct labels its marks by default; elsewhere no labels is the absence of the field.
101
+ out.dataLabels = support.dataLabels.defaultOn && labels !== null ? false : undefined;
102
+ } else if (!support.dataLabels.supported) {
103
+ out.dataLabels = undefined;
104
+ } else {
105
+ const base = isObject(chart.dataLabels) ? chart.dataLabels : {};
106
+ const merged = labels === true ? {} : { ...base, ...labels };
107
+ const next = {};
108
+ const content = Array.isArray(merged.content) ? CONTENT_ORDER.filter((part) => merged.content.includes(part) && support.dataLabels.content.includes(part)) : [];
109
+ if (content.length && !(content.length === 1 && content[0] === "value")) next.content = content;
110
+ if (typeof merged.position === "string" && merged.position !== "auto" && support.dataLabels.positions.includes(merged.position)) next.position = merged.position;
111
+ if (typeof merged.separator === "string" && merged.separator !== ", " && merged.separator !== "") next.separator = merged.separator.replace(/[\r\n]+/g, " ");
112
+ out.dataLabels = Object.keys(next).length ? next : true;
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+
118
+ /**
119
+ * Prepare one validated patch for a change to the chart's options. `change` is
120
+ * `{ axisTitles?: { category?, value? }, legend?, dataLabels? }`: `axisTitles` entries are strings (empty removes a title),
121
+ * `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.
123
+ * Fields the chart type cannot show are never written. The document is not modified.
124
+ */
125
+ export function prepareChartOptions(document, chartPath, change) {
126
+ if (!chartOptionsAvailable()) throw fail("chart-options-unavailable", "The installed @openpresentation/opf does not know the chart option fields.");
127
+ const before = validateOpfDocument(document);
128
+ const { parts, chart } = chartAt(document, chartPath);
129
+ const support = readChartOptions(chart).fields;
130
+ const wanted = desired(chart, change, support);
131
+ const patches = [];
132
+ for (const [key, value] of Object.entries(wanted)) {
133
+ const path = opfPathToJsonPointer([...parts, key]);
134
+ const present = Object.hasOwn(chart, key);
135
+ if (value === undefined) {
136
+ if (present) patches.push({ op: "remove", path });
137
+ } else if (!present) patches.push({ op: "add", path, value: structuredClone(value) });
138
+ else if (!same(chart[key], value)) patches.push({ op: "replace", path, value: structuredClone(value) });
139
+ }
140
+ const next = checkedDocument(document, patches, before);
141
+ return { action: "chart-options", chartPath, changed: patches.length > 0, patches, document: next };
142
+ }
143
+
144
+ function checkEditor(editor) {
145
+ if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function") throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
146
+ }
147
+
148
+ /** Apply a chart option change to an editor session as one undoable edit. Returns the editor change. */
149
+ export function setChartOptions(editor, chartPath, change, meta = {}) {
150
+ checkEditor(editor);
151
+ const prepared = prepareChartOptions(editor.document, chartPath, change);
152
+ const { document, patches, ...summary } = prepared;
153
+ void document;
154
+ if (!prepared.changed) return { ...summary, document: editor.document, patches: [], inversePatches: [], validation: editor.validation };
155
+ const applied = editor.applyPatch(patches, { ...meta, source: meta.source ?? "chart-option", action: prepared.action, path: chartPath });
156
+ return { ...applied, ...summary };
157
+ }
@@ -0,0 +1,91 @@
1
+ import type { ImageTarget, PromoteImageOptions } from "@openpresentation/opf/convert";
2
+ import type { PresentationPaginationOptions, PaginatedPage } from "@openpresentation/opf/pagination";
3
+ import type { EditorChange, EditorSession, JsonPatchOperation } from "./index.js";
4
+
5
+ export type { ImageTarget, PromoteImageOptions } from "@openpresentation/opf/convert";
6
+ export type SlidePages = readonly (PaginatedPage & { sourceSlideIndex: number })[];
7
+
8
+ /** What every prepare function returns without touching a session. A refusal throws `content-action-refused`. */
9
+ export interface PreparedContentAction {
10
+ document: unknown;
11
+ /** A `test` of what was read, then `replace`, or per-slide `remove` and `add`. Empty when nothing changed. */
12
+ patches: JsonPatchOperation[];
13
+ /** The path of the block or slide the change touched: select it again after applying. */
14
+ path: string;
15
+ changed: boolean;
16
+ /** True when nothing is lost; otherwise `loss` names what the result cannot carry. */
17
+ lossless: boolean;
18
+ loss: string[];
19
+ /** Why nothing changed (`changed` is false), when core can say. */
20
+ reason?: string;
21
+ /** List shifts: the nesting level of every item after the change. */
22
+ levels?: number[];
23
+ /** Slide edits: the old slides replaced (`deleteCount` from `start`) and how many slides replace them. */
24
+ range?: { start: number; deleteCount: number };
25
+ slideCount?: number;
26
+ /** Split on overflow: the pagination mapping of the new slides, for `unpaginateSlides`. */
27
+ pages?: (PaginatedPage & { sourceSlideIndex: number })[];
28
+ }
29
+ /** The session change of an applied action, with the same report fields. One undo step. */
30
+ export interface ContentActionChange extends Omit<EditorChange, "document" | "patches"> {
31
+ document: unknown;
32
+ patches: JsonPatchOperation[];
33
+ changed: boolean;
34
+ lossless: boolean;
35
+ loss: string[];
36
+ path: string;
37
+ levels?: number[];
38
+ reason?: string;
39
+ range?: { start: number; deleteCount: number };
40
+ slideCount?: number;
41
+ pages?: (PaginatedPage & { sourceSlideIndex: number })[];
42
+ }
43
+
44
+ /** Every prepare function takes `validate: false` in its options for a dry run that skips the whole-document validation (the converted slide is still validated). Applying always validates. */
45
+ export interface DryRunOptions {
46
+ validate?: boolean;
47
+ }
48
+
49
+ export interface ListShiftOptions extends DryRunOptions {
50
+ /** Move the items nested under each selected item with it (default true). */
51
+ withChildren?: boolean;
52
+ }
53
+
54
+ /** The index of the list item a selection points at (`slides.0.blocks.1.items.2.text` gives 2), or undefined. */
55
+ export declare function listItemIndexForSelection(selectedPath: string, blockPath: string): number | undefined;
56
+ export declare function prepareListShift(document: unknown, blockPath: string, indices: number[], delta: number, options?: ListShiftOptions): PreparedContentAction;
57
+ /** Nest (`delta` 1) or un-nest (`delta` -1) list items as one undoable step. */
58
+ export declare function shiftListItems(editor: EditorSession, blockPath: string, indices: number[], delta: number, options?: ListShiftOptions, meta?: Record<string, unknown>): ContentActionChange;
59
+
60
+ export declare function prepareGroupBlocks(document: unknown, containerPath: string, indices: number[], options?: DryRunOptions & { composition?: Record<string, unknown> }): PreparedContentAction;
61
+ export declare function prepareUngroupBlock(document: unknown, groupPath: string, options?: DryRunOptions): PreparedContentAction;
62
+ export declare function prepareBlocksToRegions(document: unknown, slideIndex: number, regions: string[], options?: DryRunOptions): PreparedContentAction;
63
+ export declare function prepareRegionsToBlocks(document: unknown, slideIndex: number, options?: DryRunOptions): PreparedContentAction;
64
+ export declare function prepareMoveRegion(document: unknown, slideIndex: number, from: string, to: string, options?: DryRunOptions & { swap?: boolean }): PreparedContentAction;
65
+ export declare function prepareImageToDesign(document: unknown, blockPath: string, target: ImageTarget, options?: DryRunOptions & PromoteImageOptions): PreparedContentAction;
66
+ export declare function prepareImageToContent(document: unknown, slideIndex: number, source: ImageTarget, options?: DryRunOptions & { index?: number; region?: string }): PreparedContentAction;
67
+
68
+ /** Wrap the blocks at `indices` of a slide, group or region group in a new group. */
69
+ export declare function groupBlocks(editor: EditorSession, containerPath: string, indices: number[], options?: { composition?: Record<string, unknown> }, meta?: Record<string, unknown>): ContentActionChange;
70
+ /** Replace a group by its blocks; its composition and id are reported in `loss`. */
71
+ export declare function ungroupBlock(editor: EditorSession, groupPath: string, meta?: Record<string, unknown>): ContentActionChange;
72
+ export declare function placeBlocksInRegions(editor: EditorSession, slideIndex: number, regions: string[], meta?: Record<string, unknown>): ContentActionChange;
73
+ export declare function regionsAsBlocks(editor: EditorSession, slideIndex: number, meta?: Record<string, unknown>): ContentActionChange;
74
+ export declare function moveSlideRegion(editor: EditorSession, slideIndex: number, from: string, to: string, options?: { swap?: boolean }, meta?: Record<string, unknown>): ContentActionChange;
75
+ /** Move an image block into the slide's design as its slide image, background or watermark. */
76
+ export declare function moveImageToDesign(editor: EditorSession, blockPath: string, target: ImageTarget, options?: PromoteImageOptions, meta?: Record<string, unknown>): ContentActionChange;
77
+ /** Move a slide's own slide image, background image or watermark back into its content as an image block. */
78
+ export declare function moveImageToContent(editor: EditorSession, slideIndex: number, source: ImageTarget, options?: { index?: number; region?: string }, meta?: Record<string, unknown>): ContentActionChange;
79
+
80
+ export declare function prepareSplitSlide(document: unknown, slideIndex: number, options?: { at?: number[]; each?: boolean; repeatHeadings?: boolean }): PreparedContentAction;
81
+ export declare function prepareSplitSlideOnOverflow(document: unknown, slideIndex: number, options?: PresentationPaginationOptions): PreparedContentAction;
82
+ export declare function prepareMergeSlides(document: unknown, start: number, count?: number): PreparedContentAction;
83
+ export declare function prepareUnpaginate(document: unknown, pages: SlidePages, options?: { sourceSlideIndex?: number }): PreparedContentAction;
84
+ /** Split a slide by its blocks into several slides, as one undoable step. */
85
+ export declare function splitSlideByBlocks(editor: EditorSession, slideIndex: number, options?: { at?: number[]; each?: boolean; repeatHeadings?: boolean }, meta?: Record<string, unknown>): ContentActionChange;
86
+ /** Split a slide that overflows with the existing pagination; the change carries `pages` for `unpaginateSlides`. */
87
+ export declare function splitSlideOnOverflow(editor: EditorSession, slideIndex: number, options?: PresentationPaginationOptions, meta?: Record<string, unknown>): ContentActionChange;
88
+ /** Merge consecutive slides into one. The first slide's id, headings and design win; what is dropped is reported in `loss`. */
89
+ export declare function mergeSlides(editor: EditorSession, start: number, count?: number, meta?: Record<string, unknown>): ContentActionChange;
90
+ /** Put paginated continuation slides back together, given the `pages` mapping of the pagination. */
91
+ export declare function unpaginateSlides(editor: EditorSession, pages: SlidePages, options?: { sourceSlideIndex?: number }, meta?: Record<string, unknown>): ContentActionChange;