@openpresentation/opf-editor 0.28.0 → 0.29.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 (53) hide show
  1. package/README.md +20 -6
  2. package/dist/canvas.d.ts +13 -1
  3. package/dist/canvas.js +39 -9
  4. package/dist/chart-options-panel.js +16 -16
  5. package/dist/codemirror-guard.js +69 -0
  6. package/dist/content-controls.js +127 -74
  7. package/dist/data-grid.js +15 -7
  8. package/dist/design-controls.d.ts +13 -0
  9. package/dist/design-controls.js +134 -124
  10. package/dist/exact-source.d.ts +30 -0
  11. package/dist/font-gate.js +20 -1
  12. package/dist/json-editor.js +8 -2
  13. package/dist/json-options.d.ts +2 -1
  14. package/dist/json-options.js +7 -6
  15. package/dist/layout-templates.d.ts +8 -3
  16. package/dist/layout-templates.js +30 -16
  17. package/dist/numbering-panel.js +2 -3
  18. package/dist/react.d.ts +12 -4
  19. package/dist/react.js +39 -5
  20. package/dist/slide-adapter.d.ts +49 -0
  21. package/dist/slide-adapter.js +289 -0
  22. package/dist/slide-edits.d.ts +46 -0
  23. package/dist/slide-edits.js +629 -0
  24. package/dist/slide-manager.js +2 -1
  25. package/dist/slide-scene.d.ts +96 -0
  26. package/dist/slide-scene.js +413 -0
  27. package/dist/source-checker.js +45 -6
  28. package/dist/source-completions.js +6 -4
  29. package/dist/source-pane.d.ts +18 -6
  30. package/dist/source-pane.js +45 -0
  31. package/dist/source-worker.js +8 -4
  32. package/dist/studio-canvas.d.ts +34 -0
  33. package/dist/studio-canvas.js +586 -0
  34. package/dist/studio-commands.d.ts +15 -0
  35. package/dist/studio-commands.js +155 -0
  36. package/dist/studio-design.d.ts +16 -0
  37. package/dist/studio-design.js +354 -0
  38. package/dist/studio-kit/dom.d.ts +16 -0
  39. package/dist/studio-kit/dom.js +42 -6
  40. package/dist/studio-layers.d.ts +12 -0
  41. package/dist/studio-layers.js +218 -0
  42. package/dist/studio.d.ts +31 -0
  43. package/dist/studio.js +1608 -0
  44. package/dist/styles.css +1546 -117
  45. package/dist/thumbnail-cache.d.ts +2 -0
  46. package/dist/thumbnail-cache.js +10 -1
  47. package/dist/workspace-measure.js +6 -4
  48. package/dist/workspace-parts.js +20 -0
  49. package/dist/workspace-shell.js +25 -15
  50. package/dist/workspace-theme.js +3 -0
  51. package/dist/workspace.d.ts +88 -9
  52. package/dist/workspace.js +210 -49
  53. package/package.json +29 -18
@@ -6,7 +6,7 @@ import { isolateHistory } from "@codemirror/commands";
6
6
  import { EditorSelection } from "@codemirror/state";
7
7
  import { describeField } from "@openpresentation/opf/schema";
8
8
  import { listCatalogRecords, mergeCatalogs } from "./catalogs.js";
9
- import { LAYOUT_INTENT_GROUPS, layoutRegionSummary } from "./layout-templates.js";
9
+ import { CUSTOM_LAYOUTS_GROUP, layoutGroupName, layoutGroupOrder, layoutRegionSummary } from "./layout-templates.js";
10
10
  import { Document, parse as parseYamlDocument, parseDocument } from "yaml";
11
11
  import { pathAt, scanMarkdown } from "@openpresentation/opf/source";
12
12
  import { findNodeAtLocation, getNodeValue } from "jsonc-parser";
