@openpresentation/opf-editor 0.15.0 → 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 +33 -6
- package/dist/canvas.js +14 -2
- package/dist/catalog-control.js +21 -0
- package/dist/design-controls.js +26 -6
- package/dist/export.d.ts +6 -1
- package/dist/export.js +6 -1
- package/dist/find-replace.js +1 -0
- package/dist/font-gate.js +0 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16 -4
- package/dist/json-options.js +7 -6
- 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 +5 -1
- package/dist/slide-manager.js +3 -1
- package/dist/svelte.d.ts +1 -0
- package/dist/svelte.js +3 -2
- package/dist/switches.js +3 -2
- package/dist/table-options.d.ts +6 -1
- package/dist/table-options.js +43 -0
- package/package.json +18 -9
package/README.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Unfinished prepared shaping work is preserved in the [September 15 roadmap](docs/roadmap-shaping-20260915.md); it is not part of the published runtime.
|
|
4
4
|
|
|
5
|
+
## Using the editor with opf-render 0.16: what to install (RR-63, unreleased)
|
|
6
|
+
|
|
7
|
+
`@openpresentation/opf-render` is an optional peer of the editor, and from render 0.16 its heavy pieces are optional peers too, so install only what you use. The editor itself draws with `@openpresentation/opf-render/svg` and loads fonts with `/fonts-browser`, which need nothing else. Add:
|
|
8
|
+
|
|
9
|
+
| You use | Also install |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| The canvas, the preview, SVG downloads in a browser (the host serves its own font files) | nothing |
|
|
12
|
+
| `loadFonts` from `/fonts-node` (a server, a test, a build script such as `npm run build:playground`) | the font packages of the pack: `@expo-google-fonts/roboto` and `roboto-mono` for `base`; also `arimo`, `caladea`, `cousine`, `gelasio`, `tinos` and `noto-sans` for `office`; a script face (`scripts: 'auto'`) needs its `@expo-google-fonts/noto-*` package. A missing one fails with `font-resource-unavailable` and names it |
|
|
13
|
+
| `svgToPng` / `svgToPdf` from the Node entries (`/png`, `/pdf`) | `@resvg/resvg-js` (and `sharp`, `pdf-lib` as the renderer's README says); a missing one fails with `converter-missing` |
|
|
14
|
+
| `exportDeck` PDF (vector), PNG, SVG | nothing: the browser entry `@openpresentation/opf-render/export-browser` imports no converter |
|
|
15
|
+
| `exportDeck` raster PDF (`pdfMode: "raster"`) | `pdf-lib`, imported by the page and passed as the `pdfLib` option (`import * as pdfLib from "pdf-lib"`); without it the export rejects with `converter-missing` |
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const pdfLib = await import("pdf-lib");
|
|
19
|
+
await exportDeck(editor.presentation, { format: "pdf", pdfMode: "raster", pdfLib, fonts });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The playground imports `pdf-lib` for you when its raster PDF choice is used.
|
|
23
|
+
|
|
5
24
|
## OPF 0.15: catalogs from the host, backgrounds and image blocks (FA-23)
|
|
6
25
|
|
|
7
26
|
The release after 0.14.2 moves the editor onto core 0.15 (a breaking, clean spec: no migration). See `changes/fa-23-opf-0-15.md`.
|
|
@@ -24,7 +43,7 @@ The release after 0.14.2 moves the editor onto core 0.15 (a breaking, clean spec
|
|
|
24
43
|
- **Editing a catalog's record forks it.** An edit of a layout, theme, colour scheme or font scheme embedded under `catalogs.default` or a named group (from a panel, Properties, Source or any `applyPatch`) moves it into `catalogs.custom` as `<id>-custom` (or the id in `meta.forkIds["<group>:<kind>:<id>"]`), rewrites every reference to it and drops the original, in the same undo step. The change and the session event carry `forked` and a `notice` ("… is now this presentation's own; it no longer receives catalog updates"); approved catalog updates and `moveToCustom` change records in place (`meta.catalogRecordEdit: "in-place"`). `moveToCustom(editor, { group, kind, id }, { id })` is the explicit form, offered for `opf/catalog-record-not-in-source`.
|
|
25
44
|
- **Backgrounds** take the flat image form `{ type: "image", src, alt, fit, focus, opacity, recolor, overlay }` (fits `cover`, `contain`, `stretch`, `tile`) and the image-source shorthand, on the deck and on a slide. **Image blocks** carry their own fit, focus, treatments and placement (`/image-options`, the panel's `image` section): a full-bleed photo is a background, a picture beside the content is a placed image block. `design.imageFit` is the default fit of image blocks.
|
|
26
45
|
|
|
27
|
-
|
|
46
|
+
Editor 0.15.0 uses core `^0.15.1` and the optional renderer peer `^0.15.0`. Install renderer and PPTX 0.15 together for the coordinated canvas and export workflow. The shared APIs are core's `validate`, `Finding`, `resolveSlideContext` and the catalog helpers above, and the renderer's `loadFonts()` handle passed as `fonts` to the canvas, export, composition and pagination. Historical version changes are recorded in `CHANGELOG.md`.
|
|
28
47
|
|
|
29
48
|
Version 0.8.0 required `@openpresentation/opf` ^0.11.0, renderer ^0.9.0, and PPTX ^0.9.0. Named ColorRefs (scheme slots, roles, and `var:<id>`) paint through the published renderer and hex-resolve on export. Payload ids share the slide id namespace for pagination and transfer. Native `schemeClr`, theme write, and `p:hf` remain out of scope.
|
|
30
49
|
|
|
@@ -398,6 +417,8 @@ setTableCellStyle(editor, "slides.4.blocks.1.table", [{ section: "header", colum
|
|
|
398
417
|
|
|
399
418
|
Styles use scheme roles, so they follow the color scheme, and the renderers keep the text readable on any fill. A style sets the header fill (`theme`, `plain`, `accent`), banded rows and borders (`theme`, `none`, `horizontal`, `grid`); named presets are `theme`, `banded`, `grid`, `minimal` and `open`, and `theme` removes a previous style. A style owns only the fill and border fields of a cell: text color, alignment, padding, values and merges are kept. Merging never hides text: covered cells that hold text refuse with `merge-would-lose-content` unless you pass `join: true`, which joins the words into the anchor with a space and keeps run formatting. Other refusals are `merge-overlap` (the region crosses another merge), `invalid-table-span` (outside the table, or a header cell spanning into the body) and `table-cell-covered`. Splitting leaves the formerly covered cells as empty text. `parseTableCellPath` maps a canvas selection to a table and cell, and `describeTableCell` and `readTableStyle` read the current state.
|
|
400
419
|
|
|
420
|
+
Table alt text (FA-27): `setTableAlt(editor, tablePath, { alt: "North America leads EMEA in Q4." })` writes the table's text alternative (`Table.alt`) as one undoable patch, like a chart's. `alt` is trimmed and an empty string or `null` removes it; `{ decorative: true }` writes the empty alt (decorative, and it wins over `alt`) and `{ decorative: false }` removes an empty alt. It works on a table that shows a shared dataset too, since the alt belongs to the table and not to its data. `readTableAlt(table)` gives `{ alt, decorative }` for a form and `prepareTableAlt` is the headless patch. The table section of the design controls has an "Alt text" field and a "Decorative (no alt text)" box, and find and replace searches `table.alt`. `validate` does not ask for a table alt, so the field is optional.
|
|
421
|
+
|
|
401
422
|
## Slide management (RR-21)
|
|
402
423
|
|
|
403
424
|
`@openpresentation/opf-editor/slides` adds, duplicates, deletes, reorders, hides and sections slides. Every function has a `prepare…` form that returns the JSON Patch for a document without touching a session (`{ presentation, patches, changed, selection }`), and an apply form that commits it as ONE validated, undoable change, so a single Undo restores the deck exactly and the preview, thumbnails and PPTX export follow from the document. An operation that changes nothing commits nothing (`changed: false`).
|
|
@@ -595,7 +616,7 @@ import { exportDeck } from "@openpresentation/opf-editor/export";
|
|
|
595
616
|
const result = await exportDeck(editor.presentation, {
|
|
596
617
|
format: "pdf", // "pdf" | "png" | "svg"
|
|
597
618
|
slides: "all", // or "current" with slideIndex, or [slide numbers]; hidden slides only with includeHidden
|
|
598
|
-
pdfMode: "vector", // or "raster" (an image per slide); PNG and raster density: scale 1 to 4
|
|
619
|
+
pdfMode: "vector", // or "raster" (an image per slide; also pass pdfLib: import * as pdfLib from "pdf-lib"); PNG and raster density: scale 1 to 4
|
|
599
620
|
fonts, // the renderer's fonts handle (loadFonts): measures, loads the faces the deck needs, supplies the faces to embed
|
|
600
621
|
renderOptions, // catalogs, date, ...
|
|
601
622
|
signal, onProgress, onDiagnostic,
|
|
@@ -609,7 +630,11 @@ const result = await exportDeck(editor.presentation, {
|
|
|
609
630
|
- **Names.** `exportFileName(deck, ext, suffix)` uses the deck's `filename` (a trailing .pptx/.pdf/.png/.svg dropped), else the slugified `name`, else `presentation`; slides are `name-01.png`, archives `name-png.zip`.
|
|
610
631
|
- **Progress and cancel.** `onProgress({ stage, done, total, message })` reports fonts, drawing, per-page conversion and packing; an aborted `signal` rejects with `export-aborted` between pages and slides.
|
|
611
632
|
- **Diagnostics.** `describeDiagnostic` normalises renderer and converter notes into `{ code, severity, message, slide? }`: `pdf-font-substituted`, `pdf-glyph-missing`, `pdf-raster-fallback` and the renderer's own are `warning`; `pdf-font-embedded` is `info`.
|
|
612
|
-
- PDF and PNG need `@openpresentation/opf-render` with its `export-browser` entry (RR-23, opf-render#105); without it they reject with `export-unavailable` and SVG still works. Verified in Chromium; Safari and Firefox are not exercised in CI. The playground bundle grows by the PDF writer, fontkit shaping and the bidi algorithm.
|
|
633
|
+
- PDF and PNG need `@openpresentation/opf-render` with its `export-browser` entry (RR-23, opf-render#105); without it they reject with `export-unavailable` and SVG still works. The entry imports no PDF library (opf-render 0.16): the vector PDF, PNG and SVG need nothing else, and `pdfMode: "raster"` needs the `pdfLib` option. Verified in Chromium; Safari and Firefox are not exercised in CI. The playground bundle grows by the PDF writer, fontkit shaping and the bidi algorithm.
|
|
634
|
+
|
|
635
|
+
Catalog controls initialize an absent deck or slide `design` for known theme, color-scheme and font-scheme fields as one validated transaction. Undo removes a newly created parent; existing design fields are retained. DOM, React and Svelte catalog controls accept `onError(error)` and emit a bubbling `opferror` event on rejected edits, restore the committed selection and expose native validity feedback. General `editor.set` and JSON Patch require existing parents.
|
|
636
|
+
|
|
637
|
+
Optional downloads can be deferred until requested: dynamically import `@openpresentation/opf-editor/export` inside the download handler. For a PowerPoint download, dynamically import `@openpresentation/opf-pptx` inside that handler too; the `/export` helper covers PDF, PNG and SVG. This defers conversion code, while canvas rendering and font measurement still have their own startup costs.
|
|
613
638
|
|
|
614
639
|
## Optional React Bindings
|
|
615
640
|
|
|
@@ -657,7 +682,7 @@ Public npm package publication is handled by `.github/workflows/release.yml` thr
|
|
|
657
682
|
|
|
658
683
|
The current checkout uses `@openpresentation/opf/composition` for portable geometry. Slides can select `auto`, `row`, `column`, or `grid`, set weighted tracks, and request path-specific overflow diagnostics. See the sibling OPF repo's `docs/dynamic-composition.md` for the complete contract.
|
|
659
684
|
|
|
660
|
-
|
|
685
|
+
The current editor requires core `^0.15.1`; canvas rendering uses the optional renderer peer `^0.15.0`, and playground PPTX import/export uses `@openpresentation/opf-pptx@^0.15.0`. Clean registry installs support composition without sibling checkouts. For coordinated source development, build OPF and run `node scripts/link-ecosystem.mjs` there; `pnpm test:ecosystem` verifies shared geometry and import/export behavior.
|
|
661
686
|
|
|
662
687
|
## Local interactive demo
|
|
663
688
|
|
|
@@ -669,7 +694,7 @@ Nested content groups expose their bounds through `editor.composeSlide(index).gr
|
|
|
669
694
|
|
|
670
695
|
`editor.paginateSlide(index, {minFontSize:24})` splits a crowded draft into ordinary OPF slides as one undoable transaction. It returns the change and source mappings; failed pagination leaves the document unchanged. The playground’s Split overflow action demonstrates the workflow.
|
|
671
696
|
|
|
672
|
-
`composeSlide(index, { fonts })` and `paginateSlide(index, { fonts })` accept the same fonts handle as preview and export. The local playground now uses bundled, embedded fonts and reports substitutions. The font a user selects (for example Calibri or Aptos) is the source of truth and stays in the document; because license-restricted (proprietary) fonts are never bundled, the canvas draws an open look-alike (a metric-compatible one such as Carlito for Calibri where it exists,
|
|
697
|
+
`composeSlide(index, { fonts })` and `paginateSlide(index, { fonts })` accept the same fonts handle as preview and export. The local playground now uses bundled, embedded fonts and reports substitutions. The font a user selects (for example Calibri or Aptos) is the source of truth and stays in the document; because license-restricted (proprietary) fonts are never bundled, the canvas draws an open look-alike (a metric-compatible one such as Carlito for Calibri where it exists, the metric-compatible Intos family for Aptos) and the substitution report says so. Since 0.14, PPTX export names the selected font, while preview substitutions remain visible in the font report. See the [OPF font policy](https://github.com/OpenPresentation/opf/blob/main/docs/font-fidelity.md#font-policy-ff-31).
|
|
673
698
|
|
|
674
699
|
|
|
675
700
|
## Copy, paste, and gallery imports
|
|
@@ -680,7 +705,7 @@ The `/galleries` export provides `loadOpfGallery` and `loadOpfGalleryItem`, acce
|
|
|
680
705
|
|
|
681
706
|
The playground includes Copy OPF and Import dialogs with paste, file/drop, gallery search, and URL tabs. Files accept OPF JSON and PowerPoint `.pptx` up to 20 MB. PPTX conversion runs locally using `@openpresentation/opf-pptx`; review the converted preview and diagnostics before inserting slides or opening a presentation. Applying an import is one undoable edit. Custom registries persist in local storage; defaults are configured in `examples/galleries.json`. Copy permissions may require using the Select all fallback. Pasting within text fields keeps normal text editing. Cmd/Ctrl+Shift+C opens copying and Cmd/Ctrl+O opens file import.
|
|
682
707
|
|
|
683
|
-
The **PowerPoint** button commits the current canvas draft and prepares an editable `.pptx` download from that snapshot, using the preview's text measurements. Conversion diagnostics remain visible in the download dialog. Missing/remote images require an explicit host resolver or embedded data; export does not silently fetch them or omit them. Save OPF separately to retain the complete original source.
|
|
708
|
+
The **PowerPoint** button commits the current canvas draft and prepares an editable `.pptx` download from that snapshot, using the preview's text measurements. Conversion diagnostics remain visible in the download dialog. Missing/remote images require an explicit host resolver or embedded data; export does not silently fetch them or omit them. Save OPF separately to retain the complete original source. Since 0.14, PPTX export names the font the user selected, so PowerPoint uses it when installed or available as a Microsoft 365 cloud font. The download does not embed font binaries, and license-restricted (proprietary) fonts are never embedded. Arbitrary native positions and unsupported PowerPoint features can change during reimport.
|
|
684
709
|
|
|
685
710
|
Run `npm ci`, `npm run build:playground`, then serve `artifacts/playground` with a local static server. This example uses the published core/render/PPTX packages and this checkout's editor source; it does not require an account, AI provider or paid service. `npm run test:playground` builds and executes a real browser flow (local Edge on Windows, installed Chromium in CI): author JSON, inline edit, export a native merged table, validate/reimport the actual downloaded file, undo/redo, save OPF and reject malformed input without changing the document. It is browser behavior evidence, not a claim of native PowerPoint raster equivalence.
|
|
686
711
|
|
|
@@ -716,6 +741,8 @@ The canvas can show draggable, keyboard-accessible dividers for root and nested
|
|
|
716
741
|
|
|
717
742
|
### Move complete blocks
|
|
718
743
|
|
|
744
|
+
When the slide's layout record has placeholder groups (FA-26, `{ "type": "group", "composition", "placeholders" }`), Arrange also outlines every slot of the record at its cell (`geometry.slots`): groups, filled regions and labelled empty regions. The slots belong to the record, so they are shown, not edited; the slide's content in them keeps its own selection and handles. Layout pickers and the JSON field menu describe such a record as `title, column (text, text), chart` and compare layouts by their leaf regions.
|
|
745
|
+
|
|
719
746
|
Arrange mode also shows numbered block handles. Drag to reorder siblings, use arrow keys for earlier/later, or click a handle to choose an existing destination group/slide and insertion position. The move preserves the whole block, including rich text, table/chart data, and nested groups. Each move is validated, preflighted by the renderer, and undoable. Parent weights stay with layout positions; moves that leave an empty container or create a containment cycle are rejected.
|
|
720
747
|
|
|
721
748
|
Agents can import `prepareBlockMove` and `listBlockContainers` from `@openpresentation/opf-editor/layout`. The prepared result includes a full candidate document, guarded atomic patches, the new block path, and a changed flag. Destination indexes refer to the pre-removal document. Use `canvas.openBlockMenu(path)` for the corresponding browser controls.
|
package/dist/canvas.js
CHANGED
|
@@ -23,7 +23,7 @@ import { createRichTextInput } from "./rich-text-input.js";
|
|
|
23
23
|
import { textInputOffsetAtPoint } from "./text-pointer.js";
|
|
24
24
|
import { createRichTextToolbar } from "./rich-text-toolbar.js";
|
|
25
25
|
import { createImageCropper } from "./image-cropper.js";
|
|
26
|
-
import { FONTS_PENDING, fontGate, fontsPendingError,
|
|
26
|
+
import { FONTS_PENDING, fontGate, fontsPendingError, whenFontsReady } from "./font-gate.js";
|
|
27
27
|
import { checkFormat, firstErrorMessage } from "./checks.js";
|
|
28
28
|
export { getEditableFields } from "./canvas-fields.js";
|
|
29
29
|
export { whenFontsReady, FONTS_PENDING, FONTS_UNAVAILABLE } from "./font-gate.js";
|
|
@@ -75,7 +75,9 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
75
75
|
const handle = options.fonts, fonts = fontGate(handle);
|
|
76
76
|
// The renderer draws with the session's registered catalogs (plus any `renderOptions.catalogs`): the same list core resolves with.
|
|
77
77
|
const resolveOptions = () => ({ ...renderOptions, catalogs: mergeCatalogs(editor.catalogs, renderOptions.catalogs) });
|
|
78
|
-
|
|
78
|
+
// RR-61: the handle measures, and the canvas embeds no face: the browser handle already added its faces to the page, so
|
|
79
|
+
// `@font-face` data would repeat megabytes of base64 in every redraw. Exports keep the default and carry the faces they draw.
|
|
80
|
+
const drawOptions = (extra) => ({ ...resolveOptions(), fonts: handle, embedFonts: false, ...extra });
|
|
79
81
|
let slideIndex = options.slideIndex ?? 0,
|
|
80
82
|
// RR-32: the canvas edits the document as authored, so a template's {{tokens}} stay visible and an inline edit never
|
|
81
83
|
// overwrites one with its resolved text. The Fill template panel previews the resolved deck. Pass `variables` to override.
|
|
@@ -256,6 +258,9 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
256
258
|
const svg = preview.querySelector("svg");
|
|
257
259
|
svg.setAttribute("role", "group");
|
|
258
260
|
svg.setAttribute("aria-label", "Editable slide");
|
|
261
|
+
// FA-30: the renderer labels its slide root a "slide" group named by the title; the canvas is named for editing instead, so it drops
|
|
262
|
+
// the roledescription rather than announce "Editable slide, slide".
|
|
263
|
+
svg.removeAttribute("aria-roledescription");
|
|
259
264
|
svg.removeAttribute("aria-labelledby");
|
|
260
265
|
const geometry = resolvePresentation(presentation, drawOptions()).slides[
|
|
261
266
|
slideIndex
|
|
@@ -286,6 +291,13 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
286
291
|
continue;
|
|
287
292
|
seen.add(path);
|
|
288
293
|
node.setAttribute("data-canvas-target", "");
|
|
294
|
+
// FA-30: the renderer hides decorative drawing and a picture or chart with an empty alt (aria-hidden), and names a chart with
|
|
295
|
+
// alt as an image group. An editing target is focusable, so neither it nor a group around it may be hidden or presentational:
|
|
296
|
+
// an ancestor image group (the chart's alt) stays named but becomes a plain group so the targets inside it are reachable.
|
|
297
|
+
for (let up = node; up && up !== svg; up = up.parentNode) {
|
|
298
|
+
up.removeAttribute("aria-hidden");
|
|
299
|
+
if (up !== node && up.getAttribute("role") === "img") up.setAttribute("role", "group");
|
|
300
|
+
}
|
|
289
301
|
node.setAttribute("role", "button");
|
|
290
302
|
node.setAttribute("tabindex", "0");
|
|
291
303
|
node.setAttribute(
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Framework-neutral event handling: rejected edits restore the committed value
|
|
2
|
+
// and reach the host callback and a bubbling opferror event.
|
|
3
|
+
export function commitCatalogControl(node, onError, sync, commit) {
|
|
4
|
+
try {
|
|
5
|
+
commit();
|
|
6
|
+
node.setCustomValidity?.("");
|
|
7
|
+
node.removeAttribute?.("aria-invalid");
|
|
8
|
+
node.removeAttribute?.("data-opf-error");
|
|
9
|
+
return true;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
sync();
|
|
12
|
+
node.setCustomValidity?.(error.message);
|
|
13
|
+
node.setAttribute?.("aria-invalid", "true");
|
|
14
|
+
node.setAttribute?.("data-opf-error", error.message);
|
|
15
|
+
onError?.(error);
|
|
16
|
+
const Event = node.ownerDocument?.defaultView?.CustomEvent;
|
|
17
|
+
if (Event) node.dispatchEvent?.(new Event("opferror", { detail: error, bubbles: true }));
|
|
18
|
+
node.reportValidity?.();
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/design-controls.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
import { createContentControls } from "./content-controls.js";
|
|
37
37
|
import { IMAGE_EDGES, IMAGE_FITS, IMAGE_SHAPES, readImageTreatments, setImageTreatment } from "./image-options.js";
|
|
38
38
|
import { applyCatalogUpdate, catalogRecordAt, checkCatalogUpdates, mergeCatalogs, moveToCustom, referenceFindings } from "./catalogs.js";
|
|
39
|
-
import { describeTableCell, mergeTableCells, parseTableCellPath, readTableStyle, setTableCellStyle, setTableStyle, splitTableCell } from "./table-options.js";
|
|
39
|
+
import { describeTableCell, mergeTableCells, parseTableCellPath, readTableAlt, readTableStyle, setTableAlt, setTableCellStyle, setTableStyle, splitTableCell } from "./table-options.js";
|
|
40
40
|
|
|
41
41
|
/** The sections a panel can show. `selection` and `table` follow the current selection; the rest follow the deck or the current slide. */
|
|
42
42
|
export const DESIGN_CONTROL_SECTIONS = Object.freeze(["look", "background", "header-footer", "brand", "layout-options", "info", "selection", "image", "table", "slide-content", "catalog"]);
|
|
@@ -1158,8 +1158,19 @@ export function createDesignControls(container, options = {}) {
|
|
|
1158
1158
|
const align = selectField("cell-align", "Cell text alignment", { empty: "Default", onChange: (value) => run(() => setTableCellStyle(editor, dynamic.tablePath, dynamic.cell, { align: value === "" ? null : value }), "Cell alignment changed.") });
|
|
1159
1159
|
align.setOptions(["left", "center", "right"].map((value) => ({ value, label: titleCase(value) })));
|
|
1160
1160
|
const cellBox = h("div", { class: "opf-dc-cell" }, cellLabel, columns.wrap, rows.wrap, join.wrap, h("div", { class: "opf-dc-actions" }, merge, split), fill.wrap, align.wrap);
|
|
1161
|
-
|
|
1162
|
-
|
|
1161
|
+
// FA-27: the table's text alternative, like a chart's. It belongs to the table, so a dataset-backed table has it too.
|
|
1162
|
+
const alt = textField("table-alt", "Alt text", {
|
|
1163
|
+
help: "What the table shows: its point and the key numbers, not \"a table\". Optional, because a screen reader reads a table cell by cell. Press Enter to apply.",
|
|
1164
|
+
placeholder: "What the table shows",
|
|
1165
|
+
onCommit: (value) => run(() => setTableAlt(editor, dynamic.tablePath, { alt: value }), "Table alt text changed."),
|
|
1166
|
+
});
|
|
1167
|
+
alt.input.setAttribute("aria-label", "Table alt text");
|
|
1168
|
+
const decorative = checkField("table-decorative", "Decorative (no alt text)", {
|
|
1169
|
+
onChange: (checked) => run(() => setTableAlt(editor, dynamic.tablePath, { decorative: checked }), checked ? "Table marked decorative." : "The table is no longer decorative."),
|
|
1170
|
+
});
|
|
1171
|
+
const styleControls = [headerStyle.wrap, banding.wrap, borders.wrap, resetStyle, cellBox];
|
|
1172
|
+
body.append(summary, alt.wrap, decorative.wrap, ...styleControls);
|
|
1173
|
+
dynamic.table = { details, summary, alt, decorative, styleControls, headerStyle, banding, borders, cellBox, cellLabel, columns, rows, merge, split, fill, align, join };
|
|
1163
1174
|
}
|
|
1164
1175
|
|
|
1165
1176
|
// --- sync -----------------------------------------------------------------------------------------
|
|
@@ -1210,12 +1221,21 @@ export function createDesignControls(container, options = {}) {
|
|
|
1210
1221
|
const tablePath = parsed?.tablePath ?? (content?.key === "table" ? `${blockPath}.table` : undefined);
|
|
1211
1222
|
dynamic.tablePath = tablePath;
|
|
1212
1223
|
dynamic.cell = parsed?.cell;
|
|
1213
|
-
// A table that shows a shared dataset (RR-54) has no cell styles or merges: the
|
|
1224
|
+
// A table that shows a shared dataset (RR-54) has no cell styles or merges: the style and merge controls are for inline tables.
|
|
1225
|
+
// Its alt text (FA-27) belongs to the table and not to the data, so the panel still opens for it.
|
|
1214
1226
|
const tableValue = tablePath ? editor.get(tablePath) : undefined;
|
|
1215
|
-
|
|
1227
|
+
const inline = Array.isArray(tableValue?.rows);
|
|
1228
|
+
tableUi.details.hidden = !tablePath || !(inline || typeof tableValue?.dataset === "string");
|
|
1216
1229
|
if (tablePath && !tableUi.details.hidden) {
|
|
1217
|
-
const table = tableValue;
|
|
1218
1230
|
tableUi.summary.textContent = `Table (${tablePath})`;
|
|
1231
|
+
const textAlternative = readTableAlt(tableValue);
|
|
1232
|
+
tableUi.alt.set(textAlternative.alt);
|
|
1233
|
+
tableUi.alt.input.disabled = textAlternative.decorative;
|
|
1234
|
+
tableUi.decorative.set(textAlternative.decorative);
|
|
1235
|
+
for (const node of tableUi.styleControls) node.hidden = !inline;
|
|
1236
|
+
}
|
|
1237
|
+
if (tablePath && !tableUi.details.hidden && inline) {
|
|
1238
|
+
const table = tableValue;
|
|
1219
1239
|
const style = readTableStyle(document_, tablePath);
|
|
1220
1240
|
dynamic.tableStyle = style.preset === "custom" ? { header: "theme", banding: false, borders: "theme" } : { header: style.header, banding: style.banding, borders: style.borders };
|
|
1221
1241
|
tableUi.headerStyle.set(style.header);
|
package/dist/export.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ export interface ExportOptions {
|
|
|
42
42
|
includeHidden?: boolean;
|
|
43
43
|
/** "vector" (default): selectable text and vector shapes; "raster": an image per slide. */
|
|
44
44
|
pdfMode?: "vector" | "raster";
|
|
45
|
+
/**
|
|
46
|
+
* Raster PDF only: the pdf-lib module (`import * as pdfLib from "pdf-lib"`). The renderer's `export-browser` entry imports no PDF library (opf-render 0.16), so the host passes it;
|
|
47
|
+
* without it a `pdfMode: "raster"` PDF rejects with the renderer's `converter-missing`. Vector PDF, PNG and SVG need no option.
|
|
48
|
+
*/
|
|
49
|
+
pdfLib?: { PDFDocument: { create(options?: { updateMetadata?: boolean }): Promise<any> } };
|
|
45
50
|
/** PNG pixel density (and the raster PDF's), 1 to 4; default 2. */
|
|
46
51
|
scale?: number;
|
|
47
52
|
/** The options the host draws its preview with (`catalogs`, `date`, ...); the fonts go in `fonts`. */
|
|
@@ -72,6 +77,6 @@ export interface ExportResult {
|
|
|
72
77
|
}
|
|
73
78
|
/**
|
|
74
79
|
* Draw the deck with the renderer the preview uses and convert it. Rejects with an error whose `code` is `export-aborted`, `export-fonts-unlicensed` (the license rule left the PDF converter no face for some text), `export-no-slides`,
|
|
75
|
-
* `export-format`, `export-unavailable` (PDF and PNG need the renderer's `export-browser` entry), `fonts-unavailable` or a renderer code.
|
|
80
|
+
* `export-format`, `export-unavailable` (PDF and PNG need the renderer's `export-browser` entry), `fonts-unavailable` or a renderer code (`converter-missing` for a raster PDF without `pdfLib`).
|
|
76
81
|
*/
|
|
77
82
|
export declare function exportDeck(deck: unknown, options: ExportOptions): Promise<ExportResult>;
|
package/dist/export.js
CHANGED
|
@@ -124,6 +124,9 @@ async function loadConverters(options) {
|
|
|
124
124
|
* - `format`: "pdf", "png" or "svg" (required).
|
|
125
125
|
* - `slides`: "current" (with `slideIndex`), "all" (default; hidden slides only with `includeHidden`) or an array of slide numbers.
|
|
126
126
|
* - `pdfMode`: "vector" (default: selectable text, vector shapes) or "raster" (each slide an image).
|
|
127
|
+
* - `pdfLib`: for `pdfMode: "raster"` only, the pdf-lib module (`import * as pdfLib from "pdf-lib"`). The renderer's `export-browser` entry imports no PDF library (RR-63, opf-render 0.16),
|
|
128
|
+
* so a host that offers the raster PDF imports pdf-lib itself; without it a raster PDF rejects with the renderer's `converter-missing` (its `details.install` names the package).
|
|
129
|
+
* Vector PDF, PNG and SVG need no option.
|
|
127
130
|
* - `scale`: PNG pixel density, 1 to 4 (default 2); also the raster PDF's density.
|
|
128
131
|
* - `renderOptions`: the render options the host draws with (`catalogs`, `date`, ...), the same as its preview.
|
|
129
132
|
* - `catalogs`: the host's registered catalogs (`Catalog[]`, merged with `renderOptions.catalogs`). The deck is embedded first (core's
|
|
@@ -137,7 +140,7 @@ async function loadConverters(options) {
|
|
|
137
140
|
* `export-fonts-unlicensed` (the converter's error is its `cause`).
|
|
138
141
|
* - `signal`, `onProgress({ stage, done, total, message })`, `onDiagnostic(diagnostic)`.
|
|
139
142
|
* Resolves `{ download, files, diagnostics, slides }`. Rejects with an error whose `code` is `export-aborted`,
|
|
140
|
-
* `export-fonts-unlicensed`, `export-no-slides`, `export-unavailable`, `fonts-unavailable` or a renderer code.
|
|
143
|
+
* `export-fonts-unlicensed`, `export-no-slides`, `export-unavailable`, `fonts-unavailable` or a renderer code (`converter-missing` for a raster PDF without `pdfLib`).
|
|
141
144
|
*/
|
|
142
145
|
export async function exportDeck(input, options = {}) {
|
|
143
146
|
const format = EXPORT_FORMATS[options.format];
|
|
@@ -213,6 +216,8 @@ export async function exportDeck(input, options = {}) {
|
|
|
213
216
|
metadata,
|
|
214
217
|
signal,
|
|
215
218
|
fontData: pdfFontData(embeddedFonts),
|
|
219
|
+
// RR-63: the renderer's browser entry imports no pdf-lib; raster mode takes the module the host imported.
|
|
220
|
+
...(options.pdfMode === "raster" && options.pdfLib ? { pdfLib: options.pdfLib } : {}),
|
|
216
221
|
...(options.fallbackFamily ? { defaultFontFamily: options.fallbackFamily } : {}),
|
|
217
222
|
onDiagnostic: (diagnostic) => { const described = describeDiagnostic(diagnostic, "pdf"); if (keep(described)) note(described); },
|
|
218
223
|
onProgress: ({ page, pages }) => progress("convert", page, pages, `Page ${page} of ${pages}`),
|
package/dist/find-replace.js
CHANGED
|
@@ -107,6 +107,7 @@ function payloadFields(fields, state, base, payload, prefix = "") {
|
|
|
107
107
|
}));
|
|
108
108
|
}
|
|
109
109
|
if (isObject(payload.chart)) field(fields, state, at("chart", "alt"), payload.chart.alt, `${prefix}Chart alt text`, "alt");
|
|
110
|
+
if (isObject(payload.table)) field(fields, state, at("table", "alt"), payload.table.alt, `${prefix}Table alt text`, "alt");
|
|
110
111
|
const table = payload.table;
|
|
111
112
|
if (isObject(table)) {
|
|
112
113
|
if (Array.isArray(table.columns)) table.columns.forEach((cell, c) => (isObject(cell) && !("value" in cell) ? field(fields, state, at("table", "columns", String(c), "name"), cell.name, `${prefix}Table header ${c + 1}`, "table") : cellFields(fields, state, at("table", "columns", String(c)), cell, `${prefix}Table header ${c + 1}`)));
|
package/dist/font-gate.js
CHANGED
|
@@ -13,17 +13,6 @@ export const FONTS_UNAVAILABLE = "fonts-unavailable";
|
|
|
13
13
|
|
|
14
14
|
const GATE = Symbol("opf-editor.fontGate");
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* The fonts a preview draws with: the handle's text measurement and nothing else. A full handle also lists `embeddedFonts`, which
|
|
18
|
-
* the renderer writes into every SVG it returns (an export needs that, so the SVG carries its faces); a preview inside a page
|
|
19
|
-
* that already loaded the faces would repeat megabytes of base64 in every canvas, thumbnail and panel preview. The measurement
|
|
20
|
-
* is read from the handle at each use, so faces it loads later still measure.
|
|
21
|
-
*/
|
|
22
|
-
export function previewFonts(fonts) {
|
|
23
|
-
if (!fonts || typeof fonts !== "object") return undefined;
|
|
24
|
-
return { get textMeasurement() { return fonts.textMeasurement; } };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
16
|
/**
|
|
28
17
|
* The gate over a fonts handle: the two calls the canvas and the export need, with the editor's error contract.
|
|
29
18
|
*
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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";
|
|
@@ -523,8 +524,19 @@ export function setCatalogId(editor, path, catalogKind, id, meta = {}) {
|
|
|
523
524
|
const current = editor.get(path);
|
|
524
525
|
const value = catalogReferenceValue(current, catalogId);
|
|
525
526
|
const { catalogs: _catalogs, presentation: _presentation, ...commitMeta } = meta;
|
|
526
|
-
|
|
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, {
|
|
527
538
|
...commitMeta,
|
|
539
|
+
rejectInvalid: true,
|
|
528
540
|
catalogKind,
|
|
529
541
|
catalogId,
|
|
530
542
|
source: commitMeta.source ?? "catalog-control"
|
|
@@ -589,13 +601,13 @@ export function createCatalogSelect(editor, options) {
|
|
|
589
601
|
const current = editor.get(path);
|
|
590
602
|
select.value = typeof current === "object" && current ? current.id : current ?? "";
|
|
591
603
|
};
|
|
592
|
-
const onChange = () => {
|
|
593
|
-
setCatalogId(editor, path, catalogKind, select.value, {
|
|
604
|
+
const onChange = () => commitCatalogControl(select, options?.onError, updateValue, () => {
|
|
605
|
+
return setCatalogId(editor, path, catalogKind, select.value, {
|
|
594
606
|
catalogs: options?.catalogs,
|
|
595
607
|
presentation: options?.presentation,
|
|
596
608
|
source: "catalog-select"
|
|
597
609
|
});
|
|
598
|
-
};
|
|
610
|
+
});
|
|
599
611
|
const unsubscribe = editor.subscribe(updateValue);
|
|
600
612
|
|
|
601
613
|
select.addEventListener?.("change", onChange);
|
package/dist/json-options.js
CHANGED
|
@@ -2,7 +2,7 @@ import { checkFormat, firstErrorMessage } from "./checks.js";
|
|
|
2
2
|
import { listCatalogRecords } from "./catalogs.js";
|
|
3
3
|
import { schemaAtPath, schemaVariants } from "./schema.js";
|
|
4
4
|
import { applyEdits, findNodeAtLocation, findNodeAtOffset, getNodePath, modify, parseTree } from "jsonc-parser";
|
|
5
|
-
import { populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
5
|
+
import { layoutSlotSummary, layoutSlotTypes, populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
6
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
7
|
const ROOT_REFERENCES = { narrative: "narratives", audience: "audiences", purpose: "purposes", tone: "tones" };
|
|
8
8
|
const DESIGN_REFERENCES = { theme: "themes", colorScheme: "colorSchemes", fontScheme: "fontSchemes" };
|
|
@@ -21,10 +21,9 @@ function catalogAtPath(path) {
|
|
|
21
21
|
}
|
|
22
22
|
// Compare declared placeholders, including multiplicity. This describes a layout
|
|
23
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.
|
|
24
25
|
function layoutTypes(record) {
|
|
25
|
-
|
|
26
|
-
return undefined;
|
|
27
|
-
return record.placeholders.map(p => p.type).sort();
|
|
26
|
+
return layoutSlotTypes(record)?.sort();
|
|
28
27
|
}
|
|
29
28
|
function placeholderSummary(types) {
|
|
30
29
|
if (!types)
|
|
@@ -57,8 +56,10 @@ function describeOptions(choices, records, current, catalog) {
|
|
|
57
56
|
if (catalog !== "layouts")
|
|
58
57
|
continue;
|
|
59
58
|
const types = layoutTypes(option.value === current ? currentRecord : records.get(String(option.value)));
|
|
60
|
-
|
|
61
|
-
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)}` : "");
|
|
62
63
|
const same = types && currentTypes && signature(types) === signature(currentTypes);
|
|
63
64
|
const equivalent = types && currentTypes && signature(compatible(types)) === signature(compatible(currentTypes));
|
|
64
65
|
const sameKinds = types && currentTypes && signature([...new Set(compatible(types))]) === signature([...new Set(compatible(currentTypes))]);
|
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,6 +57,7 @@ export function createOPFReactComponents(React) {
|
|
|
56
57
|
catalogKind,
|
|
57
58
|
label,
|
|
58
59
|
catalogOptions,
|
|
60
|
+
onError,
|
|
59
61
|
selectProps = {},
|
|
60
62
|
...props
|
|
61
63
|
}) {
|
|
@@ -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
|
},
|
package/dist/slide-manager.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
addSection, addSlide, duplicateSlides, hasSections, listSections, moveSection, moveSlides, moveSlidesBy, removeSection, removeSlides, renameSection, setHidden, setSection, slideTitle,
|
|
8
8
|
} from "./slides.js";
|
|
9
9
|
import { listSwitchOptions } from "./switches.js";
|
|
10
|
+
import { layoutSlotSummary } from "./layout-placeholders.js";
|
|
10
11
|
|
|
11
12
|
let instances = 0;
|
|
12
13
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
@@ -503,7 +504,8 @@ export function createSlideManager(container, options) {
|
|
|
503
504
|
};
|
|
504
505
|
const describe = () => {
|
|
505
506
|
const choice = choices.find((entry) => entry.id === select.value);
|
|
506
|
-
|
|
507
|
+
// FA-26: nested placeholder groups show with their mode, for example `title, column (text, text), chart`.
|
|
508
|
+
const slots = layoutSlotSummary(choice?.record);
|
|
507
509
|
detail.textContent = choice ? `${choice.record?.description ?? ""}${slots ? ` Slots: ${slots}.` : choice.record?.placeholders ? " An empty slide." : ""}`.trim() : "No layout matches.";
|
|
508
510
|
};
|
|
509
511
|
const form = el("form", { method: "dialog" },
|
package/dist/svelte.d.ts
CHANGED
package/dist/svelte.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commitCatalogControl } from "./catalog-control.js";
|
|
1
2
|
import {
|
|
2
3
|
createSvgTraceBinding,
|
|
3
4
|
getCatalogOptions,
|
|
@@ -59,11 +60,11 @@ export function opfCatalogSelect(node, params) {
|
|
|
59
60
|
const value = editor.get(path);
|
|
60
61
|
node.value = value && typeof value === "object" ? value.id : value ?? "";
|
|
61
62
|
};
|
|
62
|
-
const onChange = () => setCatalogId(editor, path, catalogKind, node.value, {
|
|
63
|
+
const onChange = () => commitCatalogControl(node, nextParams.onError, sync, () => setCatalogId(editor, path, catalogKind, node.value, {
|
|
63
64
|
catalogs: nextParams.catalogs,
|
|
64
65
|
presentation: nextParams.presentation,
|
|
65
66
|
source: "svelte-catalog-select"
|
|
66
|
-
});
|
|
67
|
+
}));
|
|
67
68
|
const unsubscribe = editor.subscribe(sync);
|
|
68
69
|
|
|
69
70
|
node.addEventListener?.("change", onChange);
|
package/dist/switches.js
CHANGED
|
@@ -13,7 +13,7 @@ import { chartOptionTarget } from "@openpresentation/opf/composition";
|
|
|
13
13
|
import { createValuePatch, applyJsonPatch, getValueAtPath, opfPathToJsonPointer, splitOpfPath } from "./index.js";
|
|
14
14
|
import { createContentBlock, prepareBlockReplace } from "./blocks.js";
|
|
15
15
|
import { checkedDocument, designPatches, fail, same } from "./edit-helpers.js";
|
|
16
|
-
import { populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
16
|
+
import { layoutSlotTypes, populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
17
17
|
import { blockConversionTargets, prepareBlockConversion } from "./block-convert.js";
|
|
18
18
|
import { checkFormat } from "./checks.js";
|
|
19
19
|
import { listCatalogRecords, mergeCatalogs } from "./catalogs.js";
|
|
@@ -273,7 +273,8 @@ export function prepareDimensionSwitch(presentation, dimension, value, options =
|
|
|
273
273
|
if (withRecord.slides[slideIndex].layout !== choice.reference) {
|
|
274
274
|
const layoutOps = createValuePatch(withRecord, ["slides", String(slideIndex), "layout"], choice.reference);
|
|
275
275
|
const swapped = applyJsonPatch(withRecord, layoutOps);
|
|
276
|
-
|
|
276
|
+
// FA-26: a record's placeholder groups contribute their leaf regions, in reading order.
|
|
277
|
+
const types = layoutSlotTypes(record);
|
|
277
278
|
// Add the blank payloads the layout declares, as the JSON editor's layout choice does.
|
|
278
279
|
const populated = types ? populateLayoutPlaceholders(swapped, slideIndex, types) : swapped;
|
|
279
280
|
patches = same(populated.slides[slideIndex], swapped.slides[slideIndex])
|
package/dist/table-options.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface TableCellState extends TableCellAddress {
|
|
|
37
37
|
rows: number;
|
|
38
38
|
}
|
|
39
39
|
export interface PreparedTableChange {
|
|
40
|
-
action: "merge" | "split" | "style" | "table-style";
|
|
40
|
+
action: "merge" | "split" | "style" | "table-style" | "table-alt";
|
|
41
41
|
tablePath: string;
|
|
42
42
|
presentation: unknown;
|
|
43
43
|
patches: JsonPatchOperation[];
|
|
@@ -72,6 +72,11 @@ export declare function mergeTableCells(editor: EditorSession, tablePath: string
|
|
|
72
72
|
export declare function splitTableCell(editor: EditorSession, tablePath: string, cell: TableCellAddress, meta?: Record<string, unknown>): TableChange;
|
|
73
73
|
export declare function setTableCellStyle(editor: EditorSession, tablePath: string, cells: TableCellAddress | TableCellAddress[], style: Record<string, unknown> | null, meta?: Record<string, unknown>): TableChange;
|
|
74
74
|
export declare function setTableStyle(editor: EditorSession, tablePath: string, preset: keyof typeof TABLE_STYLE_PRESETS | Partial<TableStyle>, meta?: Record<string, unknown>): TableChange;
|
|
75
|
+
/** FA-27: the table's text alternative as form state: `alt` (empty when absent or decorative) and `decorative` (the empty alt). */
|
|
76
|
+
export declare function readTableAlt(table: unknown): { alt: string; decorative: boolean };
|
|
77
|
+
/** FA-27: change the table's text alternative. `alt` is trimmed and an empty string or null removes it; `decorative: true` writes the empty alt and wins over `alt`; `decorative: false` removes an empty alt. Works on inline and dataset-backed tables. */
|
|
78
|
+
export declare function prepareTableAlt(presentation: unknown, tablePath: string, change: { alt?: string | null; decorative?: boolean }): PreparedTableChange;
|
|
79
|
+
export declare function setTableAlt(editor: EditorSession, tablePath: string, change: { alt?: string | null; decorative?: boolean }, meta?: Record<string, unknown>): TableChange;
|
|
75
80
|
/** {@link readTableStyle} for a table object. */
|
|
76
81
|
export declare function readTableStyleOfTable(table: unknown): (TableStyle | { header: "custom"; banding: "custom"; borders: "custom" }) & { preset: keyof typeof TABLE_STYLE_PRESETS | "custom" };
|
|
77
82
|
/** Apply a table style to a table object in place (the structure operations use it to keep banding, header fill and borders correct). */
|
package/dist/table-options.js
CHANGED
|
@@ -394,6 +394,44 @@ export function readTableStyleOfTable(table) {
|
|
|
394
394
|
// Table structure (RR-24): insert, delete, move and sort rows and columns, the header row, cell values.
|
|
395
395
|
export * from "./table-structure.js";
|
|
396
396
|
|
|
397
|
+
// --- text alternative (FA-27) -----------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The table's text alternative as form state, like a chart's: `alt` is the text (empty when absent or decorative) and `decorative`
|
|
401
|
+
* is the empty alt, a reviewed choice and not a missing one.
|
|
402
|
+
*/
|
|
403
|
+
export function readTableAlt(table) {
|
|
404
|
+
return { alt: typeof table?.alt === "string" ? table.alt : "", decorative: table?.alt === "" };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Compute the patch that changes the table's text alternative. `change` is `{ alt?, decorative? }`: `alt` is trimmed and an empty string
|
|
409
|
+
* or `null` removes it, `decorative: true` writes the empty alt (and wins over `alt`), `decorative: false` removes an empty alt. It works
|
|
410
|
+
* on an inline table and on a table that shows a shared dataset, because `alt` belongs to the table and not to its data. The document
|
|
411
|
+
* is not modified.
|
|
412
|
+
*/
|
|
413
|
+
export function prepareTableAlt(presentation, tablePath, change) {
|
|
414
|
+
const before = checkFormat(presentation);
|
|
415
|
+
const parts = splitOpfPath(tablePath);
|
|
416
|
+
const table = getValueAtPath(presentation, parts);
|
|
417
|
+
if (!isObject(table) || (!Array.isArray(table.rows) && typeof table.dataset !== "string")) throw fail("table-not-found", "Choose a table (a path ending in .table).", { tablePath });
|
|
418
|
+
if (!isObject(change)) throw fail("invalid-table-option", "A table alt change is { alt?, decorative? }.", { change });
|
|
419
|
+
let wanted;
|
|
420
|
+
if (change.decorative === true) wanted = "";
|
|
421
|
+
else if (change.alt !== undefined) wanted = typeof change.alt === "string" && change.alt.trim() ? change.alt.trim() : undefined;
|
|
422
|
+
else if (change.decorative === false && table.alt === "") wanted = undefined;
|
|
423
|
+
else wanted = table.alt;
|
|
424
|
+
const pointer = opfPathToJsonPointer([...parts, "alt"]);
|
|
425
|
+
const present = Object.hasOwn(table, "alt");
|
|
426
|
+
const patches = [];
|
|
427
|
+
if (wanted === undefined) {
|
|
428
|
+
if (present) patches.push({ op: "remove", path: pointer });
|
|
429
|
+
} else if (!present) patches.push({ op: "add", path: pointer, value: wanted });
|
|
430
|
+
else if (table.alt !== wanted) patches.push({ op: "replace", path: pointer, value: wanted });
|
|
431
|
+
const next = checkedDocument(presentation, patches, before);
|
|
432
|
+
return { action: "table-alt", tablePath: parts.join("."), changed: patches.length > 0, patches, presentation: clone(next) };
|
|
433
|
+
}
|
|
434
|
+
|
|
397
435
|
// --- session forms ----------------------------------------------------------------------------
|
|
398
436
|
|
|
399
437
|
function commit(editor, prepared, meta = {}) {
|
|
@@ -423,6 +461,11 @@ export function setTableCellStyle(editor, tablePath, cells, style, meta = {}) {
|
|
|
423
461
|
checkEditor(editor);
|
|
424
462
|
return commit(editor, prepareTableCellStyle(editor.presentation, tablePath, cells, style), meta);
|
|
425
463
|
}
|
|
464
|
+
/** Change the table's text alternative as one undoable transaction. See {@link prepareTableAlt}. */
|
|
465
|
+
export function setTableAlt(editor, tablePath, change, meta = {}) {
|
|
466
|
+
checkEditor(editor);
|
|
467
|
+
return commit(editor, prepareTableAlt(editor.presentation, tablePath, change), meta);
|
|
468
|
+
}
|
|
426
469
|
/** Apply a table style as one undoable transaction. See {@link prepareTableStyle}. */
|
|
427
470
|
export function setTableStyle(editor, tablePath, preset, meta = {}) {
|
|
428
471
|
checkEditor(editor);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -226,6 +226,7 @@
|
|
|
226
226
|
"test:code-browser": "node test/code-browser.mjs",
|
|
227
227
|
"test:selection-browser": "node test/selection-browser.mjs",
|
|
228
228
|
"test:click-entry-browser": "node test/click-entry-browser.mjs",
|
|
229
|
+
"test:canvas-a11y-browser": "node test/canvas-a11y-browser.mjs",
|
|
229
230
|
"test:metric-browser": "node test/metric-browser.mjs",
|
|
230
231
|
"test:rich-input-browser": "node test/rich-input-browser.mjs",
|
|
231
232
|
"test:json": "npm run build && node test/json-source.mjs && node test/json-options.mjs",
|
|
@@ -257,11 +258,11 @@
|
|
|
257
258
|
"@codemirror/state": "6.7.6",
|
|
258
259
|
"@codemirror/view": "6.43.13",
|
|
259
260
|
"@lezer/highlight": "1.2.4",
|
|
260
|
-
"@openpresentation/opf": "^0.
|
|
261
|
+
"@openpresentation/opf": "^0.16.0",
|
|
261
262
|
"jsonc-parser": "3.3.1"
|
|
262
263
|
},
|
|
263
264
|
"peerDependencies": {
|
|
264
|
-
"@openpresentation/opf-render": "^0.
|
|
265
|
+
"@openpresentation/opf-render": "^0.16.0"
|
|
265
266
|
},
|
|
266
267
|
"peerDependenciesMeta": {
|
|
267
268
|
"@openpresentation/opf-render": {
|
|
@@ -269,6 +270,12 @@
|
|
|
269
270
|
}
|
|
270
271
|
},
|
|
271
272
|
"devDependencies": {
|
|
273
|
+
"@expo-google-fonts/arimo": "0.4.3",
|
|
274
|
+
"@expo-google-fonts/caladea": "0.4.2",
|
|
275
|
+
"@expo-google-fonts/cousine": "0.4.3",
|
|
276
|
+
"@expo-google-fonts/gelasio": "0.4.1",
|
|
277
|
+
"@expo-google-fonts/noto-color-emoji": "0.4.6",
|
|
278
|
+
"@expo-google-fonts/noto-emoji": "0.4.7",
|
|
272
279
|
"@expo-google-fonts/noto-naskh-arabic": "0.4.5",
|
|
273
280
|
"@expo-google-fonts/noto-nastaliq-urdu": "0.4.2",
|
|
274
281
|
"@expo-google-fonts/noto-sans": "0.4.2",
|
|
@@ -303,15 +310,17 @@
|
|
|
303
310
|
"@expo-google-fonts/noto-sans-thai": "0.4.2",
|
|
304
311
|
"@expo-google-fonts/noto-serif-hebrew": "0.4.2",
|
|
305
312
|
"@expo-google-fonts/noto-serif-tibetan": "0.4.2",
|
|
306
|
-
"@
|
|
307
|
-
"@
|
|
313
|
+
"@expo-google-fonts/roboto": "0.4.3",
|
|
314
|
+
"@expo-google-fonts/roboto-mono": "0.4.2",
|
|
315
|
+
"@expo-google-fonts/stix-two-math": "0.4.0",
|
|
316
|
+
"@expo-google-fonts/tinos": "0.4.2",
|
|
317
|
+
"@openpresentation/opf-pptx": "^0.16.0",
|
|
318
|
+
"@openpresentation/opf-render": "^0.16.0",
|
|
308
319
|
"axe-core": "4.13.0",
|
|
309
320
|
"esbuild": "0.28.2",
|
|
310
321
|
"jszip": "3.10.2",
|
|
322
|
+
"pdf-lib": "1.17.1",
|
|
311
323
|
"pdfjs-dist": "6.3.289",
|
|
312
|
-
"playwright": "1.63.0"
|
|
313
|
-
"@expo-google-fonts/noto-color-emoji": "0.4.6",
|
|
314
|
-
"@expo-google-fonts/noto-emoji": "0.4.7",
|
|
315
|
-
"@expo-google-fonts/stix-two-math": "0.4.0"
|
|
324
|
+
"playwright": "1.63.0"
|
|
316
325
|
}
|
|
317
326
|
}
|