@openpresentation/opf-editor 0.11.3 → 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 +82 -58
- 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 +23 -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 +12 -10
- package/dist/chart-data.js +22 -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 +179 -10
- package/dist/data.d.ts +23 -2
- package/dist/data.js +40 -1
- package/dist/dataset-refs.d.ts +17 -0
- package/dist/dataset-refs.js +45 -0
- package/dist/design-controls.js +66 -47
- 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 +35 -20
- package/dist/font-gate.js +49 -58
- package/dist/galleries.js +36 -52
- package/dist/grid-model.d.ts +99 -24
- package/dist/grid-model.js +577 -101
- 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 +136 -109
- package/dist/table-options.d.ts +8 -8
- package/dist/table-options.js +41 -30
- 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 +74 -54
- package/package.json +10 -6
- package/dist/font-defaults.js +0 -25
package/dist/switches.js
CHANGED
|
@@ -2,29 +2,26 @@
|
|
|
2
2
|
// dimension that turns "switch this dimension to X" into a validated JSON Patch, and applies
|
|
3
3
|
// it to an editor session as one undoable transaction. The preview recomposes and the PPTX
|
|
4
4
|
// export follows from the same document, so no dimension needs a special refresh path.
|
|
5
|
-
import { catalogSchemaNames, catalogs as bundledCatalogs, schemas } from "@openpresentation/opf";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
createValuePatch,
|
|
9
|
-
applyJsonPatch,
|
|
10
|
-
getValueAtPath,
|
|
11
|
-
opfPathToJsonPointer,
|
|
12
|
-
splitOpfPath,
|
|
13
|
-
validateOpfDocument,
|
|
14
|
-
} from "./index.js";
|
|
5
|
+
import { catalogSchemaNames, catalogs as bundledCatalogs, isXYChartType, resolveChartData, schemas } from "@openpresentation/opf";
|
|
6
|
+
import { chartOptionTarget } from "@openpresentation/opf/composition";
|
|
7
|
+
import { OPFEditorError, createValuePatch, applyJsonPatch, getValueAtPath, opfPathToJsonPointer, splitOpfPath } from "./index.js";
|
|
15
8
|
import { createContentBlock, prepareBlockReplace } from "./blocks.js";
|
|
16
9
|
import { checkedDocument, designPatches, fail, same } from "./edit-helpers.js";
|
|
17
10
|
import { populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
18
11
|
import { blockConversionTargets, prepareBlockConversion } from "./block-convert.js";
|
|
12
|
+
import { checkFormat } from "./checks.js";
|
|
19
13
|
|
|
20
|
-
/** The schema's DimensionPreset values (RR-41), the values of the slide-sizes switch. */
|
|
21
|
-
export const SLIDE_SIZE_PRESETS = Object.freeze(["16:9", "4:3", "16:10", "letter", "a4", "widescreen", "standard"]);
|
|
14
|
+
/** The schema's DimensionPreset values (RR-41, FA-13: with the social-feed ratios), the values of the slide-sizes switch. */
|
|
15
|
+
export const SLIDE_SIZE_PRESETS = Object.freeze(["16:9", "4:3", "16:10", "1:1", "4:5", "9:16", "letter", "a4", "widescreen", "standard"]);
|
|
22
16
|
|
|
23
17
|
// Slide size in inches per preset, as core's resolveCanvasDimensions composes it (for picker labels).
|
|
24
18
|
const SLIDE_SIZE_LABELS = Object.freeze({
|
|
25
19
|
"16:9": "16:9 (13.33 x 7.5 in)",
|
|
26
20
|
"4:3": "4:3 (10 x 7.5 in)",
|
|
27
21
|
"16:10": "16:10 (10 x 6.25 in)",
|
|
22
|
+
"1:1": "1:1 square (7.5 x 7.5 in)",
|
|
23
|
+
"4:5": "4:5 portrait (7.5 x 9.375 in)",
|
|
24
|
+
"9:16": "9:16 portrait (7.5 x 13.33 in)",
|
|
28
25
|
letter: "Letter (11 x 8.5 in)",
|
|
29
26
|
a4: "A4 (11.69 x 8.27 in)",
|
|
30
27
|
widescreen: "Widescreen (13.33 x 7.5 in)",
|
|
@@ -89,55 +86,55 @@ function recordList(source) {
|
|
|
89
86
|
|
|
90
87
|
// Inline document records first (they override), then caller-loaded records, then the
|
|
91
88
|
// bundled catalog. Unlike getCatalogRecords, an inline catalog never hides bundled ids.
|
|
92
|
-
function findCatalogRecord(
|
|
89
|
+
function findCatalogRecord(presentation, kind, id, options) {
|
|
93
90
|
return (
|
|
94
|
-
recordList(
|
|
91
|
+
recordList(presentation.catalogs?.[kind]).find((record) => record.id === id) ??
|
|
95
92
|
recordList(options.catalogs?.[kind] ?? options.catalogSources?.[kind]).find((record) => record.id === id) ??
|
|
96
93
|
(options.record?.id === id ? options.record : undefined) ??
|
|
97
94
|
bundledCatalogs[kind]?.find((record) => record.id === id)
|
|
98
95
|
);
|
|
99
96
|
}
|
|
100
97
|
|
|
101
|
-
function requireCatalogId(
|
|
98
|
+
function requireCatalogId(presentation, dimension, id, options) {
|
|
102
99
|
const kind = CATALOG_KIND[dimension];
|
|
103
100
|
if (typeof id !== "string" || id.length === 0)
|
|
104
101
|
throw fail("invalid-catalog-id", `Switch ${dimension} to a non-empty catalog id.`, { dimension, id });
|
|
105
|
-
const record = findCatalogRecord(
|
|
102
|
+
const record = findCatalogRecord(presentation, kind, id, options);
|
|
106
103
|
if (!record) throw fail("unknown-catalog-id", `Unknown ${kind} catalog ID: ${id}.`, { dimension, catalogKind: kind, id });
|
|
107
104
|
return record;
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
// A gallery item supplies its own record; add it inline in the same transaction when the
|
|
111
108
|
// document and the bundled catalog do not already define the id.
|
|
112
|
-
function catalogRecordPatches(
|
|
109
|
+
function catalogRecordPatches(presentation, dimension, options) {
|
|
113
110
|
const record = options.record;
|
|
114
111
|
if (!record) return [];
|
|
115
112
|
const kind = CATALOG_KIND[dimension];
|
|
116
|
-
if (recordList(
|
|
113
|
+
if (recordList(presentation.catalogs?.[kind]).some((entry) => entry.id === record.id) || bundledCatalogs[kind]?.some((entry) => entry.id === record.id)) return [];
|
|
117
114
|
const value = { $schema: schemas[catalogSchemaNames[kind]].$id, ...record };
|
|
118
|
-
if (!
|
|
119
|
-
if (!
|
|
115
|
+
if (!presentation.catalogs || typeof presentation.catalogs !== "object") return [{ op: "add", path: "/catalogs", value: { [kind]: { records: [value] } } }];
|
|
116
|
+
if (!presentation.catalogs[kind] || typeof presentation.catalogs[kind] !== "object")
|
|
120
117
|
return [{ op: "add", path: `/catalogs/${kind}`, value: { records: [value] } }];
|
|
121
|
-
if (!Array.isArray(
|
|
118
|
+
if (!Array.isArray(presentation.catalogs[kind].records)) return [{ op: "add", path: `/catalogs/${kind}/records`, value: [value] }];
|
|
122
119
|
return [{ op: "add", path: `/catalogs/${kind}/records/-`, value }];
|
|
123
120
|
}
|
|
124
121
|
|
|
125
|
-
function slideAt(
|
|
126
|
-
if (!Number.isInteger(slideIndex) || !
|
|
122
|
+
function slideAt(presentation, slideIndex, what = "this switch") {
|
|
123
|
+
if (!Number.isInteger(slideIndex) || !presentation.slides?.[slideIndex])
|
|
127
124
|
throw fail("slide-index-out-of-range", `Choose an existing slide for ${what}.`, { slideIndex });
|
|
128
|
-
return
|
|
125
|
+
return presentation.slides[slideIndex];
|
|
129
126
|
}
|
|
130
127
|
|
|
131
|
-
function rootPatch(
|
|
132
|
-
return same(
|
|
128
|
+
function rootPatch(presentation, field, value) {
|
|
129
|
+
return same(presentation[field], value) ? [] : createValuePatch(presentation, [field], value);
|
|
133
130
|
}
|
|
134
131
|
|
|
135
132
|
// A deck-level switch does nothing for a slide that carries its own value for that key.
|
|
136
|
-
function shadowedSlides(
|
|
137
|
-
return (
|
|
133
|
+
function shadowedSlides(presentation, keys) {
|
|
134
|
+
return (presentation.slides ?? []).flatMap((slide, index) => (keys.some((key) => slide?.design?.[key] !== undefined) ? [index] : []));
|
|
138
135
|
}
|
|
139
136
|
|
|
140
|
-
function findChartOwner(
|
|
137
|
+
function findChartOwner(presentation, slideIndex) {
|
|
141
138
|
const visit = (node, path, depth) => {
|
|
142
139
|
if (!node || typeof node !== "object" || Array.isArray(node) || depth > 32) return undefined;
|
|
143
140
|
if (node.chart && typeof node.chart === "object") return path;
|
|
@@ -153,23 +150,23 @@ function findChartOwner(document, slideIndex) {
|
|
|
153
150
|
}
|
|
154
151
|
return undefined;
|
|
155
152
|
};
|
|
156
|
-
return visit(
|
|
153
|
+
return visit(presentation.slides[slideIndex], ["slides", String(slideIndex)], 0);
|
|
157
154
|
}
|
|
158
155
|
|
|
159
|
-
function socialsPatches(
|
|
156
|
+
function socialsPatches(presentation, value, options) {
|
|
160
157
|
const owner = options.owner ?? "speaker";
|
|
161
158
|
if (!["speaker", "organization"].includes(owner)) throw fail("invalid-switch-value", "Socials owner must be 'speaker' or 'organization'.", { owner });
|
|
162
159
|
if (!value || typeof value !== "object" || typeof value.platform !== "string" || typeof value.handle !== "string" || !value.handle)
|
|
163
160
|
throw fail("invalid-switch-value", "Switch socials to { platform, handle } with a non-empty handle.", { value });
|
|
164
|
-
requireCatalogId(
|
|
165
|
-
const host =
|
|
161
|
+
requireCatalogId(presentation, "socials", value.platform, options);
|
|
162
|
+
const host = presentation[owner];
|
|
166
163
|
const index = Array.isArray(host) ? (options.index ?? 0) : undefined;
|
|
167
164
|
const target = Array.isArray(host) ? host[index] : host;
|
|
168
165
|
if (!target || typeof target !== "object")
|
|
169
166
|
throw fail("missing-owner", `Add the ${owner} to the document before setting their socials.`, { owner });
|
|
170
167
|
const base = Array.isArray(host) ? [owner, String(index)] : [owner];
|
|
171
168
|
const socials = target.socials;
|
|
172
|
-
const patches = catalogRecordPatches(
|
|
169
|
+
const patches = catalogRecordPatches(presentation, "socials", options);
|
|
173
170
|
if (!socials || typeof socials !== "object") return [...patches, { op: "add", path: opfPathToJsonPointer([...base, "socials"]), value: { [value.platform]: value.handle } }];
|
|
174
171
|
if (socials[value.platform] === value.handle) return patches;
|
|
175
172
|
return [...patches, { op: Object.hasOwn(socials, value.platform) ? "replace" : "add", path: opfPathToJsonPointer([...base, "socials", value.platform]), value: value.handle }];
|
|
@@ -188,16 +185,16 @@ function blockValue(value, options) {
|
|
|
188
185
|
* Compute the patch that switches one dimension, without touching any session. The candidate
|
|
189
186
|
* document is validated; an invalid result throws unless the input was already invalid.
|
|
190
187
|
*/
|
|
191
|
-
export function prepareDimensionSwitch(
|
|
188
|
+
export function prepareDimensionSwitch(presentation, dimension, value, options = {}) {
|
|
192
189
|
if (!SWITCH_DIMENSIONS.includes(dimension))
|
|
193
190
|
throw fail("unknown-dimension", `Unknown dimension: ${dimension}. Use one of ${SWITCH_DIMENSIONS.join(", ")}.`, { dimension });
|
|
194
|
-
if (!
|
|
191
|
+
if (!presentation || typeof presentation !== "object") throw fail("invalid-input", "Switch requires an OPF document object.");
|
|
195
192
|
if (options.record) {
|
|
196
193
|
const ids = dimension === "audiences" ? [value].flat() : dimension === "socials" ? [value?.platform] : [value];
|
|
197
194
|
if (!CATALOG_KIND[dimension] || !ids.includes(options.record.id))
|
|
198
195
|
throw fail("record-id-mismatch", `The supplied record id must equal a switched ${dimension} id.`, { dimension, id: options.record.id });
|
|
199
196
|
}
|
|
200
|
-
const before =
|
|
197
|
+
const before = checkFormat(presentation);
|
|
201
198
|
const scopeIndex = options.slideIndex;
|
|
202
199
|
let patches = [];
|
|
203
200
|
let scope = "deck";
|
|
@@ -209,28 +206,24 @@ export function prepareDimensionSwitch(document, dimension, value, options = {})
|
|
|
209
206
|
// A preset string; a custom size (design.dimensions as an object with inches) is replaced by it.
|
|
210
207
|
if (!SLIDE_SIZE_PRESETS.includes(value)) throw fail("invalid-switch-value", `Switch slide-sizes to one of ${SLIDE_SIZE_PRESETS.join(", ")}.`, { value });
|
|
211
208
|
if (scopeIndex !== undefined) throw fail("deck-scope-only", "A presentation has one slide size: switch slide-sizes for the deck, without slideIndex.", { slideIndex: scopeIndex });
|
|
212
|
-
const own =
|
|
209
|
+
const own = presentation.design?.dimensions;
|
|
213
210
|
// {preset} alone is the same size as the bare preset string.
|
|
214
211
|
const sameSize = own && typeof own === "object" && !Array.isArray(own) && own.preset === value && Object.keys(own).length === 1;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (options.clearSlideOverrides) {
|
|
218
|
-
for (const index of shadowed) patches.push({ op: "remove", path: opfPathToJsonPointer(["slides", String(index), "design", "dimensions"]) });
|
|
219
|
-
shadowed = [];
|
|
220
|
-
}
|
|
212
|
+
// A slide's design cannot set dimensions (FA-07), so no slide can shadow the deck's size.
|
|
213
|
+
patches = sameSize ? [] : designPatches(presentation, [], { dimensions: value });
|
|
221
214
|
} else if (dimension === "purposes") {
|
|
222
215
|
// A catalog id, free-form goal text (no id check: any goal is valid) or an inline Purpose object; the schema validates it.
|
|
223
216
|
if (typeof value !== "string" && !(value && typeof value === "object" && !Array.isArray(value)))
|
|
224
217
|
throw fail("invalid-switch-value", "Switch purposes to a catalog id, a goal string or a purpose object.", { value });
|
|
225
218
|
if (typeof value === "string" && value.length === 0) throw fail("invalid-switch-value", "Switch purposes to a non-empty goal.", { value });
|
|
226
|
-
patches = [...catalogRecordPatches(
|
|
219
|
+
patches = [...catalogRecordPatches(presentation, dimension, options), ...rootPatch(presentation, "purpose", value)];
|
|
227
220
|
} else if (dimension === "layouts") {
|
|
228
221
|
slideIndex = scopeIndex;
|
|
229
|
-
slideAt(
|
|
222
|
+
slideAt(presentation, slideIndex, "a layout switch");
|
|
230
223
|
scope = "slide";
|
|
231
|
-
const record = requireCatalogId(
|
|
232
|
-
patches = catalogRecordPatches(
|
|
233
|
-
const withRecord = patches.length ? applyJsonPatch(
|
|
224
|
+
const record = requireCatalogId(presentation, dimension, value, options);
|
|
225
|
+
patches = catalogRecordPatches(presentation, dimension, options);
|
|
226
|
+
const withRecord = patches.length ? applyJsonPatch(presentation, patches) : presentation;
|
|
234
227
|
if (withRecord.slides[slideIndex].layout !== value) {
|
|
235
228
|
const layoutOps = createValuePatch(withRecord, ["slides", String(slideIndex), "layout"], value);
|
|
236
229
|
const swapped = applyJsonPatch(withRecord, layoutOps);
|
|
@@ -243,64 +236,65 @@ export function prepareDimensionSwitch(document, dimension, value, options = {})
|
|
|
243
236
|
}
|
|
244
237
|
} else if (dimension === "charts") {
|
|
245
238
|
slideIndex = scopeIndex;
|
|
246
|
-
slideAt(
|
|
239
|
+
slideAt(presentation, slideIndex, "a chart switch");
|
|
247
240
|
scope = "slide";
|
|
248
|
-
requireCatalogId(
|
|
249
|
-
const owner = options.path ? splitOpfPath(options.path) : findChartOwner(
|
|
241
|
+
requireCatalogId(presentation, dimension, value, options);
|
|
242
|
+
const owner = options.path ? splitOpfPath(options.path) : findChartOwner(presentation, slideIndex);
|
|
250
243
|
if (options.path && (owner[0] !== "slides" || owner[1] !== String(slideIndex)))
|
|
251
244
|
throw fail("path-slide-mismatch", "options.path is not on the slide named by options.slideIndex.", { slideIndex, path: options.path });
|
|
252
|
-
const chart = owner && getValueAtPath(
|
|
245
|
+
const chart = owner && getValueAtPath(presentation, [...owner, "chart"]);
|
|
253
246
|
if (!chart || typeof chart !== "object") throw fail("chart-not-found", "This slide has no chart to switch. Insert a chart block first.", { slideIndex, path: options.path });
|
|
254
|
-
patches = [...catalogRecordPatches(
|
|
247
|
+
patches = [...catalogRecordPatches(presentation, dimension, options), ...(chart.type === value ? [] : [...createValuePatch(presentation, [...owner, "chart", "type"], value), ...staleMappingPatches(presentation, owner, chart, value), ...staleComboPatches(presentation, owner, chart, value)])];
|
|
255
248
|
} else if (dimension === "blocks") {
|
|
256
249
|
if (options.path === undefined) throw fail("missing-path", "Choose the block to replace with options.path.");
|
|
257
250
|
// convert: true moves the block's own text into the new kind (block-convert.js) instead of replacing it.
|
|
258
251
|
if (options.convert) {
|
|
259
252
|
if (typeof value !== "string") throw fail("invalid-switch-value", "Convert a block to a content kind name.", { value });
|
|
260
|
-
const conversion = prepareBlockConversion(
|
|
253
|
+
const conversion = prepareBlockConversion(presentation, options.path, value, options.conversion);
|
|
261
254
|
patches = conversion.changed ? conversion.patches : [];
|
|
262
255
|
conversionLoss = conversion.loss;
|
|
263
256
|
} else {
|
|
264
257
|
const block = blockValue(value, options);
|
|
265
|
-
const change = prepareBlockReplace(
|
|
258
|
+
const change = prepareBlockReplace(presentation, options.path, block);
|
|
266
259
|
patches = change.changed ? change.patches : [];
|
|
267
260
|
}
|
|
268
261
|
const parts = splitOpfPath(options.path);
|
|
269
262
|
if (parts[0] === "slides") slideIndex = Number(parts[1]);
|
|
270
263
|
scope = "block";
|
|
271
264
|
} else if (dimension === "socials") {
|
|
272
|
-
patches = socialsPatches(
|
|
265
|
+
patches = socialsPatches(presentation, value, options);
|
|
273
266
|
} else if (ROOT_FIELD[dimension]) {
|
|
274
267
|
const field = ROOT_FIELD[dimension];
|
|
275
268
|
let next = value;
|
|
276
269
|
if (dimension === "audiences") {
|
|
277
270
|
next = Array.isArray(value) ? value : [value];
|
|
278
271
|
if (!next.length) throw fail("invalid-catalog-id", "Switch audiences to at least one catalog id.", { value });
|
|
279
|
-
for (const id of next) requireCatalogId(
|
|
280
|
-
} else requireCatalogId(
|
|
281
|
-
patches = [...catalogRecordPatches(
|
|
272
|
+
for (const id of next) requireCatalogId(presentation, dimension, id, options);
|
|
273
|
+
} else requireCatalogId(presentation, dimension, value, options);
|
|
274
|
+
patches = [...catalogRecordPatches(presentation, dimension, options), ...rootPatch(presentation, field, next)];
|
|
282
275
|
} else {
|
|
283
276
|
// Design dimensions: deck scope by default, or one slide with options.slideIndex.
|
|
284
277
|
let entries;
|
|
285
278
|
if (dimension === "themes") {
|
|
286
|
-
const record = requireCatalogId(
|
|
279
|
+
const record = requireCatalogId(presentation, dimension, value, options);
|
|
287
280
|
entries = { theme: value };
|
|
288
281
|
if (options.bundle !== false) {
|
|
289
282
|
// The gallery's theme snippet writes the whole bundle, so an explicit deck choice
|
|
290
283
|
// does not keep the previous theme's fonts, colors or background.
|
|
291
|
-
|
|
284
|
+
// A slide's design cannot set dimensions (a PPTX has one slide size), so only a deck-scope switch writes the theme's.
|
|
285
|
+
for (const key of ["colorScheme", "fontScheme", "background", ...(scopeIndex === undefined ? ["dimensions"] : [])]) if (record[key] !== undefined) entries[key] = record[key];
|
|
292
286
|
}
|
|
293
|
-
patches = catalogRecordPatches(
|
|
287
|
+
patches = catalogRecordPatches(presentation, dimension, options);
|
|
294
288
|
} else if (dimension === "color-schemes" || dimension === "font-schemes") {
|
|
295
|
-
requireCatalogId(
|
|
289
|
+
requireCatalogId(presentation, dimension, value, options);
|
|
296
290
|
entries = { [DESIGN_KEYS[dimension][0]]: value };
|
|
297
291
|
if (dimension === "font-schemes") {
|
|
298
292
|
// An accent font is its own choice, not part of the scheme being left: it stays (in the object form).
|
|
299
|
-
const scopeBase = scopeIndex !== undefined &&
|
|
300
|
-
const own = getValueAtPath(
|
|
293
|
+
const scopeBase = scopeIndex !== undefined && presentation.slides?.[scopeIndex] ? ["slides", String(scopeIndex)] : [];
|
|
294
|
+
const own = getValueAtPath(presentation, [...scopeBase, "design", "fontScheme"]);
|
|
301
295
|
if (own && typeof own === "object" && own.accent !== undefined) entries.fontScheme = { id: value, accent: structuredClone(own.accent) };
|
|
302
296
|
}
|
|
303
|
-
patches = catalogRecordPatches(
|
|
297
|
+
patches = catalogRecordPatches(presentation, dimension, options);
|
|
304
298
|
} else {
|
|
305
299
|
// A background is an object or a shorthand string (theme slot or hex color); the schema
|
|
306
300
|
// validates the candidate document, so a string that is neither is rejected below.
|
|
@@ -317,29 +311,29 @@ export function prepareDimensionSwitch(document, dimension, value, options = {})
|
|
|
317
311
|
}
|
|
318
312
|
let base = [];
|
|
319
313
|
if (scopeIndex !== undefined) {
|
|
320
|
-
slideAt(
|
|
314
|
+
slideAt(presentation, scopeIndex, `a ${dimension} switch`);
|
|
321
315
|
base = ["slides", String(scopeIndex)];
|
|
322
316
|
scope = "slide";
|
|
323
317
|
slideIndex = scopeIndex;
|
|
324
318
|
}
|
|
325
|
-
patches = [...patches, ...designPatches(
|
|
319
|
+
patches = [...patches, ...designPatches(presentation, base, entries)];
|
|
326
320
|
if (scope === "deck") {
|
|
327
321
|
const keys = Object.entries(entries).filter(([, entry]) => entry !== undefined).map(([key]) => key);
|
|
328
|
-
shadowed = shadowedSlides(
|
|
322
|
+
shadowed = shadowedSlides(presentation, keys);
|
|
329
323
|
if (options.clearSlideOverrides)
|
|
330
324
|
for (const index of shadowed)
|
|
331
325
|
for (const key of keys)
|
|
332
|
-
if (
|
|
326
|
+
if (presentation.slides[index].design?.[key] !== undefined) patches.push({ op: "remove", path: opfPathToJsonPointer(["slides", String(index), "design", key]) });
|
|
333
327
|
if (options.clearSlideOverrides) shadowed = [];
|
|
334
328
|
}
|
|
335
329
|
}
|
|
336
330
|
|
|
337
|
-
const next = checkedDocument(
|
|
331
|
+
const next = checkedDocument(presentation, patches, before);
|
|
338
332
|
return {
|
|
339
333
|
dimension,
|
|
340
334
|
scope,
|
|
341
335
|
...(slideIndex !== undefined ? { slideIndex } : {}),
|
|
342
|
-
|
|
336
|
+
presentation: structuredClone(next),
|
|
343
337
|
patches,
|
|
344
338
|
changed: patches.length > 0,
|
|
345
339
|
shadowed,
|
|
@@ -356,17 +350,17 @@ export function switchDimension(editor, dimension, value, options = {}) {
|
|
|
356
350
|
if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function")
|
|
357
351
|
throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
|
|
358
352
|
const { meta, ...switchOptions } = options;
|
|
359
|
-
const prepared = prepareDimensionSwitch(editor.
|
|
360
|
-
const {
|
|
361
|
-
void
|
|
362
|
-
if (!prepared.changed) return { ...summary,
|
|
353
|
+
const prepared = prepareDimensionSwitch(editor.presentation, dimension, value, switchOptions);
|
|
354
|
+
const { presentation, patches, ...summary } = prepared;
|
|
355
|
+
void presentation;
|
|
356
|
+
if (!prepared.changed) return { ...summary, presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
|
|
363
357
|
const change = editor.applyPatch(patches, { ...meta, source: meta?.source ?? "dimension-switch", dimension, scope: prepared.scope });
|
|
364
358
|
return { ...change, ...summary };
|
|
365
359
|
}
|
|
366
360
|
|
|
367
361
|
// --- options for pickers and current values ---------------------------------------------------
|
|
368
362
|
|
|
369
|
-
const labelOf = (record) => record.
|
|
363
|
+
const labelOf = (record) => record.name ?? record.id;
|
|
370
364
|
|
|
371
365
|
/**
|
|
372
366
|
* The values a picker can offer for a catalog-backed dimension, in document order: the document's
|
|
@@ -374,14 +368,14 @@ const labelOf = (record) => record.label ?? record.name ?? record.id;
|
|
|
374
368
|
* without duplicates. `blocks` lists the content kinds. `charts` lists every chart type; use
|
|
375
369
|
* `compatibleChartTypes` to narrow it to the types the chart's data can use.
|
|
376
370
|
*/
|
|
377
|
-
export function listSwitchOptions(
|
|
371
|
+
export function listSwitchOptions(presentation, dimension, options = {}) {
|
|
378
372
|
if (dimension === "blocks") return BLOCK_KINDS.map((kind) => ({ id: kind, label: kind[0].toUpperCase() + kind.slice(1) }));
|
|
379
373
|
if (dimension === "slide-sizes") return SLIDE_SIZE_PRESETS.map((preset) => ({ id: preset, label: SLIDE_SIZE_LABELS[preset] }));
|
|
380
374
|
const kind = CATALOG_KIND[dimension];
|
|
381
375
|
if (!kind) return [];
|
|
382
376
|
const seen = new Set();
|
|
383
377
|
const out = [];
|
|
384
|
-
for (const source of [
|
|
378
|
+
for (const source of [presentation?.catalogs?.[kind], options.catalogs?.[kind] ?? options.catalogSources?.[kind], bundledCatalogs[kind]])
|
|
385
379
|
for (const record of recordList(source)) {
|
|
386
380
|
if (seen.has(record.id)) continue;
|
|
387
381
|
seen.add(record.id);
|
|
@@ -392,38 +386,74 @@ export function listSwitchOptions(document, dimension, options = {}) {
|
|
|
392
386
|
|
|
393
387
|
const SINGLE_SERIES_ONLY = new Set(["pieChart", "doughnutChart", "funnelChart", "treemapChart", "waterfallChart"]);
|
|
394
388
|
const MULTI_SERIES_CAPABLE = new Set(["barChart", "lineChart", "areaChart", "radarChart"]);
|
|
389
|
+
const STACKED_GROUPINGS = new Set(["stacked", "percentStacked"]);
|
|
395
390
|
const DISTRIBUTION_ELEMENTS = new Set(["histogramChart", "boxWhiskerChart", "mapChart"]);
|
|
396
391
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
392
|
+
// RR-54: `mapping.x` names the X column of an XY chart; a type without an X axis ignores it (core warns chart-mapping-adapted). Switching to such a
|
|
393
|
+
// type removes it, and the whole mapping when nothing else is left, so the document does not keep a warning it cannot act on.
|
|
394
|
+
function staleMappingPatches(presentation, owner, chart, type) {
|
|
395
|
+
const mapping = chart.mapping;
|
|
396
|
+
if (!mapping || typeof mapping !== "object" || Array.isArray(mapping) || mapping.x === undefined) return [];
|
|
397
|
+
if (isXYChartType(type)) return [];
|
|
398
|
+
const { x: _x, ...rest } = mapping;
|
|
399
|
+
const parts = [...owner, "chart", "mapping"];
|
|
400
|
+
return Object.keys(rest).length ? createValuePatch(presentation, parts, rest) : [{ op: "remove", path: opfPathToJsonPointer(parts) }];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// FA-15: `line`, `secondaryAxis` and the secondary axis title belong to combo charts; any other type ignores them (core warns
|
|
404
|
+
// chart-option-adapted). Switching away from combo removes them, and an `axisTitles` left empty.
|
|
405
|
+
function staleComboPatches(presentation, owner, chart, type) {
|
|
406
|
+
if (chartOptionTarget(type)?.kind === "combo") return [];
|
|
407
|
+
const parts = [...owner, "chart"];
|
|
408
|
+
const patches = ["line", "secondaryAxis"].filter((key) => chart[key] !== undefined).map((key) => ({ op: "remove", path: opfPathToJsonPointer([...parts, key]) }));
|
|
409
|
+
const titles = chart.axisTitles;
|
|
410
|
+
if (titles && typeof titles === "object" && !Array.isArray(titles) && titles.secondary !== undefined) {
|
|
411
|
+
const { secondary: _secondary, ...rest } = titles;
|
|
412
|
+
patches.push(Object.keys(rest).length ? { op: "remove", path: opfPathToJsonPointer([...parts, "axisTitles", "secondary"]) } : { op: "remove", path: opfPathToJsonPointer([...parts, "axisTitles"]) });
|
|
413
|
+
}
|
|
414
|
+
return patches;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function chartDataShape(chart, presentation) {
|
|
418
|
+
// RR-54: inline data, a dataset reference and a series mapping all resolve to the columns the renderers read: the first column labels
|
|
419
|
+
// the categories and every further column is a series.
|
|
420
|
+
const resolved = resolveChartData(chart, presentation);
|
|
421
|
+
if (!resolved.ok) return undefined;
|
|
422
|
+
return { series: Math.max(0, resolved.columns.length - 1), categories: resolved.rows.length };
|
|
402
423
|
}
|
|
403
424
|
|
|
404
425
|
/**
|
|
405
426
|
* Chart types the chart's inline data can use as it is, from the chartTypes catalog: simple,
|
|
406
427
|
* non-geographic, non-distribution types whose series count fits the data (the first column labels
|
|
407
428
|
* the categories and each further column is a series; a type with N series needs exactly N value
|
|
408
|
-
* columns
|
|
429
|
+
* columns, except that a stacked or percent-stacked type and a combination such as combo take N or more;
|
|
430
|
+
* column, bar, line, area and radar take any number). Data that is read from
|
|
409
431
|
* an external source returns every simple type. This is data-shape compatibility, not a claim that
|
|
410
432
|
* an engine draws the type. `path` or `slideIndex` picks the chart (default: the slide's first).
|
|
411
433
|
* Each entry has `current: true` for the chart's present type, which is always listed.
|
|
412
434
|
*/
|
|
413
|
-
export function compatibleChartTypes(
|
|
435
|
+
export function compatibleChartTypes(presentation, options = {}) {
|
|
414
436
|
const slideIndex = options.slideIndex ?? 0;
|
|
415
|
-
const owner = options.path ? splitOpfPath(options.path) :
|
|
416
|
-
const chart = owner && getValueAtPath(
|
|
437
|
+
const owner = options.path ? splitOpfPath(options.path) : presentation.slides?.[slideIndex] ? findChartOwner(presentation, slideIndex) : undefined;
|
|
438
|
+
const chart = owner && getValueAtPath(presentation, [...owner, "chart"]);
|
|
417
439
|
if (!chart || typeof chart !== "object") return [];
|
|
418
|
-
const shape = chartDataShape(chart);
|
|
440
|
+
const shape = chartDataShape(chart, presentation);
|
|
419
441
|
const result = [];
|
|
420
|
-
for (const option of listSwitchOptions(
|
|
442
|
+
for (const option of listSwitchOptions(presentation, "charts", options)) {
|
|
421
443
|
const record = option.record;
|
|
422
444
|
const element = record.mappings?.openxml?.element;
|
|
423
445
|
const current = option.id === chart.type;
|
|
424
|
-
const simple = record.complexity === "simple" &&
|
|
446
|
+
const simple = record.complexity === "simple" && !DISTRIBUTION_ELEMENTS.has(element);
|
|
447
|
+
// A combination (composition "mixed", the FA-15 combo chart) needs at least its series count, like a stacked type.
|
|
448
|
+
const atLeast = STACKED_GROUPINGS.has(record.mappings?.openxml?.grouping) || record.mappings?.openxml?.composition === "mixed";
|
|
425
449
|
const seriesOk =
|
|
426
|
-
!shape ||
|
|
450
|
+
!shape ||
|
|
451
|
+
!record.series ||
|
|
452
|
+
(record.series > 1
|
|
453
|
+
? atLeast
|
|
454
|
+
? shape.series >= record.series
|
|
455
|
+
: record.series === shape.series
|
|
456
|
+
: shape.series === 1 || (MULTI_SERIES_CAPABLE.has(element) && !SINGLE_SERIES_ONLY.has(element)));
|
|
427
457
|
if (current || (simple && seriesOk)) result.push({ id: option.id, label: option.label, current, record });
|
|
428
458
|
}
|
|
429
459
|
return result;
|
|
@@ -435,43 +465,40 @@ export function compatibleChartTypes(document, options = {}) {
|
|
|
435
465
|
* dimension is unset. Catalog dimensions return the catalog id even when the document holds an
|
|
436
466
|
* inline object.
|
|
437
467
|
*/
|
|
438
|
-
export function currentSwitchValue(
|
|
468
|
+
export function currentSwitchValue(presentation, dimension, options = {}) {
|
|
439
469
|
const idOf = (reference) => (reference && typeof reference === "object" && !Array.isArray(reference) ? reference.id : reference);
|
|
440
470
|
const design = (key) => {
|
|
441
|
-
const slide = options.slideIndex !== undefined ?
|
|
471
|
+
const slide = options.slideIndex !== undefined ? presentation.slides?.[options.slideIndex]?.design?.[key] : undefined;
|
|
442
472
|
if (slide !== undefined) return { value: slide, scope: "slide" };
|
|
443
|
-
return
|
|
473
|
+
return presentation.design?.[key] === undefined ? { scope: "deck" } : { value: presentation.design[key], scope: "deck" };
|
|
444
474
|
};
|
|
445
|
-
if (dimension === "layouts") return { value:
|
|
475
|
+
if (dimension === "layouts") return { value: presentation.slides?.[options.slideIndex]?.layout, scope: "slide" };
|
|
446
476
|
if (dimension === "charts") {
|
|
447
|
-
const owner = options.path ? splitOpfPath(options.path) : findChartOwner(
|
|
448
|
-
return { value: owner ? getValueAtPath(
|
|
477
|
+
const owner = options.path ? splitOpfPath(options.path) : findChartOwner(presentation, options.slideIndex ?? 0);
|
|
478
|
+
return { value: owner ? getValueAtPath(presentation, [...owner, "chart", "type"]) : undefined, scope: "slide" };
|
|
449
479
|
}
|
|
450
480
|
if (dimension === "blocks") return { value: undefined, scope: "block" };
|
|
451
481
|
if (dimension === "slide-sizes") {
|
|
452
482
|
// The size the deck composes at: its own design.dimensions, else the theme's; a {preset} object reads as its preset.
|
|
453
483
|
// A custom size (inches without a preset) reads as the object itself. Unset reads as undefined (composed as widescreen).
|
|
454
|
-
|
|
455
|
-
let size = own.value;
|
|
456
|
-
let scope = own.scope;
|
|
484
|
+
let size = presentation.design?.dimensions;
|
|
457
485
|
if (size === undefined) {
|
|
458
|
-
const themeId = idOf(design
|
|
459
|
-
size = themeId ? findCatalogRecord(
|
|
460
|
-
scope = "deck";
|
|
486
|
+
const themeId = idOf(presentation.design?.theme);
|
|
487
|
+
size = themeId ? findCatalogRecord(presentation, "themes", themeId, {})?.dimensions : undefined;
|
|
461
488
|
}
|
|
462
|
-
return { value: size && typeof size === "object" && !Array.isArray(size) && Object.keys(size).length === 1 && size.preset ? size.preset : size, scope };
|
|
489
|
+
return { value: size && typeof size === "object" && !Array.isArray(size) && Object.keys(size).length === 1 && size.preset ? size.preset : size, scope: "deck" };
|
|
463
490
|
}
|
|
464
491
|
if (dimension === "purposes") {
|
|
465
492
|
// A catalog id or goal text reads as itself, a Purpose object as its id (else the object).
|
|
466
|
-
const purpose =
|
|
493
|
+
const purpose = presentation.purpose;
|
|
467
494
|
return { value: purpose && typeof purpose === "object" && !Array.isArray(purpose) ? (purpose.id ?? purpose) : purpose, scope: "deck" };
|
|
468
495
|
}
|
|
469
496
|
if (dimension === "socials") {
|
|
470
|
-
const host =
|
|
497
|
+
const host = presentation[options.owner ?? "speaker"];
|
|
471
498
|
const target = Array.isArray(host) ? host[options.index ?? 0] : host;
|
|
472
499
|
return { value: target?.socials, scope: "deck" };
|
|
473
500
|
}
|
|
474
|
-
if (ROOT_FIELD[dimension]) return { value:
|
|
501
|
+
if (ROOT_FIELD[dimension]) return { value: presentation[ROOT_FIELD[dimension]], scope: "deck" };
|
|
475
502
|
if (dimension === "color-schemes") return { ...design("colorScheme"), value: idOf(design("colorScheme").value) };
|
|
476
503
|
if (dimension === "font-schemes") return { ...design("fontScheme"), value: idOf(design("fontScheme").value) };
|
|
477
504
|
if (dimension === "themes") return { ...design("theme"), value: idOf(design("theme").value) };
|
package/dist/table-options.d.ts
CHANGED
|
@@ -39,15 +39,15 @@ export interface TableCellState extends TableCellAddress {
|
|
|
39
39
|
export interface PreparedTableChange {
|
|
40
40
|
action: "merge" | "split" | "style" | "table-style";
|
|
41
41
|
tablePath: string;
|
|
42
|
-
|
|
42
|
+
presentation: unknown;
|
|
43
43
|
patches: JsonPatchOperation[];
|
|
44
44
|
changed: boolean;
|
|
45
45
|
[key: string]: unknown;
|
|
46
46
|
}
|
|
47
|
-
export interface TableChange extends Omit<EditorChange, "
|
|
47
|
+
export interface TableChange extends Omit<EditorChange, "presentation" | "patches"> {
|
|
48
48
|
action: PreparedTableChange["action"];
|
|
49
49
|
tablePath: string;
|
|
50
|
-
|
|
50
|
+
presentation: unknown;
|
|
51
51
|
patches: JsonPatchOperation[];
|
|
52
52
|
changed: boolean;
|
|
53
53
|
[key: string]: unknown;
|
|
@@ -60,14 +60,14 @@ export declare function tableMerges(table: unknown): TableMerge[];
|
|
|
60
60
|
/** The state of one cell, including merge anchor/covered status. */
|
|
61
61
|
export declare function describeTableCell(table: unknown, cell: TableCellAddress): TableCellState;
|
|
62
62
|
/** Merge `span.colSpan` x `span.rowSpan` cells from `cell`. `join` keeps the text of covered cells by joining it into the anchor; without it, covered text refuses with `merge-would-lose-content`. */
|
|
63
|
-
export declare function prepareTableMerge(
|
|
64
|
-
export declare function prepareTableSplit(
|
|
63
|
+
export declare function prepareTableMerge(presentation: unknown, tablePath: string, cell: TableCellAddress, span: { colSpan?: number; rowSpan?: number }, options?: { join?: boolean }): PreparedTableChange;
|
|
64
|
+
export declare function prepareTableSplit(presentation: unknown, tablePath: string, cell: TableCellAddress): PreparedTableChange;
|
|
65
65
|
/** Merge style fields into the cells' styles (null removes a field, `style: null` clears). */
|
|
66
|
-
export declare function prepareTableCellStyle(
|
|
66
|
+
export declare function prepareTableCellStyle(presentation: unknown, tablePath: string, cells: TableCellAddress | TableCellAddress[], style: Record<string, unknown> | null): PreparedTableChange;
|
|
67
67
|
/** Apply `{ header, banding, borders }` (missing fields mean theme/false) or a named preset: sets the header fill, banded-row fill and borders of every cell and clears them elsewhere; `theme` removes a previous style. */
|
|
68
|
-
export declare function prepareTableStyle(
|
|
68
|
+
export declare function prepareTableStyle(presentation: unknown, tablePath: string, preset: keyof typeof TABLE_STYLE_PRESETS | Partial<TableStyle>): PreparedTableChange;
|
|
69
69
|
/** The current style per axis, or "custom" in every field when the fills and borders are not one this module writes. */
|
|
70
|
-
export declare function readTableStyle(
|
|
70
|
+
export declare function readTableStyle(presentation: unknown, tablePath: string): (TableStyle | { header: "custom"; banding: "custom"; borders: "custom" }) & { preset: keyof typeof TABLE_STYLE_PRESETS | "custom" };
|
|
71
71
|
export declare function mergeTableCells(editor: EditorSession, tablePath: string, cell: TableCellAddress, span: { colSpan?: number; rowSpan?: number }, options?: { join?: boolean; meta?: Record<string, unknown> }): TableChange;
|
|
72
72
|
export declare function splitTableCell(editor: EditorSession, tablePath: string, cell: TableCellAddress, meta?: Record<string, unknown>): TableChange;
|
|
73
73
|
export declare function setTableCellStyle(editor: EditorSession, tablePath: string, cells: TableCellAddress | TableCellAddress[], style: Record<string, unknown> | null, meta?: Record<string, unknown>): TableChange;
|