@@ -188,12 +188,14 @@ function literalOption(label, literal, extra) {
188
188
  function catalogOptions(kind, context, catalogs) {
189
189
  let records = [];
190
190
  try { records = listCatalogRecords(context.document, kind, { catalogs }); } catch { records = []; }
191
+ // Deck records first, then the catalog's groups in its own order (and its records in its order within a group), then custom layouts.
192
+ const position = new Map(records.map((record, index) => [record, index]));
193
+ const groups = kind === "layouts" ? layoutGroupOrder(records) : [];
191
194
  const order = (record) => {
192
195
  if (record.origin === "document") return -1;
193
196
  if (kind !== "layouts") return 0;
194
- const id = record.id.replace(/^default:/, "");
195
- const group = LAYOUT_INTENT_GROUPS.findIndex((entry) => entry.ids.includes(id));
196
- return group < 0 ? LAYOUT_INTENT_GROUPS.length * 100 : group * 100 + LAYOUT_INTENT_GROUPS[group].ids.indexOf(id);
197
+ const group = layoutGroupName(record);
198
+ return (group === CUSTOM_LAYOUTS_GROUP ? groups.length : groups.indexOf(group)) * 100000 + position.get(record);
197
199
  };
198
200
  records.sort((a, b) => order(a) - order(b) || a.label.localeCompare(b.label, "en", { numeric: true }) || a.id.localeCompare(b.id, "en"));
199
201
  const intent = kind === "layouts";
@@ -179,13 +179,17 @@ export interface SourcePaneOptions {
179
179
  smartListEnter?: boolean;
180
180
  /**
181
181
  * Run the pane's text checks (reading YAML or Markdown, the syntax pass of JSON and the document validation) in a Web Worker, so typing in a deck
182
- * of a thousand slides never blocks the main thread. A function returns the Worker (`() => new Worker(new URL("@openpresentation/opf-editor/source-worker", import.meta.url), { type: "module" })`
183
- * with a bundler that supports that form); a Worker is used as given; `true` starts `source-worker.js` beside the package's own module, which works
184
- * when a bundler follows `new URL(..., import.meta.url)`. Only the typing delay uses the worker: every explicit action (`getText`, switching the format,
185
- * `replaceDocument`, Format, Undo) is applied on the main thread at once. When the worker cannot start, fails or stops answering, the pane checks on the
186
- * main thread from then on and `onError` is called once; no edit is lost. Default `false` (everything on the main thread).
182
+ * of a thousand slides never blocks the main thread. `true` starts the package's own worker with `new Worker(new URL("./source-worker.js",
183
+ * import.meta.url), { type: "module" })`, which webpack, Turbopack and Vite (build) bundle; a function returns the Worker
184
+ * (`() => new Worker(new URL("@openpresentation/opf-editor/source-worker", import.meta.url), { type: "module" })`); a Worker is used as given; a URL
185
+ * (or a string) is the address of a worker script the host built itself (esbuild: build `@openpresentation/opf-editor/source-worker` as an entry
186
+ * point) and is started as a module worker. The worker file imports core, so it only runs bundled. See docs/embedding.md, "The source worker",
187
+ * for each bundler. Only the typing delay uses the worker: every explicit action (`getText`, switching the format, `replaceDocument`, Format, Undo)
188
+ * is applied on the main thread at once. When the worker cannot start, fails, stops answering, or loads but does not say it is ready within 5 s (a
189
+ * script the bundler emptied), the pane checks on the main thread from then on and `onError` is called once with a message that names the
190
+ * worker; no edit is lost. Default `false` (everything on the main thread).
187
191
  */
188
- worker?: boolean | Worker | (() => Worker);
192
+ worker?: boolean | string | URL | Worker | (() => Worker);
189
193
  /** A preview for the card F12 and Alt+F12 open on a catalog record only the app provides (see `JsonCodeEditorOptions.resolvePreview`). */
190
194
  resolvePreview?: (kind: string, id: string) => HTMLElement | string | undefined | Promise<HTMLElement | string | undefined>;
191
195
  onError?(error: unknown): void;
@@ -218,6 +222,14 @@ export interface SourcePane {
218
222
  * (`invalid-opf-edit` with `issues`) and changes nothing. Works in a read-only pane.
219
223
  */
220
224
  replaceDocument(document: unknown, options?: ReplaceDocumentOptions): EditorChange;
225
+ /**
226
+ * Load raw source text as the pane's whole text, as if pasted over everything: a file as it was read, a draft that does not parse. Nothing is
227
+ * converted; the bytes stay as given (line endings included; a leading byte order mark is dropped). `options.format` switches the pane to the
228
+ * text's format first (without writing the document in it), `options.name` renames the file. Text that reads as a document the session
229
+ * accepts is applied at once as one undoable edit; anything else stays as a draft with its problems and the session keeps its last valid
230
+ * document. Returns true when the session now holds the text's document. Throws in a read-only pane.
231
+ */
232
+ setSourceText(text: string, options?: { format?: SourceFormat; name?: string }): boolean;
221
233
  /** Re-read the document from the session. */
222
234
  refresh(): void;
223
235
  /** Replace the host findings; `null` returns to the session's own validation. */
@@ -708,6 +708,44 @@ export function createSourcePane(container, options = {}) {
708
708
  options.onFormatChange?.(format, previous);
709
709
  }
710
710
 
711
+ /**
712
+ * Put `text` in the pane as its whole source, in `next` (default the pane's format), as if the person had pasted it over everything: no
713
+ * conversion, the bytes as given (line endings included; a byte order mark is dropped). Text that reads as a document the session accepts is
714
+ * applied at once (one undoable edit; Undo in the code brings the old text back); anything else stays in the pane as a draft with its problems,
715
+ * and the session keeps its last valid document. Returns true when the session now holds the text's document.
716
+ */
717
+ function setSourceText(text, { format: next, name: nextName } = {}) {
718
+ if (destroyed) throw new Error("The source pane was destroyed.");
719
+ if (typeof text !== "string") throw new TypeError("setSourceText needs the source text as a string.");
720
+ if (next !== undefined && !FORMATS.some((entry) => entry.id === next)) throw new TypeError(`setSourceText: format must be "json", "yaml" or "md", not ${JSON.stringify(next)}.`);
721
+ if (readOnly) throw new Error("The source pane is read-only.");
722
+ flush();
723
+ closeConfirm(false);
724
+ const previous = format;
725
+ for (const key of Object.keys(drafts)) delete drafts[key];
726
+ transientNotes = [];
727
+ rejectionFindings = [];
728
+ syntaxFindings = [];
729
+ if (next !== undefined && next !== format) {
730
+ format = next;
731
+ noteFindings = [];
732
+ control.setLanguage(formatOf(next).language);
733
+ refreshDecorations();
734
+ }
735
+ if (nextName !== undefined) name = sanitizeName(nextName) || "deck";
736
+ updateBar();
737
+ control.api.replace(0, control.api.getValue().length, text.replace(/^\uFEFF/, ""));
738
+ control.api.setSelection(0);
739
+ // Read and apply it now, also when it is the text already shown (in another format, say).
740
+ baseline = null;
741
+ flush();
742
+ renderedKey = "";
743
+ renderFindings();
744
+ syncAttributes();
745
+ if (format !== previous) options.onFormatChange?.(format, previous);
746
+ return !dirty;
747
+ }
748
+
711
749
  // ---- Menu ---------------------------------------------------------------------------------------------------------
712
750
  const menuOpen = () => !menu.hidden;
713
751
  function openMenu(focus = "checked") {
@@ -795,6 +833,8 @@ export function createSourcePane(container, options = {}) {
795
833
  nameButton.textContent = name;
796
834
  nameButton.setAttribute("aria-label", `Rename ${name}`);
797
835
  formatLabel.textContent = `.opf.${current.extension}`;
836
+ // The visible extension is part of the button's name (WCAG 2.5.3, label in name).
837
+ formatButton.setAttribute("aria-label", `Source format .opf.${current.extension}`);
798
838
  root.dataset.format = current.id;
799
839
  canonicalAction.hidden = current.id === "json";
800
840
  for (const item of menuItems) item.setAttribute("aria-checked", String(item.dataset.format === current.id));
@@ -1291,6 +1331,11 @@ export function createSourcePane(container, options = {}) {
1291
1331
  renderedKey = "";
1292
1332
  renderFindings();
1293
1333
  },
1334
+ /**
1335
+ * Load raw source text (a file, a paste, a draft that does not parse) as the pane's whole text, in `options.format`, without converting it.
1336
+ * Returns true when it read as a document the session accepted, false when it stays a draft with its problems. See `setSourceText` above.
1337
+ */
1338
+ setSourceText,
1294
1339
  /** Switch the source format ("json", "yaml" or "md"). Returns false when the document cannot be written in it. */
1295
1340
  setFormat: (next) => setFormatInternal(next, false),
1296
1341
  getFormat: () => format,
@@ -1,7 +1,9 @@
1
- // The worker entry of the source pane's text checks (`createSourcePane(el, { worker })`). A host bundles this file as its own worker script
2
- // (`@openpresentation/opf-editor/source-worker`), for example `new Worker(new URL("@openpresentation/opf-editor/source-worker", import.meta.url), { type: "module" })`
3
- // with a bundler that understands that form, or builds it as an extra entry point and serves it next to the page. It does the same work as the pane's
4
- // own inline path (`./source-check.js`), off the main thread. Requests run in order, but when several checks of one kind are waiting only the newest runs
1
+ // The worker entry of the source pane's text checks (`createSourcePane(el, { worker })`). `worker: true` starts it with
2
+ // `new Worker(new URL("./source-worker.js", import.meta.url), { type: "module" })`, which webpack, Turbopack, Vite (build) and Parcel bundle; a host can
3
+ // also write `new Worker(new URL("@openpresentation/opf-editor/source-worker", import.meta.url), { type: "module" })` itself, or build this file as an
4
+ // extra entry point (esbuild) and pass its URL. The file imports core, so it runs only bundled. See docs/embedding.md, "The source worker". It does the
5
+ // same work as the pane's own inline path (`./source-check.js`), off the main thread. It says `{ ready: true }` once it runs, so the pane can tell a
6
+ // worker that loaded but never started (an emptied script) from a slow check. Requests run in order, but when several checks of one kind are waiting only the newest runs
5
7
  // (the older ones are answered with `skipped`): the pane drops stale replies anyway.
6
8
  import { createCheckService } from "./source-check.js";
7
9
 
@@ -23,6 +25,8 @@ function drain() {
23
25
  });
24
26
  }
25
27
 
28
+ self.postMessage({ ready: true });
29
+
26
30
  self.onmessage = (event) => {
27
31
  queue.push(event.data);
28
32
  // Messages that arrive while a check runs queue behind it; draining on the next task lets everything already waiting be seen together.
@@ -0,0 +1,34 @@
1
+ /** CE-20 (internal): the Studio's canvas: the kit viewport and overlay over the workspace's canvas, driven by the slide adapter. */
2
+ import type { KitDom } from "./studio-kit/dom.js";
3
+ import type { CanvasEditor } from "./canvas.js";
4
+ import type { SlideAdapter } from "./slide-adapter.js";
5
+ import type { SceneNode } from "./slide-scene.js";
6
+
7
+ export declare function dottedPath(pointer: string): string;
8
+ export declare function isTextNode(node: SceneNode | null | undefined): boolean;
9
+ export interface StudioCanvas {
10
+ paint(): void;
11
+ paintHover(): void;
12
+ readonly zoom: number;
13
+ readonly panMode: boolean;
14
+ setZoom(zoom: number, options?: { anchor?: { clientX: number; clientY: number } }): number;
15
+ step(direction: number): number;
16
+ fit(mode?: "slide" | "width"): number;
17
+ readonly tool: "select" | "hand" | "text";
18
+ setTool(tool: "select" | "hand" | "text"): void;
19
+ focusPrimary(): void;
20
+ boxOf(key: string): Element | undefined;
21
+ describe(node: SceneNode): string;
22
+ beginInsertDrag(kind: string, event: PointerEvent, options?: { onEnd?(): void }): boolean;
23
+ readonly dragging: boolean;
24
+ cancelDrag(): void;
25
+ setEnabled(on: boolean): void;
26
+ destroy(): void;
27
+ }
28
+ export declare function createStudioCanvas(options: {
29
+ dom: KitDom;
30
+ elements: { root: HTMLElement; viewport: HTMLElement; stage: HTMLElement; frame: HTMLElement; overlay: HTMLElement };
31
+ canvas: CanvasEditor;
32
+ adapter: SlideAdapter;
33
+ studio: any;
34
+ }): StudioCanvas;