@openpresentation/opf-editor 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -29
- package/dist/assets.js +1 -1
- package/dist/background-options.d.ts +10 -3
- package/dist/background-options.js +38 -13
- package/dist/canvas.d.ts +8 -1
- package/dist/canvas.js +12 -9
- package/dist/catalogs.d.ts +48 -0
- package/dist/catalogs.js +217 -0
- package/dist/checks.js +9 -7
- package/dist/content-actions.d.ts +2 -2
- package/dist/content-actions.js +3 -3
- package/dist/content-controls.js +9 -9
- package/dist/design-controls.d.ts +12 -5
- package/dist/design-controls.js +260 -76
- package/dist/design-options.d.ts +4 -4
- package/dist/design-options.js +6 -26
- package/dist/export.d.ts +2 -0
- package/dist/export.js +8 -3
- package/dist/galleries.js +32 -67
- package/dist/image-crop.js +8 -9
- package/dist/image-options.d.ts +45 -0
- package/dist/image-options.js +195 -0
- package/dist/index.d.ts +25 -8
- package/dist/index.js +91 -47
- package/dist/json-editor.js +2 -2
- package/dist/json-menu.js +2 -2
- package/dist/json-options.d.ts +3 -3
- package/dist/json-options.js +30 -41
- package/dist/react.js +1 -1
- package/dist/review-panel.js +8 -2
- package/dist/schema-inspector.js +1 -1
- package/dist/schema.js +1 -1
- package/dist/slide-manager.js +1 -1
- package/dist/slides.d.ts +1 -1
- package/dist/slides.js +6 -5
- package/dist/svelte.js +1 -2
- package/dist/switches.d.ts +31 -16
- package/dist/switches.js +191 -90
- package/dist/transfer.d.ts +12 -1
- package/dist/transfer.js +87 -131
- package/package.json +9 -5
package/dist/catalogs.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Catalogs (FA-23, OPF 0.15). The editor never ships or imports catalog data: the host registers its catalogs (`Catalog[]`,
|
|
2
|
+
// the shape core's `CatalogOptions.catalogs` takes, for example `defaultCatalog` from `@openpresentation/opf/catalog`), on the
|
|
3
|
+
// session (`createEditorSession(doc, { catalogs })`) or per call. `mergeCatalogs` is the one function that turns those into the
|
|
4
|
+
// one list the editor hands to core (resolution, validation, embedding, copying, updates), to the renderer and to the exporter.
|
|
5
|
+
// Pickers list core's `catalogRecords` and write the reference it gives (`id` or `name:id`); saving and exporting embed every
|
|
6
|
+
// referenced record with core's `embed`, so a saved document renders the same with no catalog registered.
|
|
7
|
+
import { catalogKinds, catalogRecords, embed, moveToCustom as coreMoveToCustom, updateFromCatalog } from "@openpresentation/opf";
|
|
8
|
+
import { OPFEditorError } from "./index.js";
|
|
9
|
+
|
|
10
|
+
const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One catalog list from any number of lists (or single catalogs): the session's registration first, then each host list, in order.
|
|
14
|
+
* A catalog is identified by its `source`; the first catalog with a source wins and later ones with the same source are dropped,
|
|
15
|
+
* so the host default (the first entry) never moves. Entries that are not catalogs throw `invalid-catalogs` (core's
|
|
16
|
+
* `OPFCatalogsOptionError` code).
|
|
17
|
+
*/
|
|
18
|
+
export function mergeCatalogs(...lists) {
|
|
19
|
+
const merged = [];
|
|
20
|
+
const sources = new Set();
|
|
21
|
+
for (const list of lists) {
|
|
22
|
+
if (list === undefined || list === null) continue;
|
|
23
|
+
for (const catalog of Array.isArray(list) ? list : [list]) {
|
|
24
|
+
if (catalog === undefined || catalog === null) continue;
|
|
25
|
+
if (!isObject(catalog) || typeof catalog.source !== "string" || catalog.source === "")
|
|
26
|
+
throw new OPFEditorError("invalid-catalogs", "A registered catalog is an object with a non-empty source, such as defaultCatalog from @openpresentation/opf/catalog.", { catalog });
|
|
27
|
+
if (sources.has(catalog.source)) continue;
|
|
28
|
+
sources.add(catalog.source);
|
|
29
|
+
merged.push(catalog);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return Object.freeze(merged);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The catalogs for one operation: the session's registered list (when `editor` is given), then `options.catalogs`. */
|
|
36
|
+
export function catalogsFor(editor, options = {}) {
|
|
37
|
+
return mergeCatalogs(editor?.catalogs, options?.catalogs);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The label a picker shows for a record: its name, then its gallery label, then the id. */
|
|
41
|
+
export function catalogRecordLabel(record, id) {
|
|
42
|
+
const gallery = isObject(record?.["x-gallery"]) ? record["x-gallery"] : {};
|
|
43
|
+
for (const value of [record?.name, record?.title, record?.label, gallery.label, gallery.name]) if (typeof value === "string" && value) return value;
|
|
44
|
+
return String(id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Every record a picker can offer for `kind`, from core's `catalogRecords`: the document's embedded records first, then the
|
|
49
|
+
* registered catalogs'. Each entry is `{ id, reference, label, description, record, group, source, origin }`; `id` and
|
|
50
|
+
* `reference` are the reference to write (`id` or `name:id`). A reference is listed once.
|
|
51
|
+
*/
|
|
52
|
+
export function listCatalogRecords(presentation, kind, options = {}) {
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const entry of catalogRecords(presentation ?? {}, kind, { catalogs: mergeCatalogs(options.catalogs) })) {
|
|
56
|
+
if (seen.has(entry.reference)) continue;
|
|
57
|
+
seen.add(entry.reference);
|
|
58
|
+
const record = isObject(entry.record) ? entry.record : {};
|
|
59
|
+
out.push({
|
|
60
|
+
id: entry.reference,
|
|
61
|
+
reference: entry.reference,
|
|
62
|
+
label: catalogRecordLabel(record, entry.id),
|
|
63
|
+
...(typeof record.description === "string" ? { description: record.description } : {}),
|
|
64
|
+
record,
|
|
65
|
+
group: entry.group,
|
|
66
|
+
...(entry.source !== undefined ? { source: entry.source } : {}),
|
|
67
|
+
origin: entry.origin,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The document to save or hand on: core's `embed` adds every record the document references, once, under its catalog group
|
|
75
|
+
* (with the records those reference, `x-*` display metadata stripped), so it renders the same with no catalog registered.
|
|
76
|
+
* Returns `{ document, added, unresolved }`; the input is not changed.
|
|
77
|
+
*/
|
|
78
|
+
export function prepareSave(presentation, options = {}) {
|
|
79
|
+
const result = embed(presentation, { catalogs: mergeCatalogs(options.catalogs) });
|
|
80
|
+
return { document: result.document, added: result.added ?? [], unresolved: result.unresolved ?? [] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `prepareSave` for the session's document, with the session's catalogs and `options.catalogs`. The session is not changed. */
|
|
84
|
+
export function saveDocument(editor, options = {}) {
|
|
85
|
+
assertSession(editor);
|
|
86
|
+
return prepareSave(editor.presentation, { catalogs: catalogsFor(editor, options) });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The "Update from catalog" check: core's `updateFromCatalog` compares every record embedded under `default` or a named group
|
|
91
|
+
* (never `custom`) with the registered catalog's current record. Returns `{ changes, patch }`; nothing is applied. `refs` limits
|
|
92
|
+
* the check to some references (`[{ kind, reference }]`).
|
|
93
|
+
*/
|
|
94
|
+
export function checkCatalogUpdates(presentation, options = {}) {
|
|
95
|
+
const update = updateFromCatalog(presentation, mergeCatalogs(options.catalogs), options.refs);
|
|
96
|
+
return { changes: update.changes ?? [], patch: update.patch ?? [] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Apply an update the author approved, as ONE undoable step. `options.refs` names the approved changes (`[{ kind, reference }]`);
|
|
101
|
+
* the update is recomputed for exactly those against the session's current document, so a stale review never applies. Returns
|
|
102
|
+
* the session change plus `changes` (what was applied) and `changed`.
|
|
103
|
+
*/
|
|
104
|
+
export function applyCatalogUpdate(editor, options = {}) {
|
|
105
|
+
assertSession(editor);
|
|
106
|
+
if (!Array.isArray(options.refs) || !options.refs.length) throw new OPFEditorError("no-approved-updates", "Choose the catalog updates to apply.");
|
|
107
|
+
const update = checkCatalogUpdates(editor.presentation, { catalogs: catalogsFor(editor, options), refs: options.refs });
|
|
108
|
+
if (!update.patch.length) return { changed: false, changes: [], presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
|
|
109
|
+
const change = editor.applyPatch(update.patch, { ...options.meta, source: options.meta?.source ?? "catalog-update", catalogRecordEdit: "in-place", catalogUpdate: update.changes.map(({ kind, reference }) => ({ kind, reference })) });
|
|
110
|
+
return { ...change, changed: true, changes: update.changes };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Core finding codes about catalog references and embedded records, which the editor surfaces next to its catalog controls:
|
|
115
|
+
* a reference that resolves nowhere, a prefix with no catalog group, and a record embedded under a catalog's group that the
|
|
116
|
+
* registered catalog does not publish (offer `moveToCustom`).
|
|
117
|
+
*/
|
|
118
|
+
export const REFERENCE_FINDING_CODES = Object.freeze(["opf/unresolved-reference", "opf/undeclared-catalog", "opf/catalog-record-not-in-source"]);
|
|
119
|
+
|
|
120
|
+
/** The reference and embedded-record findings (`REFERENCE_FINDING_CODES`) of a validation report (the session's `validation`). */
|
|
121
|
+
export function referenceFindings(validation) {
|
|
122
|
+
return (validation?.findings ?? []).filter((finding) => REFERENCE_FINDING_CODES.includes(finding.ruleId ?? finding.code));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function assertSession(editor) {
|
|
126
|
+
if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function")
|
|
127
|
+
throw new OPFEditorError("invalid-editor", "Expected an editor session created by createEditorSession.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The record an `opf/catalog-record-not-in-source` finding names, from its path (`/catalogs/<group>/<kind>/<id>` or the dotted
|
|
132
|
+
* form): `{ group, kind, id }`, or undefined for a path that names no embedded record.
|
|
133
|
+
*/
|
|
134
|
+
export function catalogRecordAt(path) {
|
|
135
|
+
const parts = typeof path === "string" ? (path.startsWith("/") ? path.slice(1).split("/").map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~")) : path.split(".")) : [];
|
|
136
|
+
if (parts.length !== 4 || parts[0] !== "catalogs" || !catalogKinds.includes(parts[2])) return undefined;
|
|
137
|
+
return { group: parts[1], kind: parts[2], id: parts[3] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Move a record embedded under `default` or a named group into `catalogs.custom` (the fix the `opf/catalog-record-not-in-source`
|
|
144
|
+
* finding offers, and the fork an edit makes) with core's `moveToCustom`: the document owns it from then on. Every reference that
|
|
145
|
+
* resolved to it is rewritten to the custom id (`options.id`, else its id, `<id>-2` and up on a conflict), and references written
|
|
146
|
+
* inside it keep naming what they named. Returns `{ presentation, patches, changed, from, to, references }`.
|
|
147
|
+
*/
|
|
148
|
+
export function prepareMoveToCustom(presentation, target, options = {}) {
|
|
149
|
+
const { group, kind, id } = target ?? {};
|
|
150
|
+
const groups = isObject(presentation?.catalogs) ? presentation.catalogs : {};
|
|
151
|
+
const record = isObject(groups[group]?.[kind]) ? groups[group][kind][id] : undefined;
|
|
152
|
+
if (group === "custom" || record === undefined) throw new OPFEditorError("not-a-catalog-record", "Choose a record embedded under catalogs.default or a named catalog group.", { group, kind, id });
|
|
153
|
+
// Core's moveToCustom is the one implementation: references follow, the record's own references keep naming what they named.
|
|
154
|
+
const reference = group === "default" ? id : `${group}:${id}`;
|
|
155
|
+
const moved = coreMoveToCustom(presentation, { kind, reference }, { catalogs: mergeCatalogs(options.catalogs), ...(options.id ? { id: options.id } : {}) });
|
|
156
|
+
const patches = [{ op: "test", path: "/catalogs", value: structuredClone(presentation.catalogs) }, ...moved.patch];
|
|
157
|
+
return { presentation: moved.document, patches, changed: true, from: { group, kind, id }, to: { group: "custom", kind, id: moved.to.id }, references: moved.references ?? [] };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* `prepareMoveToCustom` on the session's document, as ONE undoable step (`meta.source: "catalog-move-to-custom"`). `options.id` names
|
|
162
|
+
* the custom record (default: the record's id, `<id>-2` on a conflict).
|
|
163
|
+
*/
|
|
164
|
+
export function moveToCustom(editor, target, options = {}) {
|
|
165
|
+
assertSession(editor);
|
|
166
|
+
const prepared = prepareMoveToCustom(editor.presentation, target, { catalogs: catalogsFor(editor, options), ...(options.id ? { id: options.id } : {}) });
|
|
167
|
+
const change = editor.applyPatch(prepared.patches, { ...options.meta, source: options.meta?.source ?? "catalog-move-to-custom", catalogRecordEdit: "in-place" });
|
|
168
|
+
return { ...change, changed: true, from: prepared.from, to: prepared.to };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The record kinds a user edits, which an edit forks into `catalogs.custom` when they live under a catalog's group. */
|
|
172
|
+
export const FORKED_RECORD_KINDS = Object.freeze(["layouts", "themes", "colorSchemes", "fontSchemes"]);
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Records under `catalogs.default` or a named group whose content an edit from `before` to `after` changed (present in both, not
|
|
176
|
+
* equal). An added or removed record is not an edit. Returns `[{ group, kind, id }]`.
|
|
177
|
+
*/
|
|
178
|
+
export function editedCatalogRecords(before, after) {
|
|
179
|
+
const out = [];
|
|
180
|
+
const groupsBefore = isObject(before?.catalogs) ? before.catalogs : {};
|
|
181
|
+
const groupsAfter = isObject(after?.catalogs) ? after.catalogs : {};
|
|
182
|
+
for (const [group, value] of Object.entries(groupsAfter)) {
|
|
183
|
+
if (group === "custom" || !isObject(value) || !isObject(groupsBefore[group])) continue;
|
|
184
|
+
for (const kind of FORKED_RECORD_KINDS) {
|
|
185
|
+
const was = isObject(groupsBefore[group][kind]) ? groupsBefore[group][kind] : {};
|
|
186
|
+
const now = isObject(value[kind]) ? value[kind] : {};
|
|
187
|
+
for (const id of Object.keys(now)) if (Object.hasOwn(was, id) && !sameJson(was[id], now[id])) out.push({ group, kind, id });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The notice an editor shows after a fork: the record is the deck's own now and no longer receives catalog updates. */
|
|
194
|
+
export function forkNotice(fork) {
|
|
195
|
+
const label = { layouts: "layout", themes: "theme", colorSchemes: "color scheme", fontSchemes: "font scheme" }[fork.from.kind] ?? fork.from.kind;
|
|
196
|
+
return `The ${label} ${fork.from.id} is now this presentation's own (${fork.to.id}); it no longer receives catalog updates.`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Owner rule (FA-23): editing a record that lives under `catalogs.default` or a named group forks it into `catalogs.custom` instead
|
|
201
|
+
* of changing the catalog's record in place. Given the document before and after an edit, returns the document after the forks
|
|
202
|
+
* (each edited record moved to custom as `<id>-custom`, or `forkIds[<group>:<kind>:<id>]`, every reference rewritten, the
|
|
203
|
+
* original dropped) and `forks: [{ from, to }]`. Nothing to fork returns `after` and `[]`.
|
|
204
|
+
*/
|
|
205
|
+
export function forkEditedRecords(before, after, options = {}) {
|
|
206
|
+
let document = after;
|
|
207
|
+
const forks = [];
|
|
208
|
+
for (const target of editedCatalogRecords(before, after)) {
|
|
209
|
+
const custom = isObject(document.catalogs?.custom?.[target.kind]) ? document.catalogs.custom[target.kind] : {};
|
|
210
|
+
let id = options.forkIds?.[`${target.group}:${target.kind}:${target.id}`] ?? `${target.id}-custom`;
|
|
211
|
+
for (let n = 2; Object.hasOwn(custom, id); n += 1) id = `${target.id}-custom-${n}`;
|
|
212
|
+
const moved = prepareMoveToCustom(document, target, { catalogs: options.catalogs, id });
|
|
213
|
+
document = moved.presentation;
|
|
214
|
+
forks.push({ from: moved.from, to: moved.to });
|
|
215
|
+
}
|
|
216
|
+
return { document, forks };
|
|
217
|
+
}
|
package/dist/checks.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
// The editor's one use of core's checker. Every edit is validated with `validate
|
|
2
|
-
// schema and semantic OPF rules, with no
|
|
3
|
-
//
|
|
1
|
+
// The editor's one use of core's checker. Every edit is validated with core's `validate` for the `format` category and the two
|
|
2
|
+
// catalog rules (`opf/unresolved-reference`, `opf/undeclared-catalog`, `opf/catalog-record-not-in-source`): the schema and semantic OPF rules, with no
|
|
3
|
+
// layout built, so it is cheap enough to run on every keystroke and undo. `catalogs` are the host's registered catalogs, so a
|
|
4
|
+
// reference that resolves in one of them is not reported. The full report (accessibility, layout, content) belongs to the Review
|
|
5
|
+
// panel, which runs it on idle (`./review.js`).
|
|
4
6
|
import { validate } from "@openpresentation/opf";
|
|
5
7
|
|
|
6
|
-
const
|
|
8
|
+
const EDIT_CATEGORIES = Object.freeze(["format", "opf/unresolved-reference", "opf/undeclared-catalog", "opf/catalog-record-not-in-source"]);
|
|
7
9
|
|
|
8
|
-
/** Core's `ValidationReport` for the `format` category: `valid`, `findings`, `counts`, `schemaValid`. */
|
|
9
|
-
export function checkFormat(presentation) {
|
|
10
|
-
return validate(presentation,
|
|
10
|
+
/** Core's `ValidationReport` for the `format` category and the catalog reference rules: `valid`, `findings`, `counts`, `schemaValid`. */
|
|
11
|
+
export function checkFormat(presentation, options = {}) {
|
|
12
|
+
return validate(presentation, { only: EDIT_CATEGORIES, ...(options.catalogs?.length ? { catalogs: options.catalogs } : {}) });
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
/** The error-severity findings of a report: what makes it invalid. */
|
|
@@ -72,9 +72,9 @@ export declare function ungroupBlock(editor: EditorSession, groupPath: string, m
|
|
|
72
72
|
export declare function placeBlocksInRegions(editor: EditorSession, slideIndex: number, regions: string[], meta?: Record<string, unknown>): ContentActionChange;
|
|
73
73
|
export declare function regionsAsBlocks(editor: EditorSession, slideIndex: number, meta?: Record<string, unknown>): ContentActionChange;
|
|
74
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
|
|
75
|
+
/** Move an image block into the slide's design as its background or watermark. */
|
|
76
76
|
export declare function moveImageToDesign(editor: EditorSession, blockPath: string, target: ImageTarget, options?: PromoteImageOptions, meta?: Record<string, unknown>): ContentActionChange;
|
|
77
|
-
/** Move a slide's own
|
|
77
|
+
/** Move a slide's own 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
80
|
export declare function prepareSplitSlide(presentation: unknown, slideIndex: number, options?: { at?: number[]; each?: boolean; repeatHeadings?: boolean }): PreparedContentAction;
|
package/dist/content-actions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Content actions (RR-26): list levels, grouping, regions, images between content and design, and slide
|
|
1
|
+
// Content actions (RR-26): list levels, grouping, regions, images between content and design (background or watermark), and slide
|
|
2
2
|
// split and merge, as editor transactions. The pure transforms live in core (`@openpresentation/opf/convert`);
|
|
3
3
|
// each action here finds its target in the document, calls the transform, turns the result into a guarded patch
|
|
4
4
|
// (`test` of what was read, then `replace`, or per-slide `remove` and `add`), validates the document and, in the
|
|
@@ -143,12 +143,12 @@ export function prepareMoveRegion(presentation, slideIndex, from, to, options =
|
|
|
143
143
|
slideParts(presentation, `slides.${slideIndex}`, "a slide");
|
|
144
144
|
return slideAction(presentation, slideIndex, (slide) => moveRegion(slide, from, to, options), undefined, options.validate !== false);
|
|
145
145
|
}
|
|
146
|
-
/** Move the image block at `blockPath` into the slide's design as
|
|
146
|
+
/** Move the image block at `blockPath` into the slide's design as its `background` or `watermark` (core's `promoteImage`). */
|
|
147
147
|
export function prepareImageToDesign(presentation, blockPath, target, options = {}) {
|
|
148
148
|
const { slideIndex, relative } = slideParts(presentation, blockPath, "an image block");
|
|
149
149
|
return slideAction(presentation, slideIndex, (slide) => promoteImage(slide, asNumbers(relative), target, options), `slides.${slideIndex}`, options.validate !== false);
|
|
150
150
|
}
|
|
151
|
-
/** Move a slide's own `
|
|
151
|
+
/** Move a slide's own `background` image or `watermark` back into its content as an image block (core's `demoteImage`). */
|
|
152
152
|
export function prepareImageToContent(presentation, slideIndex, source, options = {}) {
|
|
153
153
|
slideParts(presentation, `slides.${slideIndex}`, "a slide");
|
|
154
154
|
return slideAction(presentation, slideIndex, (slide) => demoteImage(slide, source, options), undefined, options.validate !== false);
|
package/dist/content-controls.js
CHANGED
|
@@ -36,20 +36,20 @@ export const REGION_LAYOUTS = Object.freeze({
|
|
|
36
36
|
4: [{ id: "grid", label: "Two by two", regions: ["top:left", "top:right", "bottom:left", "bottom:right"] }],
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Where an image block can go in the slide's design (OPF 0.15): its background (a full-bleed photo behind the content) or its
|
|
41
|
+
* watermark. Bleeding an image to one edge beside the content is not a move: it is the block's `placement` (image-options.js).
|
|
42
|
+
*/
|
|
40
43
|
export const IMAGE_DESTINATIONS = Object.freeze([
|
|
41
|
-
{ value: "
|
|
42
|
-
{ value: "slideImage:left", label: "Slide image, on the left", target: "slideImage", options: { position: "left" } },
|
|
43
|
-
{ value: "slideImage:top", label: "Slide image, along the top", target: "slideImage", options: { position: "top" } },
|
|
44
|
-
{ value: "slideImage:bottom", label: "Slide image, along the bottom", target: "slideImage", options: { position: "bottom" } },
|
|
45
|
-
{ value: "slideImage:background", label: "Slide image, full slide behind the content", target: "slideImage", options: { position: "background" } },
|
|
46
|
-
{ value: "background", label: "Slide background", target: "background", options: {} },
|
|
44
|
+
{ value: "background", label: "Slide background, full slide behind the content", target: "background", options: {} },
|
|
47
45
|
{ value: "watermark", label: "Watermark", target: "watermark", options: {} },
|
|
48
46
|
]);
|
|
49
47
|
|
|
50
48
|
const REGION_KEY = /^(?:(?:top|middle|bottom)(?:\+(?:top|middle|bottom))*(?::(?:left|center|right)(?:\+(?:left|center|right))*)?|(?:left|center|right)(?:\+(?:left|center|right))*)$/;
|
|
51
49
|
const regionKeysOfSlide = (slide) => Object.keys(slide).filter((key) => REGION_KEY.test(key));
|
|
52
|
-
const DESIGN_IMAGE_LABELS = {
|
|
50
|
+
const DESIGN_IMAGE_LABELS = { background: "background image", watermark: "watermark" };
|
|
51
|
+
// An image block's own framing fields (FA-22) do not stop it being "a block that holds only an image".
|
|
52
|
+
const IMAGE_BLOCK_FIELDS = new Set(["id", "extensions", "type", "fit", "focus", "aspectRatio", "shape", "cornerRadius", "border", "opacity", "recolor", "overlay", "placement"]);
|
|
53
53
|
const messageOf = (error) => error?.issues?.[0]?.message ?? error?.message ?? String(error);
|
|
54
54
|
const lossText = (prepared) => (prepared.loss.length ? `Not carried over: ${prepared.loss.join(", ")}.` : "Nothing is lost.");
|
|
55
55
|
|
|
@@ -321,6 +321,6 @@ export function createContentControls(ctx) {
|
|
|
321
321
|
function isImageBlock(editor, blockPath) {
|
|
322
322
|
const block = editor.get(blockPath);
|
|
323
323
|
if (!block || typeof block !== "object" || Array.isArray(block)) return false;
|
|
324
|
-
const keys = Object.keys(block).filter((key) => key
|
|
324
|
+
const keys = Object.keys(block).filter((key) => !IMAGE_BLOCK_FIELDS.has(key));
|
|
325
325
|
return keys.length === 1 && keys[0] === "image";
|
|
326
326
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import type { Catalog } from "@openpresentation/opf";
|
|
1
2
|
import type { EditorSession } from "./index.js";
|
|
3
|
+
import type { SwitchVocabularies } from "./switches.js";
|
|
2
4
|
|
|
3
|
-
export type DesignControlSection = "look" | "background" | "
|
|
4
|
-
/**
|
|
5
|
+
export type DesignControlSection = "look" | "background" | "header-footer" | "brand" | "layout-options" | "info" | "selection" | "image" | "table" | "slide-content" | "catalog";
|
|
6
|
+
/**
|
|
7
|
+
* Every section, in panel order. `selection`, `image` (the selected image block's fit, focus, treatments and placement) and `table`
|
|
8
|
+
* follow the host's current selection; `slide-content` (RR-26: blocks and regions, images between content and design) follows the
|
|
9
|
+
* current slide; `catalog` lists the catalog reference findings and runs the explicit "Update from catalog".
|
|
10
|
+
*/
|
|
5
11
|
export declare const DESIGN_CONTROL_SECTIONS: readonly DesignControlSection[];
|
|
6
12
|
|
|
7
13
|
export interface DesignControlsOptions {
|
|
@@ -14,9 +20,10 @@ export interface DesignControlsOptions {
|
|
|
14
20
|
sections?: readonly DesignControlSection[];
|
|
15
21
|
/** Initial "Applies to" choice. */
|
|
16
22
|
scope?: "deck" | "slide";
|
|
17
|
-
/**
|
|
18
|
-
catalogs?:
|
|
19
|
-
|
|
23
|
+
/** Host catalogs for the pickers, merged after the session's registered ones. */
|
|
24
|
+
catalogs?: readonly Catalog[];
|
|
25
|
+
/** Display metadata for the engine vocabularies (languages, chart types, social platforms): `catalogDisplay` from `@openpresentation/opf/catalog`. */
|
|
26
|
+
vocabularies?: SwitchVocabularies;
|
|
20
27
|
/** Called after every committed change (never for a refused one). */
|
|
21
28
|
onChange?: (change: unknown) => void;
|
|
22
29
|
/** Called after a content conversion or replacement with the path to keep selected (the block, or the inline payload field), since the old selection path may no longer exist. */
|