@openpresentation/opf-editor 0.12.1 → 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.
Files changed (103) hide show
  1. package/README.md +122 -80
  2. package/dist/annotations.d.ts +16 -16
  3. package/dist/annotations.js +71 -79
  4. package/dist/assets.d.ts +7 -7
  5. package/dist/assets.js +16 -16
  6. package/dist/background-options.d.ts +12 -5
  7. package/dist/background-options.js +44 -19
  8. package/dist/block-controls.js +7 -7
  9. package/dist/block-convert.d.ts +7 -7
  10. package/dist/block-convert.js +22 -21
  11. package/dist/blocks.d.ts +8 -8
  12. package/dist/blocks.js +41 -40
  13. package/dist/canvas-fields.js +35 -12
  14. package/dist/canvas.d.ts +30 -31
  15. package/dist/canvas.js +64 -68
  16. package/dist/catalogs.d.ts +48 -0
  17. package/dist/catalogs.js +217 -0
  18. package/dist/chart-data.d.ts +10 -10
  19. package/dist/chart-data.js +21 -21
  20. package/dist/chart-options-panel.d.ts +2 -2
  21. package/dist/chart-options-panel.js +82 -8
  22. package/dist/chart-options.d.ts +35 -9
  23. package/dist/chart-options.js +133 -40
  24. package/dist/checks.js +23 -0
  25. package/dist/color-authoring.js +1 -1
  26. package/dist/content-actions.d.ts +17 -17
  27. package/dist/content-actions.js +74 -73
  28. package/dist/content-controls.js +21 -21
  29. package/dist/data-grid.js +7 -8
  30. package/dist/data.d.ts +4 -4
  31. package/dist/data.js +5 -5
  32. package/dist/dataset-refs.d.ts +2 -2
  33. package/dist/dataset-refs.js +5 -5
  34. package/dist/design-controls.d.ts +12 -5
  35. package/dist/design-controls.js +312 -111
  36. package/dist/design-options.d.ts +20 -18
  37. package/dist/design-options.js +91 -99
  38. package/dist/edit-helpers.js +8 -7
  39. package/dist/exact-source.js +5 -5
  40. package/dist/export.d.ts +7 -7
  41. package/dist/export.js +48 -17
  42. package/dist/find-panel.js +5 -5
  43. package/dist/find-replace.d.ts +3 -3
  44. package/dist/find-replace.js +23 -20
  45. package/dist/font-gate.js +49 -58
  46. package/dist/galleries.js +50 -101
  47. package/dist/grid-model.d.ts +25 -27
  48. package/dist/grid-model.js +92 -102
  49. package/dist/image-crop.d.ts +4 -4
  50. package/dist/image-crop.js +28 -29
  51. package/dist/image-cropper.js +3 -3
  52. package/dist/image-options.d.ts +45 -0
  53. package/dist/image-options.js +195 -0
  54. package/dist/index.d.ts +48 -31
  55. package/dist/index.js +154 -126
  56. package/dist/json-editor.js +2 -2
  57. package/dist/json-menu.js +2 -2
  58. package/dist/json-options.d.ts +3 -3
  59. package/dist/json-options.js +44 -54
  60. package/dist/layout-handles.js +5 -5
  61. package/dist/layout-placeholders.js +4 -4
  62. package/dist/layout.d.ts +2 -2
  63. package/dist/layout.js +8 -7
  64. package/dist/numbering-panel.js +3 -5
  65. package/dist/numbering.d.ts +3 -5
  66. package/dist/numbering.js +14 -26
  67. package/dist/outline-view.js +24 -24
  68. package/dist/outline.d.ts +12 -12
  69. package/dist/outline.js +52 -50
  70. package/dist/persistence.d.ts +1 -1
  71. package/dist/persistence.js +19 -18
  72. package/dist/presentation-ids.js +2 -2
  73. package/dist/react.js +3 -3
  74. package/dist/review-panel.d.ts +30 -13
  75. package/dist/review-panel.js +160 -53
  76. package/dist/review.d.ts +36 -43
  77. package/dist/review.js +92 -53
  78. package/dist/rich-text-input.js +4 -3
  79. package/dist/rich-text-toolbar.js +5 -3
  80. package/dist/rich-text.js +5 -2
  81. package/dist/schema-inspector.d.ts +2 -2
  82. package/dist/schema-inspector.js +16 -11
  83. package/dist/schema.d.ts +1 -1
  84. package/dist/schema.js +26 -6
  85. package/dist/slide-manager.d.ts +2 -2
  86. package/dist/slide-manager.js +19 -19
  87. package/dist/slides.d.ts +21 -21
  88. package/dist/slides.js +104 -101
  89. package/dist/svelte.js +2 -3
  90. package/dist/switches.d.ts +37 -22
  91. package/dist/switches.js +282 -171
  92. package/dist/table-options.d.ts +8 -8
  93. package/dist/table-options.js +28 -27
  94. package/dist/table-structure.d.ts +10 -10
  95. package/dist/table-structure.js +21 -21
  96. package/dist/template-panel.d.ts +3 -3
  97. package/dist/template-panel.js +37 -13
  98. package/dist/templates.d.ts +22 -11
  99. package/dist/templates.js +48 -55
  100. package/dist/transfer.d.ts +15 -4
  101. package/dist/transfer.js +107 -161
  102. package/package.json +12 -6
  103. package/dist/font-defaults.js +0 -25
