@openpresentation/opf-editor 0.14.2 → 0.16.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 +93 -35
- 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 +25 -10
- package/dist/catalog-control.js +21 -0
- 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 +286 -82
- package/dist/design-options.d.ts +4 -4
- package/dist/design-options.js +6 -26
- package/dist/export.d.ts +8 -1
- package/dist/export.js +14 -4
- package/dist/find-replace.js +1 -0
- package/dist/font-gate.js +0 -11
- 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 +26 -8
- package/dist/index.js +107 -51
- 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 +37 -47
- package/dist/layout-handles.js +13 -0
- package/dist/layout-placeholders.js +15 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.js +6 -2
- 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 +4 -2
- package/dist/slides.d.ts +1 -1
- package/dist/slides.js +6 -5
- package/dist/svelte.d.ts +1 -0
- package/dist/svelte.js +4 -4
- package/dist/switches.d.ts +31 -16
- package/dist/switches.js +194 -92
- package/dist/table-options.d.ts +6 -1
- package/dist/table-options.js +43 -0
- package/dist/transfer.d.ts +12 -1
- package/dist/transfer.js +87 -131
- package/package.json +22 -9
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { PaginationOptions, PaginationResult } from "@openpresentation/opf/pagination";
|
|
2
2
|
import type { Composition, ComposeSlideOptions, SlideComposition } from "@openpresentation/opf/composition";
|
|
3
|
-
import type { Fonts, PresentationStats, SlideContextDiagnostic, StatsOptions, ValidationReport } from "@openpresentation/opf";
|
|
3
|
+
import type { Catalog, Fonts, PresentationStats, SlideContextDiagnostic, StatsOptions, ValidationReport } from "@openpresentation/opf";
|
|
4
|
+
export * from "./catalogs.js";
|
|
4
5
|
/**
|
|
5
6
|
* What the editor adds to the options of `composeSlide` and `paginateSlide`: `fonts` is the renderer's fonts handle (`loadFonts()`), whose
|
|
6
|
-
* `textMeasurement` measures the text; `catalogs` are host
|
|
7
|
-
* `
|
|
8
|
-
* the resolved one.
|
|
7
|
+
* `textMeasurement` measures the text; `catalogs` are host catalogs for this call (merged after the session's registered ones);
|
|
8
|
+
* `onDiagnostic` hears every diagnostic of core's `resolveSlideContext` (`unresolved-reference`). Any other
|
|
9
|
+
* option overrides the resolved one.
|
|
9
10
|
*/
|
|
10
11
|
export interface EditorDiagnosticOptions {
|
|
11
12
|
fonts?: Fonts;
|
|
12
|
-
catalogs?:
|
|
13
|
+
catalogs?: readonly Catalog[];
|
|
13
14
|
onDiagnostic?: (diagnostic: SlideContextDiagnostic) => void;
|
|
14
15
|
}
|
|
15
16
|
export declare const packageName = "@openpresentation/opf-editor";
|
|
@@ -56,7 +57,8 @@ export interface EditorChange {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export interface EditorEvent {
|
|
59
|
-
|
|
60
|
+
/** `catalogs`: the registered catalogs changed (`setCatalogs`); the document did not. */
|
|
61
|
+
type: "patch" | "undo" | "redo" | "restore" | "catalogs";
|
|
60
62
|
patches: JsonPatchOperation[];
|
|
61
63
|
inversePatches?: JsonPatchOperation[];
|
|
62
64
|
redoPatches?: JsonPatchOperation[];
|
|
@@ -73,7 +75,12 @@ export interface EditorSession {
|
|
|
73
75
|
composeSlide(slideIndex: number, options?: ComposeSlideOptions & EditorDiagnosticOptions): SlideComposition;
|
|
74
76
|
setComposition(slideIndex: number, composition: Composition, meta?: Record<string, unknown>): EditorChange;
|
|
75
77
|
readonly presentation: unknown;
|
|
78
|
+
/** The `format` report of the document plus the catalog reference rules, checked against the registered catalogs. */
|
|
76
79
|
readonly validation: ValidationReport;
|
|
80
|
+
/** The host's registered catalogs, frozen; the first is the host default. */
|
|
81
|
+
readonly catalogs: readonly Catalog[];
|
|
82
|
+
/** Replace the registered catalogs: re-validates the document and emits one `catalogs` event. */
|
|
83
|
+
setCatalogs(catalogs: readonly Catalog[] | undefined, meta?: Record<string, unknown>): readonly Catalog[];
|
|
77
84
|
readonly canUndo: boolean;
|
|
78
85
|
readonly canRedo: boolean;
|
|
79
86
|
snapshot(): EditorSnapshot;
|
|
@@ -94,18 +101,28 @@ export interface EditorSession {
|
|
|
94
101
|
export interface CreateEditorSessionOptions {
|
|
95
102
|
/** Refuse every edit whose result has an error finding (the default per edit is `meta.rejectInvalid`). */
|
|
96
103
|
rejectInvalid?: boolean;
|
|
104
|
+
/** The host's catalogs (for example `[defaultCatalog]` from `@openpresentation/opf/catalog`): resolution, validation, pickers, embedding. */
|
|
105
|
+
catalogs?: readonly Catalog[];
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
export interface CatalogOption {
|
|
109
|
+
/** The reference to write: `id` or `name:id`. */
|
|
100
110
|
id: string;
|
|
111
|
+
reference: string;
|
|
101
112
|
label: string;
|
|
113
|
+
description?: string;
|
|
102
114
|
record: Record<string, unknown>;
|
|
115
|
+
/** The catalog group the record resolves in (`custom`, `default` or a named group). */
|
|
116
|
+
group: string;
|
|
117
|
+
source?: string;
|
|
118
|
+
origin: "document" | "host";
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
export interface CatalogOptionsInput {
|
|
106
122
|
presentation?: { catalogs?: Record<string, unknown> } | Record<string, unknown>;
|
|
107
|
-
catalogs
|
|
108
|
-
|
|
123
|
+
/** Host catalogs, merged after `editor`'s registered ones. */
|
|
124
|
+
catalogs?: readonly Catalog[];
|
|
125
|
+
editor?: EditorSession;
|
|
109
126
|
[key: string]: unknown;
|
|
110
127
|
}
|
|
111
128
|
|
|
@@ -145,6 +162,7 @@ export interface CatalogSelectOptions extends CatalogOptionsInput {
|
|
|
145
162
|
path: string;
|
|
146
163
|
catalogKind: string;
|
|
147
164
|
label?: string;
|
|
165
|
+
onError?: (error: Error) => void;
|
|
148
166
|
document?: { createElement(tagName: string): any };
|
|
149
167
|
}
|
|
150
168
|
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,39 @@
|
|
|
1
|
+
import { commitCatalogControl } from "./catalog-control.js";
|
|
1
2
|
import { paginateSlide } from "@openpresentation/opf/pagination";
|
|
2
3
|
import { OPFPatchError, applyPatch as applyCorePatch, applyPatchWithInverse, formatPointer, invertPatch, jsonEqual, parsePointer, readPointer } from "@openpresentation/opf/patch";
|
|
3
4
|
import { collectReservedPresentationIds } from "./presentation-ids.js";
|
|
4
5
|
import { composeSlide } from "@openpresentation/opf/composition";
|
|
5
|
-
import { catalogKinds,
|
|
6
|
+
import { catalogKinds, resolveSlideContext, stats } from "@openpresentation/opf";
|
|
6
7
|
import { checkFormat, errorFindings } from "./checks.js";
|
|
8
|
+
import { forkEditedRecords, forkNotice, listCatalogRecords, mergeCatalogs } from "./catalogs.js";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
REFERENCE_FINDING_CODES,
|
|
12
|
+
applyCatalogUpdate,
|
|
13
|
+
catalogRecordAt,
|
|
14
|
+
catalogRecordLabel,
|
|
15
|
+
catalogsFor,
|
|
16
|
+
checkCatalogUpdates,
|
|
17
|
+
editedCatalogRecords,
|
|
18
|
+
FORKED_RECORD_KINDS,
|
|
19
|
+
forkEditedRecords,
|
|
20
|
+
forkNotice,
|
|
21
|
+
listCatalogRecords,
|
|
22
|
+
mergeCatalogs,
|
|
23
|
+
moveToCustom,
|
|
24
|
+
prepareMoveToCustom,
|
|
25
|
+
prepareSave,
|
|
26
|
+
referenceFindings,
|
|
27
|
+
saveDocument,
|
|
28
|
+
} from "./catalogs.js";
|
|
7
29
|
|
|
8
30
|
// The options `composeSlide` and `paginateSlide` take for one slide of the open deck. Core's `resolveSlideContext` resolves the
|
|
9
31
|
// slide's canvas, layout, theme, colour scheme and font families the one way every engine does (slide design, deck design,
|
|
10
|
-
// theme, default), so the editor measures and composes what the renderer draws
|
|
11
|
-
// renderer's fonts handle (its `textMeasurement` measures); `options.
|
|
32
|
+
// theme, engine default), with the session's registered catalogs, so the editor measures and composes what the renderer draws
|
|
33
|
+
// and the exporter writes. `options.fonts` is the renderer's fonts handle (its `textMeasurement` measures); `options.catalogs`
|
|
34
|
+
// adds catalogs for this call; `options.onDiagnostic` hears each diagnostic (`unresolved-reference`);
|
|
12
35
|
// any other option overrides the resolved one (`layout`, ...).
|
|
13
|
-
function slideContext(presentation, slideIndex, { fonts, catalogs, onDiagnostic, ...overrides } = {}) {
|
|
36
|
+
function slideContext(presentation, slideIndex, catalogs, { fonts, catalogs: _extra, onDiagnostic, ...overrides } = {}) {
|
|
14
37
|
if (!Number.isInteger(slideIndex) || !presentation.slides?.[slideIndex]) throw new OPFEditorError("slide-index-out-of-range", "Slide index is out of range.");
|
|
15
38
|
const { options, diagnostics } = resolveSlideContext(presentation, slideIndex, { fonts, catalogs });
|
|
16
39
|
for (const diagnostic of diagnostics) onDiagnostic?.(diagnostic);
|
|
@@ -123,7 +146,10 @@ export function deckStats(editor, options) {
|
|
|
123
146
|
|
|
124
147
|
export function createEditorSession(input, options = {}) {
|
|
125
148
|
let presentation = parseInput(input);
|
|
126
|
-
|
|
149
|
+
// The host's registered catalogs (FA-23): one list, handed to every core call, the renderer and the exporter.
|
|
150
|
+
let catalogs = mergeCatalogs(options.catalogs);
|
|
151
|
+
const check = (document) => checkFormat(document, { catalogs });
|
|
152
|
+
let validation = check(presentation);
|
|
127
153
|
const undoStack = [];
|
|
128
154
|
const redoStack = [];
|
|
129
155
|
const listeners = new Set();
|
|
@@ -138,7 +164,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
138
164
|
|
|
139
165
|
function commitPatch(operations, meta = {}) {
|
|
140
166
|
assertPatchOperations(operations);
|
|
141
|
-
|
|
167
|
+
let patches = operations.map(normalizeOperation);
|
|
142
168
|
const before = presentation;
|
|
143
169
|
let next, inversePatches;
|
|
144
170
|
try {
|
|
@@ -146,7 +172,24 @@ export function createEditorSession(input, options = {}) {
|
|
|
146
172
|
} catch (error) {
|
|
147
173
|
throw editorPatchError(error);
|
|
148
174
|
}
|
|
149
|
-
|
|
175
|
+
// Owner rule (FA-23): an edit of a record under catalogs.default or a named group forks it into catalogs.custom (every reference
|
|
176
|
+
// follows, the original is dropped), in this same undo step. Catalog updates and moves change catalog records on purpose.
|
|
177
|
+
let forks = [];
|
|
178
|
+
if (meta.catalogRecordEdit !== "in-place") {
|
|
179
|
+
const forked = forkEditedRecords(before, next, { catalogs, forkIds: meta.forkIds });
|
|
180
|
+
if (forked.forks.length) {
|
|
181
|
+
forks = forked.forks;
|
|
182
|
+
for (const key of new Set([...Object.keys(next), ...Object.keys(forked.document)]))
|
|
183
|
+
if (!jsonEqual(next[key], forked.document[key])) patches = [...patches, forked.document[key] === undefined ? { op: "remove", path: formatPointer([key]) } : { op: Object.hasOwn(next, key) ? "replace" : "add", path: formatPointer([key]), value: clone(forked.document[key]) }];
|
|
184
|
+
try {
|
|
185
|
+
({ presentation: next, inverse: inversePatches } = applyPatchWithInverse(before, patches));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw editorPatchError(error);
|
|
188
|
+
}
|
|
189
|
+
meta = { ...meta, forked: forks, notice: forks.map(forkNotice).join(" ") };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const nextValidation = check(next);
|
|
150
193
|
|
|
151
194
|
if ((meta.rejectInvalid ?? rejectInvalid) && !nextValidation.valid) {
|
|
152
195
|
throw new OPFEditorError("invalid-opf-edit", "OPF edit produced an invalid document.", {
|
|
@@ -174,7 +217,8 @@ export function createEditorSession(input, options = {}) {
|
|
|
174
217
|
presentation: clone(presentation),
|
|
175
218
|
patches: clonePatchOperations(patches),
|
|
176
219
|
inversePatches: clonePatchOperations(inversePatches),
|
|
177
|
-
validation
|
|
220
|
+
validation,
|
|
221
|
+
...(forks.length ? { forked: forks, notice: meta.notice } : {})
|
|
178
222
|
};
|
|
179
223
|
}
|
|
180
224
|
|
|
@@ -185,6 +229,20 @@ export function createEditorSession(input, options = {}) {
|
|
|
185
229
|
get validation() {
|
|
186
230
|
return validation;
|
|
187
231
|
},
|
|
232
|
+
/** The catalogs the host registered (`Catalog[]`, frozen): the first is the host default. */
|
|
233
|
+
get catalogs() {
|
|
234
|
+
return catalogs;
|
|
235
|
+
},
|
|
236
|
+
/**
|
|
237
|
+
* Replace the registered catalogs (for example after the host loads a company catalog). The document is not changed; it is
|
|
238
|
+
* re-validated against the new list (reference findings depend on it) and one `catalogs` event tells every view to redraw.
|
|
239
|
+
*/
|
|
240
|
+
setCatalogs(next, meta = {}) {
|
|
241
|
+
catalogs = mergeCatalogs(next);
|
|
242
|
+
validation = check(presentation);
|
|
243
|
+
emit({ type: "catalogs", patches: [], validation, meta });
|
|
244
|
+
return catalogs;
|
|
245
|
+
},
|
|
188
246
|
get canUndo() {
|
|
189
247
|
return undoStack.length > 0;
|
|
190
248
|
},
|
|
@@ -218,13 +276,15 @@ export function createEditorSession(input, options = {}) {
|
|
|
218
276
|
});
|
|
219
277
|
},
|
|
220
278
|
composeSlide(slideIndex, options = {}) {
|
|
221
|
-
return composeSlide(presentation.slides?.[slideIndex], slideContext(presentation, slideIndex, options));
|
|
279
|
+
return composeSlide(presentation.slides?.[slideIndex], slideContext(presentation, slideIndex, mergeCatalogs(catalogs, options.catalogs), options));
|
|
222
280
|
},
|
|
223
281
|
paginateSlide(slideIndex, options = {}, meta = {}) {
|
|
282
|
+
const list = mergeCatalogs(catalogs, options.catalogs);
|
|
224
283
|
// Core pagination reads the measurement from `fonts`, so the resolved context hands it over that way.
|
|
225
|
-
const { textMeasurement, ...resolved } = slideContext(presentation, slideIndex, options);
|
|
284
|
+
const { textMeasurement, ...resolved } = slideContext(presentation, slideIndex, list, options);
|
|
226
285
|
const pagination = paginateSlide(presentation.slides[slideIndex], {
|
|
227
286
|
...resolved,
|
|
287
|
+
catalogs: list,
|
|
228
288
|
fonts: options.fonts ?? (textMeasurement ? { textMeasurement } : undefined),
|
|
229
289
|
reservedIds: collectReservedPresentationIds(presentation),
|
|
230
290
|
});
|
|
@@ -267,7 +327,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
267
327
|
restoreState(state, meta = {}) {
|
|
268
328
|
if (!state || typeof state !== "object") throw new OPFEditorError("invalid-state", "restoreState needs { presentation, undo?, redo? }.");
|
|
269
329
|
const next = parseInput(state.presentation);
|
|
270
|
-
const nextValidation =
|
|
330
|
+
const nextValidation = check(next);
|
|
271
331
|
if ((meta.rejectInvalid ?? rejectInvalid) && !nextValidation.valid) {
|
|
272
332
|
throw new OPFEditorError("invalid-opf-edit", "The restored document is not valid OPF.", { issues: errorFindings(nextValidation) });
|
|
273
333
|
}
|
|
@@ -304,7 +364,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
304
364
|
const entry = undoStack.pop();
|
|
305
365
|
if (!entry) return null;
|
|
306
366
|
presentation = applyJsonPatch(presentation, entry.inversePatches);
|
|
307
|
-
validation =
|
|
367
|
+
validation = check(presentation);
|
|
308
368
|
redoStack.push(entry);
|
|
309
369
|
emit({
|
|
310
370
|
type: "undo",
|
|
@@ -324,7 +384,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
324
384
|
const entry = redoStack.pop();
|
|
325
385
|
if (!entry) return null;
|
|
326
386
|
presentation = applyJsonPatch(presentation, entry.patches);
|
|
327
|
-
validation =
|
|
387
|
+
validation = check(presentation);
|
|
328
388
|
undoStack.push(entry);
|
|
329
389
|
emit({
|
|
330
390
|
type: "redo",
|
|
@@ -415,29 +475,29 @@ export function createSvgTraceBinding(root, editor, options = {}) {
|
|
|
415
475
|
};
|
|
416
476
|
}
|
|
417
477
|
|
|
478
|
+
/**
|
|
479
|
+
* The records a catalog control can offer for `catalogKind`, from core's `catalogRecords`: the document's embedded records,
|
|
480
|
+
* then the registered catalogs' (`options.catalogs`, merged with `options.editor`'s). Each is the record itself.
|
|
481
|
+
*/
|
|
418
482
|
export function getCatalogRecords(catalogKind, options = {}) {
|
|
419
|
-
|
|
420
|
-
const presentationCatalog = options.presentation?.catalogs?.[catalogKind];
|
|
421
|
-
const explicitCatalog = options.catalogs?.[catalogKind] ?? options.catalogSources?.[catalogKind];
|
|
422
|
-
|
|
423
|
-
return firstRecords(presentationCatalog)
|
|
424
|
-
?? firstRecords(explicitCatalog)
|
|
425
|
-
?? firstRecords(bundledCatalogs[catalogKind])
|
|
426
|
-
?? [];
|
|
483
|
+
return getCatalogOptions(catalogKind, options).map((option) => option.record);
|
|
427
484
|
}
|
|
428
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Picker options for `catalogKind`: `{ id, reference, label, record, group, source, origin }`, where `id` (= `reference`) is the
|
|
488
|
+
* reference to write, `id` or `name:id`. `options.presentation` is the document (default: `options.editor`'s), `options.catalogs`
|
|
489
|
+
* the host catalogs (merged after the session's registered ones).
|
|
490
|
+
*/
|
|
429
491
|
export function getCatalogOptions(catalogKind, options = {}) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
record
|
|
434
|
-
}));
|
|
492
|
+
assertCatalogKind(catalogKind);
|
|
493
|
+
const presentation = options.presentation ?? options.editor?.presentation ?? {};
|
|
494
|
+
return listCatalogRecords(presentation, catalogKind, { catalogs: mergeCatalogs(options.editor?.catalogs, options.catalogs) });
|
|
435
495
|
}
|
|
436
496
|
|
|
437
497
|
export function assertCatalogId(catalogKind, id, options = {}) {
|
|
438
498
|
assertCatalogKind(catalogKind);
|
|
439
499
|
if (typeof id !== "string" || id.length === 0) {
|
|
440
|
-
throw new OPFEditorError("invalid-catalog-id", "Catalog control values must be non-empty catalog
|
|
500
|
+
throw new OPFEditorError("invalid-catalog-id", "Catalog control values must be non-empty catalog references.", {
|
|
441
501
|
catalogKind,
|
|
442
502
|
id
|
|
443
503
|
});
|
|
@@ -445,7 +505,7 @@ export function assertCatalogId(catalogKind, id, options = {}) {
|
|
|
445
505
|
|
|
446
506
|
const optionsById = new Set(getCatalogOptions(catalogKind, options).map((option) => option.id));
|
|
447
507
|
if (!optionsById.has(id)) {
|
|
448
|
-
throw new OPFEditorError("unknown-catalog-id", `Unknown ${catalogKind} catalog
|
|
508
|
+
throw new OPFEditorError("unknown-catalog-id", `Unknown ${catalogKind} catalog reference: ${id}.`, {
|
|
449
509
|
catalogKind,
|
|
450
510
|
id
|
|
451
511
|
});
|
|
@@ -454,18 +514,29 @@ export function assertCatalogId(catalogKind, id, options = {}) {
|
|
|
454
514
|
return id;
|
|
455
515
|
}
|
|
456
516
|
|
|
517
|
+
/** Write the catalog reference `id` at `path` (an object reference keeps its overrides and takes the new `id`). */
|
|
457
518
|
export function setCatalogId(editor, path, catalogKind, id, meta = {}) {
|
|
458
519
|
assertEditorSession(editor);
|
|
459
520
|
const catalogId = assertCatalogId(catalogKind, id, {
|
|
460
521
|
presentation: meta.presentation ?? editor.presentation,
|
|
461
|
-
catalogs: meta.catalogs
|
|
462
|
-
catalogSources: meta.catalogSources
|
|
522
|
+
catalogs: mergeCatalogs(editor.catalogs, meta.catalogs)
|
|
463
523
|
});
|
|
464
524
|
const current = editor.get(path);
|
|
465
525
|
const value = catalogReferenceValue(current, catalogId);
|
|
466
|
-
const { catalogs
|
|
467
|
-
|
|
526
|
+
const { catalogs: _catalogs, presentation: _presentation, ...commitMeta } = meta;
|
|
527
|
+
// Only documented design references may initialize their optional design parent.
|
|
528
|
+
// Ordinary set/applyPatch retain RFC 6902's existing-parent requirement.
|
|
529
|
+
const segments = splitOpfPath(path);
|
|
530
|
+
const designField = { themes: "theme", colorSchemes: "colorScheme", fontSchemes: "fontScheme" }[catalogKind];
|
|
531
|
+
const deckDesign = segments.length === 2 && segments[0] === "design";
|
|
532
|
+
const slideDesign = segments.length === 4 && segments[0] === "slides" && /^(0|[1-9][0-9]*)$/.test(segments[1]) && segments[2] === "design" && editor.get(segments.slice(0, 2));
|
|
533
|
+
const parent = segments.slice(0, -1);
|
|
534
|
+
const patches = designField === segments.at(-1) && (deckDesign || slideDesign) && !hasValueAtPath(editor.presentation, parent)
|
|
535
|
+
? [{ op: "add", path: opfPathToJsonPointer(parent), value: { [designField]: value } }]
|
|
536
|
+
: createValuePatch(editor.presentation, path, value);
|
|
537
|
+
return editor.applyPatch(patches, {
|
|
468
538
|
...commitMeta,
|
|
539
|
+
rejectInvalid: true,
|
|
469
540
|
catalogKind,
|
|
470
541
|
catalogId,
|
|
471
542
|
source: commitMeta.source ?? "catalog-control"
|
|
@@ -517,7 +588,7 @@ export function createCatalogSelect(editor, options) {
|
|
|
517
588
|
if (options?.label) select.setAttribute("aria-label", options.label);
|
|
518
589
|
|
|
519
590
|
for (const option of getCatalogOptions(catalogKind, {
|
|
520
|
-
|
|
591
|
+
catalogs: mergeCatalogs(editor.catalogs, options?.catalogs),
|
|
521
592
|
presentation: options?.presentation ?? editor.presentation
|
|
522
593
|
})) {
|
|
523
594
|
const optionElement = documentRef.createElement("option");
|
|
@@ -530,14 +601,13 @@ export function createCatalogSelect(editor, options) {
|
|
|
530
601
|
const current = editor.get(path);
|
|
531
602
|
select.value = typeof current === "object" && current ? current.id : current ?? "";
|
|
532
603
|
};
|
|
533
|
-
const onChange = () => {
|
|
534
|
-
setCatalogId(editor, path, catalogKind, select.value, {
|
|
604
|
+
const onChange = () => commitCatalogControl(select, options?.onError, updateValue, () => {
|
|
605
|
+
return setCatalogId(editor, path, catalogKind, select.value, {
|
|
535
606
|
catalogs: options?.catalogs,
|
|
536
|
-
catalogSources: options?.catalogSources,
|
|
537
607
|
presentation: options?.presentation,
|
|
538
608
|
source: "catalog-select"
|
|
539
609
|
});
|
|
540
|
-
};
|
|
610
|
+
});
|
|
541
611
|
const unsubscribe = editor.subscribe(updateValue);
|
|
542
612
|
|
|
543
613
|
select.addEventListener?.("change", onChange);
|
|
@@ -677,20 +747,6 @@ function assertCatalogKind(catalogKind) {
|
|
|
677
747
|
}
|
|
678
748
|
}
|
|
679
749
|
|
|
680
|
-
function firstRecords(value) {
|
|
681
|
-
if (Array.isArray(value)) return value.filter(isCatalogRecord);
|
|
682
|
-
if (Array.isArray(value?.records)) return value.records.filter(isCatalogRecord);
|
|
683
|
-
return null;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function isCatalogRecord(record) {
|
|
687
|
-
return Boolean(record && typeof record === "object" && typeof record.id === "string");
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function catalogLabel(record) {
|
|
691
|
-
return String(record.name ?? record.title ?? record.label ?? record.id);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
750
|
function catalogReferenceValue(current, id) {
|
|
695
751
|
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
696
752
|
return { ...current, id };
|
package/dist/json-editor.js
CHANGED
|
@@ -103,7 +103,7 @@ function skipDelimiter(view, delimiter) {
|
|
|
103
103
|
}
|
|
104
104
|
function mountJsonCodeEditor(parent, options) {
|
|
105
105
|
const attributes = new Compartment(), indent = new Compartment();
|
|
106
|
-
let disposed=false, menu=null, catalogs=options.catalogs??
|
|
106
|
+
let disposed=false, menu=null, catalogs=options.catalogs??[], generation=0;
|
|
107
107
|
function closeOptions(){generation++;menu?.destroy();menu=null;}
|
|
108
108
|
async function openOptions(left,top){
|
|
109
109
|
closeOptions();
|
|
@@ -244,7 +244,7 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
244
244
|
return {
|
|
245
245
|
api,
|
|
246
246
|
openOptions,
|
|
247
|
-
setCatalogs(value){catalogs=value??
|
|
247
|
+
setCatalogs(value){catalogs=value??[];closeOptions();},
|
|
248
248
|
update(code, invalid=false, field=null, describedBy) {
|
|
249
249
|
if(code!==api.getValue())closeOptions();
|
|
250
250
|
const current = view.state.doc.toString(), normalized=textInputMap(code).text;
|
package/dist/json-menu.js
CHANGED
|
@@ -26,9 +26,9 @@ export function mountJsonFieldMenu(parent,api,catalogs,left,top,onClose){
|
|
|
26
26
|
const error=add('p',undefined,'json-field-empty');error.setAttribute('role','alert');error.hidden=true;
|
|
27
27
|
if(isLayout){
|
|
28
28
|
const help=add('details',undefined,'json-field-catalog-help');add('summary','Where do these layouts come from?',undefined,help);
|
|
29
|
-
add('p','
|
|
29
|
+
add('p','Provided by app: records from the catalogs the host app registered. In this document: records embedded in your JSON, which take precedence. Nothing is fetched; saving embeds every record the document uses.',undefined,help);
|
|
30
30
|
}
|
|
31
|
-
add('p',context.unloadedSource?'Using
|
|
31
|
+
add('p',context.unloadedSource?'Using the embedded records: a catalog this document names is not registered here.':'Choose an option or press Escape to keep editing JSON.','json-field-footnote');
|
|
32
32
|
let selected=Math.max(0,context.options.findIndex(option=>option.value===context.value)),choices=[],disposed=false;
|
|
33
33
|
function close(restore=false){if(disposed)return;disposed=true;observer.disconnect();root.remove();doc.removeEventListener('pointerdown',outside);doc.removeEventListener('wheel',outside,true);doc.removeEventListener('touchmove',outside,true);win.removeEventListener('resize',resize);onClose();if(restore){api.focus();api.setSelection(...selection);}}
|
|
34
34
|
function outside(event){if(!root.contains(event.target))close();}
|
package/dist/json-options.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export type JsonCatalogContext =
|
|
1
|
+
/** The host's catalogs (core `Catalog[]`): records offered after the document's embedded ones. */
|
|
2
|
+
export type JsonCatalogContext = readonly import("@openpresentation/opf").Catalog[];
|
|
3
3
|
export type JsonOptionValue = string | number | boolean;
|
|
4
4
|
export interface JsonFieldOption {
|
|
5
5
|
value: JsonOptionValue;
|
|
6
6
|
label: string;
|
|
7
7
|
description?: string;
|
|
8
|
-
source: '
|
|
8
|
+
source: 'Loaded catalog' | 'Document catalog' | 'Schema' | 'Current value';
|
|
9
9
|
suggested?: boolean;
|
|
10
10
|
/** User-facing provenance; source remains stable for existing consumers. */
|
|
11
11
|
sourceLabel?: string;
|
package/dist/json-options.js
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
import { catalogKinds, catalogSchemaNames, schemas, validateCatalogRecord } from "@openpresentation/opf";
|
|
2
1
|
import { checkFormat, firstErrorMessage } from "./checks.js";
|
|
3
|
-
import {
|
|
2
|
+
import { listCatalogRecords } from "./catalogs.js";
|
|
4
3
|
import { schemaAtPath, schemaVariants } from "./schema.js";
|
|
5
4
|
import { applyEdits, findNodeAtLocation, findNodeAtOffset, getNodePath, modify, parseTree } from "jsonc-parser";
|
|
6
|
-
import { populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import { layoutSlotSummary, layoutSlotTypes, populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
6
|
+
// The catalog kind a field references (OPF 0.15: every content kind is `id` or `name:id`), read from where the field sits.
|
|
7
|
+
const ROOT_REFERENCES = { narrative: "narratives", audience: "audiences", purpose: "purposes", tone: "tones" };
|
|
8
|
+
const DESIGN_REFERENCES = { theme: "themes", colorScheme: "colorSchemes", fontScheme: "fontSchemes" };
|
|
9
|
+
function catalogAtPath(path) {
|
|
10
|
+
const parts = path.map(String);
|
|
11
|
+
// A reference's object form names its record with `id`; an audience array's entries are references too.
|
|
12
|
+
const field = parts.at(-1) === "id" ? parts.slice(0, -1) : parts;
|
|
13
|
+
const tail = /^\d+$/.test(field.at(-1) ?? "") && field.at(-2) === "audience" ? field.slice(0, -1) : field;
|
|
14
|
+
const key = tail.at(-1);
|
|
15
|
+
if (tail.length === 3 && tail[0] === "slides" && key === "layout") return "layouts";
|
|
16
|
+
if (tail.length === 1 && ROOT_REFERENCES[key]) return ROOT_REFERENCES[key];
|
|
17
|
+
if (DESIGN_REFERENCES[key] && ((tail.length === 2 && tail[0] === "design") || (tail.length === 4 && tail[0] === "slides" && tail[2] === "design"))) return DESIGN_REFERENCES[key];
|
|
18
|
+
// Inside an embedded record: a theme's colour or font scheme.
|
|
19
|
+
if (tail.length === 5 && tail[0] === "catalogs" && tail[2] === "themes" && (key === "colorScheme" || key === "fontScheme")) return DESIGN_REFERENCES[key];
|
|
20
|
+
return undefined;
|
|
10
21
|
}
|
|
11
22
|
// Compare declared placeholders, including multiplicity. This describes a layout
|
|
12
23
|
// contract, not a claim that arbitrary slide content will render without overflow.
|
|
24
|
+
// FA-26: a record's placeholder groups compare by their leaf regions, the slots its content fills.
|
|
13
25
|
function layoutTypes(record) {
|
|
14
|
-
|
|
15
|
-
return undefined;
|
|
16
|
-
return record.placeholders.map(p => p.type).sort();
|
|
26
|
+
return layoutSlotTypes(record)?.sort();
|
|
17
27
|
}
|
|
18
28
|
function placeholderSummary(types) {
|
|
19
29
|
if (!types)
|
|
@@ -28,9 +38,8 @@ function placeholderSummary(types) {
|
|
|
28
38
|
}).join(" + ");
|
|
29
39
|
}
|
|
30
40
|
const sourceDetails = {
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"Document catalog": ["In this document", "Defined in this document’s catalogs. Takes precedence over app and standard records with the same ID."],
|
|
41
|
+
"Loaded catalog": ["Provided by app", "Records from a catalog this app registered; nothing is fetched from a URL. Saving embeds the record in the document."],
|
|
42
|
+
"Document catalog": ["In this document", "Embedded in this document’s catalogs. Takes precedence over the app’s records with the same reference."],
|
|
34
43
|
"Current value": ["Custom value", "The current value is not in the available catalogs."],
|
|
35
44
|
"Schema": ["OPF option", "Allowed by the OPF schema."],
|
|
36
45
|
};
|
|
@@ -47,8 +56,10 @@ function describeOptions(choices, records, current, catalog) {
|
|
|
47
56
|
if (catalog !== "layouts")
|
|
48
57
|
continue;
|
|
49
58
|
const types = layoutTypes(option.value === current ? currentRecord : records.get(String(option.value)));
|
|
50
|
-
|
|
51
|
-
option.
|
|
59
|
+
const record = records.get(String(option.value));
|
|
60
|
+
option.placeholderTypes = layoutSlotTypes(record);
|
|
61
|
+
// A record with placeholder groups also shows how its slots nest.
|
|
62
|
+
option.placeholders = placeholderSummary(types) + (record?.placeholders?.some?.(entry => entry?.type === "group") ? ` · ${layoutSlotSummary(record)}` : "");
|
|
52
63
|
const same = types && currentTypes && signature(types) === signature(currentTypes);
|
|
53
64
|
const equivalent = types && currentTypes && signature(compatible(types)) === signature(compatible(currentTypes));
|
|
54
65
|
const sameKinds = types && currentTypes && signature([...new Set(compatible(types))]) === signature([...new Set(compatible(currentTypes))]);
|
|
@@ -64,7 +75,7 @@ function describeOptions(choices, records, current, catalog) {
|
|
|
64
75
|
}
|
|
65
76
|
// The installed schema identifies fields; a document field merely named "layout"
|
|
66
77
|
// inside arbitrary metadata is not mistaken for an OPF layout reference.
|
|
67
|
-
export function getJsonFieldContext(source, position, loadedCatalogs =
|
|
78
|
+
export function getJsonFieldContext(source, position, loadedCatalogs = []) {
|
|
68
79
|
let presentation;
|
|
69
80
|
try {
|
|
70
81
|
presentation = JSON.parse(source);
|
|
@@ -87,42 +98,21 @@ export function getJsonFieldContext(source, position, loadedCatalogs = {}) {
|
|
|
87
98
|
return null;
|
|
88
99
|
const schema = schemaAtPath(presentation, path.map(String));
|
|
89
100
|
const variants = schemaVariants(schema);
|
|
90
|
-
const
|
|
91
|
-
let catalog = catalogFor(description);
|
|
92
|
-
// Audience arrays inherit the catalog context from the array property.
|
|
93
|
-
if (!catalog && typeof path.at(-1) === "number")
|
|
94
|
-
catalog = catalogFor(String(schemaAtPath(presentation, path.slice(0, -1).map(String)).description ?? ""));
|
|
101
|
+
const catalog = catalogAtPath(path);
|
|
95
102
|
const options = new Map();
|
|
96
103
|
const records = new Map();
|
|
97
|
-
|
|
104
|
+
// A document group whose source no registered catalog matches: its records beyond the embedded ones are not available here.
|
|
105
|
+
const registered = new Set((Array.isArray(loadedCatalogs) ? loadedCatalogs : []).map(entry => entry?.source));
|
|
106
|
+
const unloadedSource = Boolean(catalog) && Object.entries(presentation.catalogs ?? {}).some(([name, group]) => name !== "custom" && group && typeof group === "object" && typeof group.source === "string" && !registered.has(group.source));
|
|
98
107
|
if (catalog && typeof node.value === "string") {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
const hostCatalogs = Array.isArray(loadedCatalogs) ? loadedCatalogs : [];
|
|
109
|
+
// A draft whose own catalogs are malformed still offers the host's records; a malformed host list offers nothing.
|
|
110
|
+
const attempt = (document) => { try { return listCatalogRecords(document, catalog, { catalogs: hostCatalogs }); } catch { return undefined; } };
|
|
111
|
+
const available = attempt(presentation) ?? attempt({ ...presentation, catalogs: undefined }) ?? [];
|
|
112
|
+
for (const option of available) {
|
|
103
113
|
records.set(option.id, option.record);
|
|
104
|
-
options.set(option.id, { value: option.id, label: option.label, description:
|
|
105
|
-
source: "
|
|
106
|
-
}
|
|
107
|
-
for (const { value, source } of [...(Array.isArray(loadedCatalogs?.[catalog]) ? loadedCatalogs[catalog] : []).map(value => ({ value, source: "Loaded catalog" })),
|
|
108
|
-
...(Array.isArray(local?.records) ? local.records : []).map(value => ({ value, source: "Document catalog" }))]) {
|
|
109
|
-
if (!value || typeof value !== "object")
|
|
110
|
-
continue;
|
|
111
|
-
const record = value;
|
|
112
|
-
if (typeof record.id !== "string")
|
|
113
|
-
continue;
|
|
114
|
-
// Inline records may omit the standalone document's $schema marker.
|
|
115
|
-
// Supply it only for validation; never change the authored record.
|
|
116
|
-
const schemaId = schemas[catalogSchemaNames[catalog]].$id;
|
|
117
|
-
if (!validateCatalogRecord(catalog, { $schema: schemaId, ...record }).valid) {
|
|
118
|
-
options.delete(record.id);
|
|
119
|
-
records.delete(record.id);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
records.set(record.id, record);
|
|
123
|
-
options.set(record.id, { value: record.id, label: typeof record.name === "string" ? record.name : record.id,
|
|
124
|
-
description: typeof record.description === "string" ? record.description : undefined,
|
|
125
|
-
source });
|
|
114
|
+
options.set(option.id, { value: option.id, label: option.label, description: option.description,
|
|
115
|
+
source: option.origin === "document" ? "Document catalog" : "Loaded catalog" });
|
|
126
116
|
}
|
|
127
117
|
}
|
|
128
118
|
else {
|
package/dist/layout-handles.js
CHANGED
|
@@ -60,6 +60,19 @@ export function createLayoutHandles(root, options) {
|
|
|
60
60
|
lastGeometry=geometry;
|
|
61
61
|
layer.replaceChildren();
|
|
62
62
|
if(!enabled||options.isTextEditing())return;
|
|
63
|
+
// FA-26: the slots of a layout record with placeholder groups, drawn under the handles while arranging: each group and
|
|
64
|
+
// region as a dashed outline at its cell, deeper groups inset, an empty region labelled with its kind. They are the
|
|
65
|
+
// record's, not the document's, so they are not editable here; the content in a slot keeps its own selection.
|
|
66
|
+
for(const slot of geometry.slots??[]){
|
|
67
|
+
const node=doc.createElement('div');
|
|
68
|
+
node.dataset.layoutSlot=slot.path;node.dataset.layoutSlotType=slot.type;node.setAttribute('aria-hidden','true');
|
|
69
|
+
const group=slot.type==='group',empty=!group&&!slot.content;
|
|
70
|
+
node.title=group?`Layout group (${slot.path})`:`${empty?'Empty ':''}${slot.type} slot (${slot.path})`;
|
|
71
|
+
const inset=group?Math.min(4,slot.depth*2):0;
|
|
72
|
+
node.style.cssText=`position:absolute;pointer-events:none;box-sizing:border-box;left:calc(${slot.box.x/geometry.width*100}% + ${inset}px);top:calc(${slot.box.y/geometry.height*100}% + ${inset}px);width:calc(${slot.box.width/geometry.width*100}% - ${2*inset}px);height:calc(${slot.box.height/geometry.height*100}% - ${2*inset}px);border:1px dashed ${group?'#8170d5':'#8a78d7aa'};border-radius:4px;${empty?'background:#8a78d714;':''}`;
|
|
73
|
+
if(empty){const label=doc.createElement('span');label.textContent=`Empty ${slot.type} slot`;label.style.cssText='position:absolute;left:6px;top:4px;font:11px system-ui;color:#675394';node.append(label);}
|
|
74
|
+
layer.append(node);
|
|
75
|
+
}
|
|
63
76
|
for(const flow of geometry.flows??[]){
|
|
64
77
|
const {axis,size,tracks}=trackData(flow);
|
|
65
78
|
if(tracks.length<2||tracks.length>12||flow.itemCount!==flow.slotCount)continue;
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
+
import {layoutLeaves,layoutStructure} from '@openpresentation/opf/composition';
|
|
1
2
|
import {listBlockContainers,prepareBlockInsert} from './blocks.js';
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The slot kinds a layout record declares, in reading order: its headings and the leaf regions of its placeholder groups
|
|
6
|
+
* (FA-26), the kinds a slide's flat content fills. Undefined when the record states no placeholders.
|
|
7
|
+
*/
|
|
8
|
+
export function layoutSlotTypes(record){
|
|
9
|
+
if(!Array.isArray(record?.placeholders))return undefined;
|
|
10
|
+
try{return layoutLeaves(record).map(leaf=>leaf.type);}catch{return record.placeholders.filter(entry=>entry?.type!=='group').map(entry=>entry?.type);}
|
|
11
|
+
}
|
|
12
|
+
/** A one-line description of a record's slots, nested groups in parentheses: `title, column (text, text), chart`. */
|
|
13
|
+
export function layoutSlotSummary(record){
|
|
14
|
+
if(!Array.isArray(record?.placeholders))return '';
|
|
15
|
+
try{return layoutStructure(record);}catch{return layoutSlotTypes(record).join(', ');}
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
const payloadFields=['text','items','bullets','image','video','chart','table','code','metric','quote','timeline'];
|
|
4
19
|
const headingFields=new Set(['title','subtitle','tag']);
|
|
5
20
|
const fields={text:'text',list:'items',image:'image',video:'video',chart:'chart',table:'table',code:'code',metric:'metric',quote:'quote',timeline:'timeline'};
|
package/dist/react.d.ts
CHANGED
package/dist/react.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commitCatalogControl } from "./catalog-control.js";
|
|
1
2
|
import {
|
|
2
3
|
getCatalogOptions,
|
|
3
4
|
getValueAtPath,
|
|
@@ -56,13 +57,14 @@ export function createOPFReactComponents(React) {
|
|
|
56
57
|
catalogKind,
|
|
57
58
|
label,
|
|
58
59
|
catalogOptions,
|
|
60
|
+
onError,
|
|
59
61
|
selectProps = {},
|
|
60
62
|
...props
|
|
61
63
|
}) {
|
|
62
64
|
const snapshot = useEditorSnapshot(editor);
|
|
63
65
|
const value = getValueAtPath(snapshot.presentation, path, "");
|
|
64
66
|
const currentId = value && typeof value === "object" ? value.id : value;
|
|
65
|
-
const options = catalogOptions ?? getCatalogOptions(catalogKind, { presentation: snapshot.presentation });
|
|
67
|
+
const options = catalogOptions ?? getCatalogOptions(catalogKind, { presentation: snapshot.presentation, catalogs: editor.catalogs });
|
|
66
68
|
|
|
67
69
|
return React.createElement(
|
|
68
70
|
"select",
|
|
@@ -78,7 +80,9 @@ export function createOPFReactComponents(React) {
|
|
|
78
80
|
selectProps.onChange?.(event);
|
|
79
81
|
props.onChange?.(event);
|
|
80
82
|
if (!event.defaultPrevented) {
|
|
81
|
-
|
|
83
|
+
const node = event.currentTarget;
|
|
84
|
+
commitCatalogControl(node, onError, () => { node.value = currentId ?? ""; }, () =>
|
|
85
|
+
setCatalogId(editor, path, catalogKind, node.value, { source: "react-catalog-select" }));
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
},
|