package/dist/canvas.d.ts CHANGED
@@ -1,55 +1,54 @@
1
1
  import type { EditorSession } from "./index.js";
2
2
  import type { RenderSvgOptions } from "@openpresentation/opf-render";
3
+ import type { BrowserFontsHandle } from "@openpresentation/opf-render/fonts-browser";
3
4
  import type { SlideComposition } from "@openpresentation/opf/composition";
4
- /** Faces a document needs (script packages and vendored files) that are not loaded yet, and the way to load them. */
5
- export interface FontGate {
6
- /** Synchronous. Empty means the document can render now. Never throws. `renderOptions` are the options the host renders with (`catalogs`, ...); the canvas passes its current ones. */
7
- pending(document: unknown, renderOptions?: RenderSvgOptions): string[];
8
- /** Load every pending face (vendored faces first, then script faces). Rejects with a `fonts-unavailable` error; never retries by itself. */
9
- ensure(document: unknown, options?: { signal?: AbortSignal; renderOptions?: RenderSvgOptions }): Promise<void>;
10
- /** `whenFontsReady` on this gate. */
11
- run(document: unknown, handlers?: FontGateHandlers): Promise<void>;
12
- }
13
- export interface FontGateHandlers {
5
+ import type { Catalog } from "@openpresentation/opf";
6
+ /**
7
+ * The fonts the canvas, the export and `whenFontsReady` take: the renderer's handle from `loadFonts()` (`@openpresentation/opf-render/fonts-browser`).
8
+ * Only `textMeasurement` is required to measure; `pending` and `ensure` make the canvas load script and vendored faces before it draws.
9
+ */
10
+ export type EditorFonts = Pick<BrowserFontsHandle, "textMeasurement"> & Partial<Pick<BrowserFontsHandle, "embeddedFonts" | "pending" | "ensure" | "registry">>;
11
+ export interface FontsReadyHandlers {
14
12
  /** Return false once the document was superseded; the late result is then dropped. */
15
13
  isCurrent?: () => boolean;
16
14
  loading?: (pending: string[]) => void;
17
15
  ready?: () => void;
18
16
  failed?: (error: Error) => void;
19
- /** The options the host renders with (`catalogs`, ...), passed to the gate. */
17
+ /** The options the host renders with (`catalogs`, ...), passed to the fonts handle. */
20
18
  renderOptions?: RenderSvgOptions;
21
19
  }
22
20
  export declare const FONTS_PENDING: "fonts-pending";
23
21
  export declare const FONTS_UNAVAILABLE: "fonts-unavailable";
24
- /** Wrap a browser font registry (`loadBrowserFontRegistry`). Registries without the lazy loaders gate nothing. */
25
22
  /**
26
- * `options.renderOptions` (an object, or a function called on every use) are the default render options passed to the registry
27
- * (`catalogs`, ...), under a call's own; the canvas passes its current `renderOptions` itself, so a gate needs none.
28
- */
29
- export declare function createFontGate(registry: object, options?: { renderOptions?: RenderSvgOptions | (() => RenderSvgOptions | undefined) }): FontGate;
30
- /**
31
- * Ensure fonts for a document, then run `ready` (synchronously when nothing is pending). The one
32
- * "ensure fonts, then render" helper: load failures and exceptions from `ready` go to `failed`.
23
+ * Ensure fonts for a presentation, then run `ready` (synchronously when nothing is pending). The one
24
+ * "ensure fonts, then render" helper: load failures and exceptions from `ready` go to `failed`. `fonts` is the renderer's fonts handle.
33
25
  */
34
26
  export declare function whenFontsReady(
35
- gate: Pick<FontGate, "pending" | "ensure"> | undefined,
36
- document: unknown,
37
- handlers?: FontGateHandlers,
27
+ fonts: Pick<EditorFonts, "pending" | "ensure"> | undefined,
28
+ presentation: unknown,
29
+ handlers?: FontsReadyHandlers,
38
30
  ): Promise<void>;
39
31
  export interface CanvasEditorOptions {
40
32
  editor?: EditorSession;
41
- document?: unknown;
33
+ presentation?: unknown;
34
+ /**
35
+ * The host's catalogs (core `Catalog[]`, for example `[defaultCatalog]` from `@openpresentation/opf/catalog`) for the session the
36
+ * canvas creates from `presentation`. With `editor`, register them on that session instead (`createEditorSession(doc, { catalogs })`);
37
+ * `renderOptions.catalogs` adds catalogs for drawing only, merged after the session's.
38
+ */
39
+ catalogs?: readonly Catalog[];
42
40
  slideIndex?: number;
43
41
  /** Show keyboard-accessible dividers for resizing composition tracks. */
44
42
  layoutEditing?: boolean;
45
43
  /** Render options. The canvas draws the document as authored (`variables: false`: a template's `{{tokens}}` stay visible, so inline edits never overwrite them); pass `variables` to draw resolved values instead. */
46
44
  renderOptions?: RenderSvgOptions;
47
45
  /**
48
- * A font gate (`createFontGate(registry)`). With one, the canvas never renders a document whose faces are still
49
- * loading: it shows "Loading fonts…", loads them and then renders, on every path (editor changes, undo/redo,
50
- * imports, dimension switches, slide changes, drafts). Without one every document renders at once.
46
+ * The renderer's fonts handle (`loadFonts()` from `@openpresentation/opf-render/fonts-browser`). Its `textMeasurement` lays the slide out and
47
+ * its faces draw it. With its `pending` and `ensure` the canvas never renders a document whose faces are still loading: it shows
48
+ * "Loading fonts…", loads them and then renders, on every path (editor changes, undo/redo, imports, dimension switches, slide
49
+ * changes, drafts). Without a handle every document renders at once, laid out with core's portable text estimate.
51
50
  */
52
- fonts?: Pick<FontGate, "pending" | "ensure">;
51
+ fonts?: EditorFonts;
53
52
  /**
54
53
  * How a pointer enters text editing. "click" (default): a single press on editable text starts editing with the caret at the
55
54
  * pressed character, and a press-drag selects a range. "dblclick": a click selects, a double-click enters with the caret at the
@@ -68,7 +67,7 @@ export interface CanvasEditorOptions {
68
67
  editor: EditorSession;
69
68
  }) => void;
70
69
  onDraft?: (draft: {
71
- document: unknown;
70
+ presentation: unknown;
72
71
  path: string;
73
72
  value: unknown;
74
73
  }) => void;
@@ -78,7 +77,7 @@ export interface CanvasEditorOptions {
78
77
  /** Font loading progress of the canvas: `loading`, `error` (a face could not be loaded; the canvas offers a retry) and `ready`. */
79
78
  onFonts?: (event: { state: "loading"; pending: string[] } | { state: "error"; error: Error } | { state: "ready" }) => void;
80
79
  onRender?: (event: {
81
- document: unknown;
80
+ presentation: unknown;
82
81
  slideIndex: number;
83
82
  svg: SVGSVGElement;
84
83
  geometry: SlideComposition;
@@ -94,7 +93,7 @@ export interface CanvasEditor {
94
93
  /** True while the crop layer is open. */
95
94
  readonly cropping: boolean;
96
95
  /**
97
- * Open the crop layer for the picture at `path` (an image block, a slide's `image`, or `slides.N.design.slideImage`); `tool: "focus"`
96
+ * Open the crop layer for the picture at `path` (an image block or a slide's `image`); `tool: "focus"`
98
97
  * starts with the focal point. Apply writes one undoable change (see `@openpresentation/opf-editor/image-crop`). Resolves to whether it opened.
99
98
  */
100
99
  cropImage(path: string, options?: { tool?: "crop" | "focus" }): Promise<boolean>;
@@ -110,7 +109,7 @@ export interface CanvasEditor {
110
109
  editProperties(path: string): boolean;
111
110
  commit(): boolean;
112
111
  cancel(): void;
113
- render(document?: unknown): void;
112
+ render(presentation?: unknown): void;
114
113
  setSlide(index: number): boolean;
115
114
  /**
116
115
  * Select the content at `path`, or the closest enclosing content the canvas can select (a list item selects its list),
package/dist/canvas.js CHANGED
@@ -1,17 +1,19 @@
1
1
  import {
2
2
  createEditorSession,
3
+ mergeCatalogs,
3
4
  opfPathToJsonPointer,
4
5
  applyJsonPatch,
5
- validateOpfDocument,
6
6
  getValueAtPath,
7
7
  splitOpfPath,
8
8
  } from "./index.js";
9
9
  import {
10
- renderSvg,
10
+ renderSlideSvg,
11
11
  resolvePresentation,
12
12
  } from "@openpresentation/opf-render/svg";
13
13
  import {
14
14
  getEditableFields,
15
+ emptyLike,
16
+ fieldChoices,
15
17
  parseCanvasValue,
16
18
  createCanvasDraft,
17
19
  } from "./canvas-fields.js";
@@ -21,9 +23,10 @@ import { createRichTextInput } from "./rich-text-input.js";
21
23
  import { textInputOffsetAtPoint } from "./text-pointer.js";
22
24
  import { createRichTextToolbar } from "./rich-text-toolbar.js";
23
25
  import { createImageCropper } from "./image-cropper.js";
24
- import { FONTS_PENDING, fontsPendingError, whenFontsReady } from "./font-gate.js";
26
+ import { FONTS_PENDING, fontGate, fontsPendingError, previewFonts, whenFontsReady } from "./font-gate.js";
27
+ import { checkFormat, firstErrorMessage } from "./checks.js";
25
28
  export { getEditableFields } from "./canvas-fields.js";
26
- export { createFontGate, whenFontsReady, FONTS_PENDING, FONTS_UNAVAILABLE } from "./font-gate.js";
29
+ export { whenFontsReady, FONTS_PENDING, FONTS_UNAVAILABLE } from "./font-gate.js";
27
30
 
28
31
  /** Allocated placeholder or internal-part bounds for the selection outline, not glyph ink. */
29
32
  export function allocatedSelectionBox(node, item) {
@@ -65,10 +68,14 @@ export function createCanvasEditor(container, options = {}) {
65
68
  win = doc.defaultView;
66
69
  const editor =
67
70
  options.editor ??
68
- createEditorSession(options.document, { rejectInvalid: true });
69
- // `options.fonts` is a font gate (createFontGate): a document whose faces are not loaded yet is never rendered. The canvas
70
- // loads them first and shows "Loading fonts…" meanwhile; without a gate every document renders at once, as before.
71
- const fonts = options.fonts;
71
+ createEditorSession(options.presentation, { rejectInvalid: true, catalogs: options.catalogs });
72
+ // `options.fonts` is the renderer's fonts handle (`loadFonts()`): it measures the text and draws the faces. A document whose faces
73
+ // are not loaded yet is never rendered: the canvas has the handle load them first and shows "Loading fonts…" meanwhile. Without a
74
+ // handle every document renders at once, with core's portable text estimate.
75
+ const handle = options.fonts, fonts = fontGate(handle);
76
+ // The renderer draws with the session's registered catalogs (plus any `renderOptions.catalogs`): the same list core resolves with.
77
+ const resolveOptions = () => ({ ...renderOptions, catalogs: mergeCatalogs(editor.catalogs, renderOptions.catalogs) });
78
+ const drawOptions = (extra) => ({ ...resolveOptions(), fonts: previewFonts(handle), ...extra });
72
79
  let slideIndex = options.slideIndex ?? 0,
73
80
  // RR-32: the canvas edits the document as authored, so a template's {{tokens}} stay visible and an inline edit never
74
81
  // overwrites one with its resolved text. The Fill template panel previews the resolved deck. Pass `variables` to override.
@@ -120,7 +127,7 @@ export function createCanvasEditor(container, options = {}) {
120
127
  onTyping: beginEdit,
121
128
  onProperties(path) { if (commit()) openProperties(path, editor.get(path)); },
122
129
  validateChange(path, value) {
123
- validateRender(createCanvasDraft(editor.document, path, value));
130
+ validateRender(createCanvasDraft(editor.presentation, path, value));
124
131
  },
125
132
  onChange(path) { clearNotice(); options.onCommit?.({path, editor}); }
126
133
  });
@@ -144,7 +151,7 @@ export function createCanvasEditor(container, options = {}) {
144
151
  const blockControls = createBlockControls(root, {
145
152
  editor, render: renderFor, beforeEdit: () => {if(!commit())return false;richToolbar.hide();return true;}, enabled: () => layoutHandles.enabled,
146
153
  isEditing: () => !!active || !!layoutHandles.editingPath, slideIndex: () => slideIndex,
147
- validate: document => validateRender(document),
154
+ validate: presentation => validateRender(presentation),
148
155
  clearError: clearNotice, onError: report,
149
156
  onMove: path => choose(splitOpfPath(path).join(".")),
150
157
  onCommit: value => options.onCommit?.(value),
@@ -175,20 +182,20 @@ export function createCanvasEditor(container, options = {}) {
175
182
  const pendingMessage = "Loading fonts for this document…";
176
183
  // A synchronous render or validation of a document whose faces are still loading fails with a clear "fonts-pending"
177
184
  // error and starts the load, so the next attempt succeeds. It never draws glyphs the registry cannot provide.
178
- function requireFonts(document) {
179
- const pending = fonts?.pending(document, renderOptions) ?? [];
185
+ function requireFonts(presentation) {
186
+ const pending = fonts?.pending(presentation, resolveOptions()) ?? [];
180
187
  if (!pending.length) return;
181
188
  notice.hidden = false;
182
189
  notice.textContent = pendingMessage;
183
- fonts.ensure(document, { renderOptions }).then(
190
+ fonts.ensure(presentation, { renderOptions: resolveOptions() }).then(
184
191
  () => { if (!disposed && notice.textContent === pendingMessage) clearNotice(); },
185
192
  (error) => { if (!disposed) report(error); },
186
193
  );
187
194
  throw fontsPendingError(pending);
188
195
  }
189
- function validateRender(document) {
190
- requireFonts(document);
191
- return renderSvg(document, {...renderOptions, slideIndex});
196
+ function validateRender(presentation) {
197
+ requireFonts(presentation);
198
+ return renderSlideSvg(presentation, slideIndex, drawOptions());
192
199
  }
193
200
  function fontsState(kind, detail) {
194
201
  fontsShown = true;
@@ -218,8 +225,8 @@ export function createCanvasEditor(container, options = {}) {
218
225
  function show(target) {
219
226
  if (disposed) return;
220
227
  const token = ++showToken;
221
- return whenFontsReady(fonts, target ?? editor.document, {
222
- renderOptions,
228
+ return whenFontsReady(fonts, target ?? editor.presentation, {
229
+ renderOptions: resolveOptions(),
223
230
  isCurrent: () => !disposed && token === showToken,
224
231
  loading: (pending) => fontsState("loading", pending),
225
232
  ready: () => {
@@ -234,27 +241,23 @@ export function createCanvasEditor(container, options = {}) {
234
241
  });
235
242
  }
236
243
  // Renders now when the document's fonts are loaded (errors throw to the caller), otherwise after loading them.
237
- function renderFor(document) {
238
- if (fonts?.pending(document ?? editor.document, renderOptions).length) show(document);
239
- else render(document);
244
+ function renderFor(presentation) {
245
+ if (fonts?.pending(presentation ?? editor.presentation, resolveOptions()).length) show(presentation);
246
+ else render(presentation);
240
247
  }
241
- function render(document = editor.document) {
248
+ function render(presentation = editor.presentation) {
242
249
  if (disposed) return;
243
- requireFonts(document);
250
+ requireFonts(presentation);
244
251
  showToken++;
245
- const slides = document.slides ?? [];
252
+ const slides = presentation.slides ?? [];
246
253
  slideIndex = Math.max(0, Math.min(slideIndex, slides.length - 1));
247
- const svgText = renderSvg(document, {
248
- ...renderOptions,
249
- slideIndex,
250
- trace: true,
251
- });
254
+ const svgText = renderSlideSvg(presentation, slideIndex, drawOptions({ trace: true }));
252
255
  preview.innerHTML = svgText;
253
256
  const svg = preview.querySelector("svg");
254
257
  svg.setAttribute("role", "group");
255
258
  svg.setAttribute("aria-label", "Editable slide");
256
259
  svg.removeAttribute("aria-labelledby");
257
- const geometry = resolvePresentation(document, renderOptions).slides[
260
+ const geometry = resolvePresentation(presentation, drawOptions()).slides[
258
261
  slideIndex
259
262
  ].geometry;
260
263
  const candidates = [...svg.querySelectorAll("[data-opf-path]")];
@@ -262,7 +265,7 @@ export function createCanvasEditor(container, options = {}) {
262
265
  for (const node of candidates) {
263
266
  const path = node.getAttribute("data-opf-path");
264
267
  const item = geometry.items.find((item) => item.path === path);
265
- const value = getValueAtPath(document, path);
268
+ const value = getValueAtPath(presentation, path);
266
269
  if (
267
270
  seen.has(path) ||
268
271
  node.getAttribute('data-opf-generated') === 'true' ||
@@ -340,8 +343,8 @@ export function createCanvasEditor(container, options = {}) {
340
343
  });
341
344
  if (path === selectedPath) node.setAttribute("data-canvas-selected", "");
342
345
  }
343
- layoutHandles.update(document, geometry);
344
- blockControls.update(document, geometry);
346
+ layoutHandles.update(presentation, geometry);
347
+ blockControls.update(presentation, geometry);
345
348
  imageCropper?.update();
346
349
  if (active?.kind === "text") positionInput();
347
350
  if (active?.kind === "rich-text") active.rich?.update();
@@ -350,7 +353,7 @@ export function createCanvasEditor(container, options = {}) {
350
353
  options.onFonts?.({ state: "ready" });
351
354
  }
352
355
  options.onRender?.({
353
- document,
356
+ presentation,
354
357
  slideIndex,
355
358
  svg,
356
359
  geometry,
@@ -366,11 +369,11 @@ export function createCanvasEditor(container, options = {}) {
366
369
  }
367
370
  // An in-progress edit whose text needs faces that are not loaded yet waits for them, then draws again.
368
371
  function deferDraft(draft) {
369
- if (!fonts?.pending(draft, renderOptions).length) return false;
372
+ if (!fonts?.pending(draft, resolveOptions()).length) return false;
370
373
  active.valid = false;
371
374
  notice.hidden = false;
372
375
  notice.textContent = pendingMessage;
373
- fonts.ensure(draft, { renderOptions }).then(
376
+ fonts.ensure(draft, { renderOptions: resolveOptions() }).then(
374
377
  () => {
375
378
  if (disposed) return;
376
379
  if (notice.textContent === pendingMessage) clearNotice();
@@ -392,7 +395,7 @@ export function createCanvasEditor(container, options = {}) {
392
395
  clearNotice();
393
396
  render(draft);
394
397
  options.onDraft?.({
395
- document: draft,
398
+ presentation: draft,
396
399
  path: active.path,
397
400
  value: undefined,
398
401
  });
@@ -403,13 +406,13 @@ export function createCanvasEditor(container, options = {}) {
403
406
  }
404
407
  try {
405
408
  const value = active.kind === "rich-text" ? active.rich.value : parseCanvasValue(active.input.value, active.type, active.originalValue);
406
- const draft = createCanvasDraft(editor.document, active.path, value);
409
+ const draft = createCanvasDraft(editor.presentation, active.path, value);
407
410
  if (deferDraft(draft)) return;
408
411
  clearNotice();
409
412
  active.valid = true;
410
413
  active.draft = draft;
411
414
  render(draft);
412
- options.onDraft?.({ document: draft, path: active.path, value });
415
+ options.onDraft?.({ presentation: draft, path: active.path, value });
413
416
  } catch (error) {
414
417
  active.valid = false;
415
418
  report(error);
@@ -429,8 +432,8 @@ export function createCanvasEditor(container, options = {}) {
429
432
  const font = win.getComputedStyle(text),
430
433
  fontSize = parseFloat(font.fontSize),
431
434
  geometry = resolvePresentation(
432
- active.draft ?? editor.document,
433
- renderOptions,
435
+ active.draft ?? editor.presentation,
436
+ drawOptions(),
434
437
  ).slides[slideIndex].geometry;
435
438
  const item = geometry.items.find((item) => item.path === active.path);
436
439
  const metricLayout=geometry.items.find(item=>item.metricLayout?.parts.some(part=>part.path===active.path))?.metricLayout;
@@ -600,7 +603,7 @@ export function createCanvasEditor(container, options = {}) {
600
603
  // Only offer rich text where the canonical schema accepts TextRun[].
601
604
  if (typeof value === "string") {
602
605
  try {
603
- createCanvasDraft(editor.document, path, [value]);
606
+ createCanvasDraft(editor.presentation, path, [value]);
604
607
  const format = doc.createElement("button");
605
608
  format.type = "button";
606
609
  format.textContent = "Format text";
@@ -683,11 +686,11 @@ export function createCanvasEditor(container, options = {}) {
683
686
  }));
684
687
  }
685
688
  function propertyDraft(edit) {
686
- const draft = applyJsonPatch(editor.document, propertyPatches(edit)),
687
- validation = validateOpfDocument(draft);
689
+ const draft = applyJsonPatch(editor.presentation, propertyPatches(edit)),
690
+ validation = checkFormat(draft);
688
691
  if (!validation.valid)
689
692
  throw new Error(
690
- validation.errors[0]?.message ?? "This change is not valid OPF.",
693
+ firstErrorMessage(validation, "This change is not valid OPF."),
691
694
  );
692
695
  return draft;
693
696
  }
@@ -706,10 +709,20 @@ export function createCanvasEditor(container, options = {}) {
706
709
  label.style.cssText =
707
710
  "display:block;margin-top:10px;color:#756b85;font-size:11px";
708
711
  label.textContent = field.label;
712
+ const choices = field.type === "string" ? fieldChoices(field) : undefined;
709
713
  const input = doc.createElement(
710
- field.type === "boolean" ? "input" : "textarea",
714
+ choices ? "select" : field.type === "boolean" ? "input" : "textarea",
711
715
  );
712
- if (field.type === "boolean") {
716
+ if (choices) {
717
+ // A fixed set of values (a timeline event's status) is a menu, not free text.
718
+ for (const choice of choices) {
719
+ const option = doc.createElement("option");
720
+ option.value = choice;
721
+ option.textContent = choice;
722
+ input.append(option);
723
+ }
724
+ input.value = field.value;
725
+ } else if (field.type === "boolean") {
713
726
  input.type = "checkbox";
714
727
  input.checked = field.value;
715
728
  } else {
@@ -780,26 +793,9 @@ export function createCanvasEditor(container, options = {}) {
780
793
  if (!commit()) return;
781
794
  try {
782
795
  const values = editor.get(array.path);
783
- const empty = (value, key = "") =>
784
- Array.isArray(value)
785
- ? value.map((child) => empty(child))
786
- : value && typeof value === "object"
787
- ? Object.fromEntries(
788
- Object.entries(value).map(([key, child]) => [
789
- key,
790
- empty(child, key),
791
- ]),
792
- )
793
- : typeof value === "number"
794
- ? 0
795
- : typeof value === "boolean"
796
- ? false
797
- : ["type", "mode", "language"].includes(key)
798
- ? value
799
- : "";
800
796
  const next = remove
801
797
  ? values.slice(0, -1)
802
- : [...values, empty(values.at(-1) ?? "")];
798
+ : [...values, emptyLike(values.at(-1) ?? "")];
803
799
  editor.set(array.path, next, {
804
800
  source: "canvas-collection",
805
801
  rejectInvalid: true,
@@ -876,7 +872,7 @@ export function createCanvasEditor(container, options = {}) {
876
872
  value = parseCanvasValue(edit.input.value, edit.type, edit.originalValue);
877
873
  else value = propertyDraft(edit);
878
874
  if (edit.kind === "text" || edit.kind === "rich-text")
879
- validateRender(createCanvasDraft(editor.document, edit.path, value));
875
+ validateRender(createCanvasDraft(editor.presentation, edit.path, value));
880
876
  committing = true;
881
877
  if ((edit.kind === "text" || edit.kind === "rich-text") && JSON.stringify(value) !== edit.base)
882
878
  editor.set(edit.path, value, { source: "canvas", rejectInvalid: true });
@@ -928,7 +924,7 @@ export function createCanvasEditor(container, options = {}) {
928
924
  options.onCancel?.({ path });
929
925
  }
930
926
  function setSlide(index) {
931
- if (!Number.isInteger(index) || !editor.document.slides?.[index])
927
+ if (!Number.isInteger(index) || !editor.presentation.slides?.[index])
932
928
  throw new RangeError("Slide index is out of range.");
933
929
  if (index === slideIndex) {
934
930
  if (!active) renderFor();
@@ -1027,7 +1023,7 @@ export function createCanvasEditor(container, options = {}) {
1027
1023
  get layoutEditing() { return layoutHandles.enabled; },
1028
1024
  /** True while the crop layer is open. */
1029
1025
  get cropping() { return !!imageCropper?.isOpen; },
1030
- /** Open the crop layer for the picture at `path` (an image block, a slide's `image`, a slide image). `tool: "focus"` starts with the focal point. */
1026
+ /** Open the crop layer for the picture at `path` (an image block or a slide's `image`). `tool: "focus"` starts with the focal point. */
1031
1027
  cropImage(path, cropOptions) {
1032
1028
  if (!imageCropper) return Promise.resolve(false);
1033
1029
  return imageCropper.open(path, cropOptions);
@@ -0,0 +1,48 @@
1
+ import type { Catalog, CatalogKind, CatalogRecordChange, EmbeddedRecord, UnresolvedReferenceDiagnostic, ValidationReport } from "@openpresentation/opf";
2
+ import type { CatalogOption, EditorChange, EditorSession, JsonPatchOperation } from "./index.js";
3
+
4
+ /** One catalog list: the session's registration first, then each host list; the first catalog with a `source` wins. Throws `invalid-catalogs` for an entry without a source. */
5
+ export declare function mergeCatalogs(...lists: (readonly Catalog[] | Catalog | undefined | null)[]): readonly Catalog[];
6
+ /** The catalogs for one operation: `editor.catalogs`, then `options.catalogs`. */
7
+ export declare function catalogsFor(editor: EditorSession | undefined, options?: { catalogs?: readonly Catalog[] }): readonly Catalog[];
8
+ /** A record's picker label: its name, then its gallery label, then the id. */
9
+ export declare function catalogRecordLabel(record: Record<string, unknown> | undefined, id: string): string;
10
+ /** Core's `catalogRecords` as picker options: embedded records first, then registered ones; `id` is the reference to write. */
11
+ export declare function listCatalogRecords(presentation: unknown, kind: CatalogKind, options?: { catalogs?: readonly Catalog[] }): CatalogOption[];
12
+
13
+ export interface SavedDocument {
14
+ /** The self-contained document: every referenced record embedded once under its group, `x-*` stripped. */
15
+ document: unknown;
16
+ added: EmbeddedRecord[];
17
+ unresolved: UnresolvedReferenceDiagnostic[];
18
+ }
19
+ /** Core's `embed`: the document to save or hand on, which renders the same with no catalog registered. The input is not changed. */
20
+ export declare function prepareSave(presentation: unknown, options?: { catalogs?: readonly Catalog[] }): SavedDocument;
21
+ /** `prepareSave` of the session's document with its registered catalogs (and `options.catalogs`). The session is not changed. */
22
+ export declare function saveDocument(editor: EditorSession, options?: { catalogs?: readonly Catalog[] }): SavedDocument;
23
+
24
+ export interface CatalogUpdateCheck {
25
+ /** One per embedded record (under `default` or a named group) whose registered record differs. */
26
+ changes: CatalogRecordChange[];
27
+ /** RFC 6902; apply only after the author approves (`applyCatalogUpdate`). */
28
+ patch: JsonPatchOperation[];
29
+ }
30
+ /** The "Update from catalog" check (core's `updateFromCatalog`). Applies nothing. `refs` limits it to some references. */
31
+ export declare function checkCatalogUpdates(presentation: unknown, options?: { catalogs?: readonly Catalog[]; refs?: readonly { kind: CatalogKind; reference: string }[] }): CatalogUpdateCheck;
32
+ /** Apply the approved updates (`refs`) as ONE undoable step, recomputed against the session's current document. */
33
+ export declare function applyCatalogUpdate(
34
+ editor: EditorSession,
35
+ options: { refs: readonly { kind: CatalogKind; reference: string }[]; catalogs?: readonly Catalog[]; meta?: Record<string, unknown> },
36
+ ): EditorChange & { changed: boolean; changes: CatalogRecordChange[] };
37
+
38
+ export declare const REFERENCE_FINDING_CODES: readonly ["opf/unresolved-reference", "opf/undeclared-catalog", "opf/catalog-record-not-in-source"];
39
+ /** The reference and embedded-record findings (`REFERENCE_FINDING_CODES`) of a report. */
40
+ export declare function referenceFindings(validation: ValidationReport | undefined): ValidationReport["findings"];
41
+
42
+ export interface CatalogRecordRef { group: string; kind: CatalogKind; id: string }
43
+ /** The record a `opf/catalog-record-not-in-source` finding path names (`/catalogs/<group>/<kind>/<id>`). */
44
+ export declare function catalogRecordAt(path: string): CatalogRecordRef | undefined;
45
+ /** Move a record embedded under `default` or a named group into `catalogs.custom`, rewriting the references that resolved to it. */
46
+ export declare function prepareMoveToCustom(presentation: unknown, target: CatalogRecordRef, options?: { catalogs?: readonly Catalog[] }): { presentation: unknown; patches: JsonPatchOperation[]; changed: boolean; from: CatalogRecordRef; to: CatalogRecordRef };
47
+ /** `prepareMoveToCustom` as one undoable step on the session. */
48
+ export declare function moveToCustom(editor: EditorSession, target: CatalogRecordRef, options?: { catalogs?: readonly Catalog[]; meta?: Record<string, unknown> }): EditorChange & { changed: boolean; from: CatalogRecordRef; to: CatalogRecordRef };