@openpresentation/opf-editor 0.20.3 → 0.22.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 +8 -4
- package/dist/blocks.js +4 -1
- package/dist/canvas.js +6 -0
- package/dist/checks.js +1 -1
- package/dist/data-grid.js +16 -1
- package/dist/design-controls.js +45 -9
- package/dist/find-replace.js +4 -1
- package/dist/image-crop.d.ts +2 -2
- package/dist/image-crop.js +30 -9
- package/dist/image-cropper.js +6 -2
- package/dist/index.js +2 -1
- package/dist/persistence.d.ts +3 -3
- package/dist/persistence.js +23 -4
- package/dist/slides.d.ts +5 -1
- package/dist/table-options.d.ts +1 -1
- package/dist/table-options.js +4 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
|
-
##
|
|
5
|
+
## Install the editor and optional export dependencies (RR-63)
|
|
6
6
|
|
|
7
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
8
|
|
|
@@ -110,13 +110,16 @@ Chart options (RR-35): `@openpresentation/opf-editor/chart-options` edits a char
|
|
|
110
110
|
|
|
111
111
|
Chart highlight (FA-14): the same module edits `chart.highlight`: `setChartOptions(editor, chartPath, { highlight: { series: ["Revenue"], categories: ["Q3"] } })` replaces each listed part (an empty list removes it, `null` removes the field), refuses a name the chart does not have, and writes only what the chart type can highlight (core `chartOptionSupport(...).highlight`). `readChartOptions(chart, document)` returns the `choices` (plotted series and category labels, resolved through the document for a dataset chart), and the panel lists them as two groups of checkboxes ("Highlight series", "Highlight categories").
|
|
112
112
|
|
|
113
|
+
Chart colors (known gap): core 0.21 added `chart.colors` (a position-matched list of series or category colors, as a hex color, a scheme slot or a role), and opf-pptx 0.22.0 exports it (the opf-render preview follows once opf-render#256 lands), but the editor has no control for it yet. The chart options panel edits `highlight` as its only color-related setting (the two checkbox groups above), and `setChartOptions` does not accept `colors`. Until a control exists, set `chart.colors` in the document directly (the JSON control or a patch).
|
|
114
|
+
|
|
113
115
|
```js
|
|
114
116
|
import { createCanvasEditor } from '@openpresentation/opf-editor/canvas';
|
|
117
|
+
import { saveDocument } from '@openpresentation/opf-editor';
|
|
115
118
|
|
|
116
119
|
const canvas = createCanvasEditor(container, {
|
|
117
120
|
presentation,
|
|
118
121
|
fonts, // the renderer's fonts handle: const fonts = await loadFonts({ faces, ... })
|
|
119
|
-
onCommit: ({ editor }) => saveDocument(editor.
|
|
122
|
+
onCommit: ({ editor }) => console.log(saveDocument(editor).document),
|
|
120
123
|
});
|
|
121
124
|
await canvas.ready;
|
|
122
125
|
// canvas.destroy() when unmounting.
|
|
@@ -430,9 +433,10 @@ setTableStyle(editor, "slides.4.blocks.1.table", "banded"); //
|
|
|
430
433
|
mergeTableCells(editor, "slides.4.blocks.1.table", { section: "body", row: 1, column: 0 }, { colSpan: 3 });
|
|
431
434
|
splitTableCell(editor, "slides.4.blocks.1.table", { section: "body", row: 1, column: 0 });
|
|
432
435
|
setTableCellStyle(editor, "slides.4.blocks.1.table", [{ section: "header", column: 0 }], { fill: "accent", align: "center" });
|
|
436
|
+
setTableCellStyle(editor, "slides.4.blocks.1.table", { section: "body", row: 2, column: 3 }, { bold: true }); // a bold number cell keeps its column format and alignment; { bold: null } removes it
|
|
433
437
|
```
|
|
434
438
|
|
|
435
|
-
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.
|
|
439
|
+
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, bold, padding, values and merges are kept. To bold one cell, set `bold: true` on its style with `setTableCellStyle` (the panel's Bold checkbox does): the cell stays a plain value in a numeric column, so it keeps the column's number `format` and alignment, which rich runs would drop (needs core with `TableCellStyle.bold`). 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.
|
|
436
440
|
|
|
437
441
|
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.
|
|
438
442
|
|
|
@@ -520,7 +524,7 @@ const selectionControls = createDesignControls(contentPanel, { editor, getSlideI
|
|
|
520
524
|
controls.refresh(); // when the slide or the selection changes
|
|
521
525
|
```
|
|
522
526
|
|
|
523
|
-
Sections: `look` (theme, color scheme, font scheme, language, this slide's layout), `background` (type, theme slot, solid color, gradient with its stops, image with fit, focus, alt text, recolor and overlay, any of the 54 patterns, opacity; a draft until Apply, with Remove), `header-footer` (choose header or footer and a zone, then every part the zone supports: a text box with an "Insert value" menu that puts the slide number, slide count, section, organization, speaker or deck name at the cursor as a `{{ }}` variable (Enter commits, Shift+Enter starts a new line), an image with an "Insert logo" action (a shape and, for several organizations, an organization; it writes the logo reference as the zone's image), social profiles, and the current or fixed date and format, with a summary of the zones in use), `brand` (the organization logo: one image for every shape, or each of the four shapes for both, light and dark backgrounds, for the organization you choose; which organization's logo and shape covers, sections and picture bullets draw, or none; picture bullets, accent font, watermark), `layout-options` (alignment, direction, primary chart, content box, default image fit), `info` (narrative, tone, audience, socials), `selection` (content type with a loss report, list levels, group of metrics, group and ungroup, image to background or watermark, replacement, chart type), `image` (the selected image block's fit, focus, shape, corner radius, border, opacity, recolor, overlay, aspect ratio and placement), `catalog` (reference findings and the explicit "Update from catalog"), `slide-content` (blocks to regions and back, design images back into the content) and `table` (style, merge, split, cell fill and
|
|
527
|
+
Sections: `look` (theme, color scheme, font scheme, language, this slide's layout), `background` (type, theme slot, solid color, gradient with its stops, image with fit, focus, alt text, recolor and overlay, any of the 54 patterns, opacity; a draft until Apply, with Remove), `header-footer` (choose header or footer and a zone, then every part the zone supports: a text box with an "Insert value" menu that puts the slide number, slide count, section, organization, speaker or deck name at the cursor as a `{{ }}` variable (Enter commits, Shift+Enter starts a new line), an image with an "Insert logo" action (a shape and, for several organizations, an organization; it writes the logo reference as the zone's image), social profiles, and the current or fixed date and format, with a summary of the zones in use), `brand` (the organization logo: one image for every shape, or each of the four shapes for both, light and dark backgrounds, for the organization you choose; which organization's logo and shape covers, sections and picture bullets draw, or none; picture bullets, accent font, watermark), `layout-options` (alignment, direction, primary chart, content box, default image fit), `info` (narrative, tone, audience, socials), `selection` (content type with a loss report, list levels, group of metrics, group and ungroup, image to background or watermark, replacement, chart type), `image` (the selected image block's fit, focus, shape, corner radius, border, opacity, recolor, overlay, aspect ratio and placement), `catalog` (reference findings and the explicit "Update from catalog"), `slide-content` (blocks to regions and back, design images back into the content) and `table` (style, merge, split, cell fill, alignment and bold). The panel is native `details`, `fieldset`, `select`, checkbox and text controls with a label on each, so it works with the keyboard and with screen readers: groups open with Enter or Space, a select changes with the arrow keys, text fields commit on Enter or when you leave them, and results and refusals are announced in `role="status"` and `role="alert"` regions. A refused change (an invalid value, text a merge would hide) is explained, changes nothing, and puts the field back. "Applies to" switches the design controls between the whole presentation and the current slide, and a control shows when a slide value is "set on this slide" or "from the presentation". The panel offers only conversions that exist, shows what a conversion loses before you choose it, and lists why an unavailable one is unavailable.
|
|
524
528
|
|
|
525
529
|
In React (or Svelte, or anything else) mount it into a ref and destroy it on unmount; it needs no framework runtime:
|
|
526
530
|
|
package/dist/blocks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { splitOpfPath, opfPathToJsonPointer, getValueAtPath, applyJsonPatch } from './index.js';
|
|
2
2
|
import { firstErrorMessage, checkFormat } from "./checks.js";
|
|
3
|
+
import {collectReservedPresentationIds, remapSlideTreeIds} from "./presentation-ids.js";
|
|
3
4
|
const prefix=(a,b)=>a.length<=b.length&&a.every((part,i)=>part===b[i]);
|
|
4
5
|
|
|
5
6
|
/** Move one complete block. The insertion index refers to the destination before removal. */
|
|
@@ -120,7 +121,9 @@ export function prepareBlockReplace(presentation,path,block) {
|
|
|
120
121
|
/** Duplicate the entire block immediately after itself, preserving formatting and asset references. */
|
|
121
122
|
export function prepareBlockDuplicate(presentation,path) {
|
|
122
123
|
const {container,index,blocks}=checkedBlock(presentation,path);
|
|
123
|
-
|
|
124
|
+
const copy=structuredClone(blocks[index]);
|
|
125
|
+
remapSlideTreeIds({blocks:[copy]},new Set(collectReservedPresentationIds(presentation)));
|
|
126
|
+
return prepareBlockInsert(presentation,opfPathToJsonPointer(container),copy,index+1);
|
|
124
127
|
}
|
|
125
128
|
/** Delete a block and prune empty ancestor groups; a slide itself is never deleted. */
|
|
126
129
|
export function prepareBlockRemove(presentation,path) {
|
package/dist/canvas.js
CHANGED
|
@@ -1029,11 +1029,17 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
1029
1029
|
try {
|
|
1030
1030
|
renderFor();
|
|
1031
1031
|
} catch (error) {
|
|
1032
|
+
disposed = true;
|
|
1033
|
+
stopDrag?.();
|
|
1034
|
+
active?.rich?.destroy();
|
|
1035
|
+
imageCropper?.destroy();
|
|
1032
1036
|
unsubscribe();
|
|
1033
1037
|
resize.disconnect();
|
|
1038
|
+
win.visualViewport?.removeEventListener("resize", keepEditVisible);
|
|
1034
1039
|
richToolbar.destroy();
|
|
1035
1040
|
layoutHandles.destroy();
|
|
1036
1041
|
blockControls.destroy();
|
|
1042
|
+
if (frame) win.cancelAnimationFrame(frame);
|
|
1037
1043
|
root.remove();
|
|
1038
1044
|
throw error;
|
|
1039
1045
|
}
|
package/dist/checks.js
CHANGED
|
@@ -9,7 +9,7 @@ const EDIT_CATEGORIES = Object.freeze(["format", "opf/unresolved-reference", "op
|
|
|
9
9
|
|
|
10
10
|
/** Core's `ValidationReport` for the `format` category and the catalog reference rules: `valid`, `findings`, `counts`, `schemaValid`. */
|
|
11
11
|
export function checkFormat(presentation, options = {}) {
|
|
12
|
-
return validate(presentation, { only: EDIT_CATEGORIES, ...(options.catalogs
|
|
12
|
+
return validate(presentation, { only: EDIT_CATEGORIES, ...(options.catalogs !== undefined ? { catalogs: options.catalogs } : {}) });
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/** The error-severity findings of a report: what makes it invalid. */
|
package/dist/data-grid.js
CHANGED
|
@@ -914,10 +914,12 @@ export function createDataGrid(container, options = {}) {
|
|
|
914
914
|
function copyToClipboard(then) {
|
|
915
915
|
const { text, rows, columns } = selectionText();
|
|
916
916
|
const done = () => {
|
|
917
|
+
if (destroyed) return;
|
|
917
918
|
say(`Copied ${plural(rows, "row")} by ${plural(columns, "column")} as tab-separated text.`);
|
|
918
919
|
then?.();
|
|
919
920
|
};
|
|
920
921
|
const fallback = () => {
|
|
922
|
+
if (destroyed) return;
|
|
921
923
|
const previous = doc.activeElement;
|
|
922
924
|
const area = h("textarea", { "aria-hidden": "true", tabindex: -1 });
|
|
923
925
|
area.style.cssText = "position:fixed;opacity:0;left:-9999px";
|
|
@@ -1045,7 +1047,20 @@ export function createDataGrid(container, options = {}) {
|
|
|
1045
1047
|
// Browsers differ in firing copy for a focused cell that holds no selection, so the keys do the copy themselves.
|
|
1046
1048
|
event.preventDefault();
|
|
1047
1049
|
event.stopPropagation();
|
|
1048
|
-
|
|
1050
|
+
const source = editor.snapshot(), path = target.path;
|
|
1051
|
+
const edits = [];
|
|
1052
|
+
if (lower === "x") {
|
|
1053
|
+
const r = rect();
|
|
1054
|
+
for (let u = r.top; u <= r.bottom; u += 1)
|
|
1055
|
+
for (let c = r.left; c <= r.right; c += 1) {
|
|
1056
|
+
const cell = cellAt(u, c);
|
|
1057
|
+
if (cell && !cell.covered) edits.push({ ...addr(u, c), text: "" });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
copyToClipboard(lower === "x" ? () => {
|
|
1061
|
+
if (destroyed || editor.snapshot() !== source || !edits.length) return;
|
|
1062
|
+
attempt(() => setGridCells(editor, path, edits, opts()), `${plural(edits.length, "cell")} cleared.`);
|
|
1063
|
+
} : undefined);
|
|
1049
1064
|
return;
|
|
1050
1065
|
}
|
|
1051
1066
|
}
|
package/dist/design-controls.js
CHANGED
|
@@ -45,6 +45,7 @@ import { createContentControls } from "./content-controls.js";
|
|
|
45
45
|
import { IMAGE_EDGES, IMAGE_FITS, IMAGE_SHAPES, readImageTreatments, setImageTreatment } from "./image-options.js";
|
|
46
46
|
import { applyCatalogUpdate, catalogRecordAt, checkCatalogUpdates, mergeCatalogs, moveToCustom, referenceFindings } from "./catalogs.js";
|
|
47
47
|
import { describeTableCell, mergeTableCells, parseTableCellPath, readTableAlt, readTableStyle, setTableAlt, setTableCellStyle, setTableStyle, splitTableCell } from "./table-options.js";
|
|
48
|
+
import { OPFEditorError } from "./index.js";
|
|
48
49
|
|
|
49
50
|
/** The sections a panel can show. `selection` and `table` follow the current selection; the rest follow the deck or the current slide. */
|
|
50
51
|
export const DESIGN_CONTROL_SECTIONS = Object.freeze(["look", "background", "header-footer", "brand", "layout-options", "info", "selection", "image", "table", "slide-content", "catalog"]);
|
|
@@ -117,6 +118,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
117
118
|
const state = { scope: options.scope === "slide" ? "slide" : "deck" };
|
|
118
119
|
const syncs = [];
|
|
119
120
|
let destroyed = false;
|
|
121
|
+
let uploadEpoch = 0;
|
|
120
122
|
// After a refused change the field the person typed in is reset too, even though it still has focus.
|
|
121
123
|
let restoring = false;
|
|
122
124
|
// The background form is a draft until Apply; edits to it survive unrelated refreshes.
|
|
@@ -138,6 +140,10 @@ export function createDesignControls(container, options = {}) {
|
|
|
138
140
|
};
|
|
139
141
|
|
|
140
142
|
const root = h("div", { class: "opf-design-controls", "data-opf-component": "design-controls" });
|
|
143
|
+
const invalidateUpload = () => { uploadEpoch++; };
|
|
144
|
+
// Capture before the file input's own listener starts the next upload.
|
|
145
|
+
root.addEventListener("input", invalidateUpload, true);
|
|
146
|
+
root.addEventListener("change", invalidateUpload, true);
|
|
141
147
|
const statusEl = h("p", { class: "opf-dc-status", role: "status", "aria-live": "polite" });
|
|
142
148
|
const errorEl = h("p", { class: "opf-dc-error", role: "alert" });
|
|
143
149
|
|
|
@@ -328,9 +334,10 @@ export function createDesignControls(container, options = {}) {
|
|
|
328
334
|
|
|
329
335
|
/**
|
|
330
336
|
* Source text, file upload and alt text for one image. `apply(ref | null)` commits a typed source (null removes it);
|
|
331
|
-
* `
|
|
337
|
+
* `captureBuild()` freezes the target and form before reading a file. Its returned
|
|
338
|
+
* builder prepares the change after the asset patch, in the same undo step.
|
|
332
339
|
*/
|
|
333
|
-
function imageCluster(name, label, { apply,
|
|
340
|
+
function imageCluster(name, label, { apply, captureBuild, help }) {
|
|
334
341
|
const source = textField(`${name}-source`, `${label} source`, {
|
|
335
342
|
list: true,
|
|
336
343
|
placeholder: "asset:photo or https://…",
|
|
@@ -363,6 +370,18 @@ export function createDesignControls(container, options = {}) {
|
|
|
363
370
|
const file = input.files?.[0];
|
|
364
371
|
input.value = "";
|
|
365
372
|
if (!file) return;
|
|
373
|
+
const epoch = ++uploadEpoch;
|
|
374
|
+
const targetKey = lastKey;
|
|
375
|
+
// A host may register assets before returning asset:id. Only assets are
|
|
376
|
+
// excluded; replacing content or changing any other source is stale.
|
|
377
|
+
const sourceSignature = () => JSON.stringify({ ...editor.presentation, assets: undefined });
|
|
378
|
+
const originalSource = sourceSignature();
|
|
379
|
+
const capturedBuild = captureBuild();
|
|
380
|
+
const build = (ref, presentation) => {
|
|
381
|
+
if (destroyed || uploadEpoch !== epoch || lastKey !== targetKey || sourceSignature() !== originalSource)
|
|
382
|
+
throw new OPFEditorError("stale-image-upload", "The upload target or form changed. Choose the image again.", {});
|
|
383
|
+
return capturedBuild(ref, presentation);
|
|
384
|
+
};
|
|
366
385
|
statusEl.textContent = `Adding ${file.name}…`;
|
|
367
386
|
errorEl.textContent = "";
|
|
368
387
|
try {
|
|
@@ -371,6 +390,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
371
390
|
say(`${label} set to ${file.name}${change.assetId ? `, added to assets as ${change.assetId}` : ""}.${warnings} Undo restores the previous state.`);
|
|
372
391
|
options.onChange?.(change);
|
|
373
392
|
} catch (error) {
|
|
393
|
+
if (destroyed) return;
|
|
374
394
|
complain(messageOf(error));
|
|
375
395
|
sync();
|
|
376
396
|
}
|
|
@@ -384,6 +404,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
384
404
|
// `key` names what the image is for (variant, zone, slide, scope): alt text typed for one target is not offered to another.
|
|
385
405
|
set(ref, note = "", key = "") {
|
|
386
406
|
const targetChanged = (ref ?? "") !== lastRef || key !== lastKey;
|
|
407
|
+
if (targetChanged) invalidateUpload();
|
|
387
408
|
lastKey = key;
|
|
388
409
|
lastRef = ref ?? "";
|
|
389
410
|
source.setList(assetList());
|
|
@@ -568,7 +589,10 @@ export function createDesignControls(container, options = {}) {
|
|
|
568
589
|
};
|
|
569
590
|
const bgImage = imageCluster("bg-image", "Background image", {
|
|
570
591
|
apply: (ref) => (ref === null ? setBackground(editor, null, scoped()) : setBackground(editor, imageSpec(ref), scoped())),
|
|
571
|
-
|
|
592
|
+
captureBuild: () => {
|
|
593
|
+
const spec = imageSpec(undefined), target = scoped();
|
|
594
|
+
return (ref, doc) => prepareBackground(doc, { ...spec, src: ref }, target);
|
|
595
|
+
},
|
|
572
596
|
help: "An asset reference, a web address or a data address. Press Enter or choose a file to apply it with the settings here.",
|
|
573
597
|
});
|
|
574
598
|
imagePanel.append(bgImage.wrap, fit.wrap, bgAlt.wrap, focusX.wrap, focusY.wrap, bgRecolor.wrap, bgDuoDark.wrap, bgDuoLight.wrap, overlay.wrap);
|
|
@@ -727,7 +751,10 @@ export function createDesignControls(container, options = {}) {
|
|
|
727
751
|
});
|
|
728
752
|
const image = imageCluster("hf-image", "Image", {
|
|
729
753
|
apply: (ref) => setHeaderFooterZone(editor, which(), zone(), { image: ref }, scoped()),
|
|
730
|
-
|
|
754
|
+
captureBuild: () => {
|
|
755
|
+
const kind = which(), place = zone(), target = scoped();
|
|
756
|
+
return (ref, doc) => prepareHeaderFooterZone(doc, kind, place, { image: ref }, target);
|
|
757
|
+
},
|
|
731
758
|
help: "A picture in this zone, such as a partner mark or badge: an asset reference, a web address or a data address, or an organization logo (Insert logo, below). Press Enter to apply; clear the field to remove it.",
|
|
732
759
|
});
|
|
733
760
|
// RR-71: a logo is the zone's image, written as a reference to the organization's logo (var:organization.logo.icon). Which artwork is drawn (light or dark) follows each slide's background.
|
|
@@ -838,9 +865,9 @@ export function createDesignControls(container, options = {}) {
|
|
|
838
865
|
const { shape, ...rest } = logoTarget();
|
|
839
866
|
return setOrganizationLogo(editor, shape, ref, rest);
|
|
840
867
|
},
|
|
841
|
-
|
|
868
|
+
captureBuild: () => {
|
|
842
869
|
const { shape, ...rest } = logoTarget();
|
|
843
|
-
return prepareOrganizationLogo(doc, shape, ref, rest);
|
|
870
|
+
return (ref, doc) => prepareOrganizationLogo(doc, shape, ref, rest);
|
|
844
871
|
},
|
|
845
872
|
help: "An asset reference, a web address or a data address for the chosen shape and background. Press Enter to apply; clear the field to remove it.",
|
|
846
873
|
});
|
|
@@ -933,7 +960,10 @@ export function createDesignControls(container, options = {}) {
|
|
|
933
960
|
});
|
|
934
961
|
const watermarkSource = imageCluster("watermark", "Watermark", {
|
|
935
962
|
apply: (ref) => setDesignOption(editor, "watermark", ref === null ? null : { src: ref }, scoped()),
|
|
936
|
-
|
|
963
|
+
captureBuild: () => {
|
|
964
|
+
const target = scoped();
|
|
965
|
+
return (ref, doc) => prepareDesignOption(doc, "watermark", { src: ref }, target);
|
|
966
|
+
},
|
|
937
967
|
});
|
|
938
968
|
// FA-13: a watermark is an image or a text stamp; the text field and the image cluster replace each other.
|
|
939
969
|
const watermarkText = textField("watermark-text", "Watermark text", {
|
|
@@ -1311,7 +1341,9 @@ export function createDesignControls(container, options = {}) {
|
|
|
1311
1341
|
fill.setOptions(CELL_FILLS.map(([value, label]) => ({ value, label })));
|
|
1312
1342
|
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.") });
|
|
1313
1343
|
align.setOptions(["left", "center", "right"].map((value) => ({ value, label: titleCase(value) })));
|
|
1314
|
-
|
|
1344
|
+
// RR-93: `bold` is a cell style field, so a number cell stays bold without rich runs and keeps its number format and alignment. Unchecking removes the field.
|
|
1345
|
+
const bold = checkField("cell-bold", "Bold cell text", { onChange: (checked) => run(() => setTableCellStyle(editor, dynamic.tablePath, dynamic.cell, { bold: checked ? true : null }), checked ? "Cell text is bold." : "Cell text is no longer bold.") });
|
|
1346
|
+
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, bold.wrap);
|
|
1315
1347
|
// FA-27: the table's text alternative, like a chart's. It belongs to the table, so a dataset-backed table has it too.
|
|
1316
1348
|
const alt = textField("table-alt", "Alt text", {
|
|
1317
1349
|
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.",
|
|
@@ -1324,7 +1356,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1324
1356
|
});
|
|
1325
1357
|
const styleControls = [headerStyle.wrap, banding.wrap, borders.wrap, resetStyle, cellBox];
|
|
1326
1358
|
body.append(summary, alt.wrap, decorative.wrap, ...styleControls);
|
|
1327
|
-
dynamic.table = { details, summary, alt, decorative, styleControls, headerStyle, banding, borders, cellBox, cellLabel, columns, rows, merge, split, fill, align, join };
|
|
1359
|
+
dynamic.table = { details, summary, alt, decorative, styleControls, headerStyle, banding, borders, cellBox, cellLabel, columns, rows, merge, split, fill, align, bold, join };
|
|
1328
1360
|
}
|
|
1329
1361
|
|
|
1330
1362
|
// --- sync -----------------------------------------------------------------------------------------
|
|
@@ -1415,6 +1447,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1415
1447
|
const fillValue = typeof state.style.fill === "string" ? state.style.fill : "";
|
|
1416
1448
|
tableUi.fill.set(CELL_FILLS.some(([value]) => value === fillValue) ? fillValue : fillValue ? "(custom)" : "");
|
|
1417
1449
|
tableUi.align.set(state.style.align ?? "");
|
|
1450
|
+
tableUi.bold.set(state.style.bold === true);
|
|
1418
1451
|
}
|
|
1419
1452
|
}
|
|
1420
1453
|
}
|
|
@@ -1459,6 +1492,9 @@ export function createDesignControls(container, options = {}) {
|
|
|
1459
1492
|
},
|
|
1460
1493
|
destroy() {
|
|
1461
1494
|
destroyed = true;
|
|
1495
|
+
invalidateUpload();
|
|
1496
|
+
root.removeEventListener("input", invalidateUpload, true);
|
|
1497
|
+
root.removeEventListener("change", invalidateUpload, true);
|
|
1462
1498
|
unsubscribe();
|
|
1463
1499
|
root.remove();
|
|
1464
1500
|
},
|
package/dist/find-replace.js
CHANGED
|
@@ -226,7 +226,10 @@ export function findMatches(presentation, query, options = {}) {
|
|
|
226
226
|
const tokens = item.text.includes("{{") ? [...item.text.matchAll(VARIABLE_TOKEN)].map((token) => [token.index, token.index + token[0].length]) : [];
|
|
227
227
|
while ((found = compiled.regex.exec(item.text))) {
|
|
228
228
|
if (found[0] === "") {
|
|
229
|
-
|
|
229
|
+
// AdvanceStringIndex in Unicode mode must skip a whole surrogate pair:
|
|
230
|
+
// exec() otherwise rewinds an index inside it to the same empty match.
|
|
231
|
+
const at = compiled.regex.lastIndex;
|
|
232
|
+
compiled.regex.lastIndex = at + (compiled.regex.unicode && item.text.codePointAt(at) > 0xFFFF ? 2 : 1);
|
|
230
233
|
continue;
|
|
231
234
|
}
|
|
232
235
|
if (tokens.some(([from, to]) => found.index < to && found.index + found[0].length > from)) continue;
|
package/dist/image-crop.d.ts
CHANGED
|
@@ -55,5 +55,5 @@ export interface LoadedImage { image: HTMLImageElement; width: number; height: n
|
|
|
55
55
|
export declare function loadImagePixels(src: string, options?: { document?: Document; signal?: AbortSignal }): Promise<LoadedImage>;
|
|
56
56
|
/** Cut a rectangle out of a loaded picture at its own resolution (JPEG stays JPEG, everything else PNG). Needs a browser canvas. */
|
|
57
57
|
export declare function cropImagePixels(loaded: LoadedImage, rect: Rect, options?: { mediaType?: string; maxBytes?: number; document?: Document }): Promise<CroppedPixels>;
|
|
58
|
-
/** Crop a picture
|
|
59
|
-
export declare function applyCrop(editor: EditorSession, path: string, rect: Rect, options?: { loaded?: LoadedImage; maxBytes?: number; meta?: Record<string, unknown
|
|
58
|
+
/** Crop a picture as one undoable change; rejects stale document revisions and cancellation before committing. */
|
|
59
|
+
export declare function applyCrop(editor: EditorSession, path: string, rect: Rect, options?: { loaded?: LoadedImage; maxBytes?: number; meta?: Record<string, unknown>; signal?: AbortSignal }): Promise<EditorChange & { assetId: string; reference: string; width: number; height: number }>;
|
package/dist/image-crop.js
CHANGED
|
@@ -259,17 +259,30 @@ export async function loadImagePixels(src, { document: doc = globalThis.document
|
|
|
259
259
|
const win = doc.defaultView ?? globalThis;
|
|
260
260
|
const attempt = (anonymous) => new Promise((resolve, reject) => {
|
|
261
261
|
const image = new win.Image();
|
|
262
|
+
let settled = false;
|
|
263
|
+
const finish = (callback, value) => {
|
|
264
|
+
if (settled) return;
|
|
265
|
+
settled = true;
|
|
266
|
+
image.onload = image.onerror = null;
|
|
267
|
+
signal?.removeEventListener("abort", abort);
|
|
268
|
+
callback(value);
|
|
269
|
+
};
|
|
270
|
+
const abort = () => {
|
|
271
|
+
finish(reject, signal.reason ?? new win.DOMException("Image loading was cancelled.", "AbortError"));
|
|
272
|
+
image.src = "";
|
|
273
|
+
};
|
|
262
274
|
if (anonymous) image.crossOrigin = "anonymous";
|
|
263
275
|
image.decoding = "async";
|
|
264
|
-
image.onload = () => resolve
|
|
265
|
-
image.onerror = () => reject
|
|
266
|
-
signal?.addEventListener("abort",
|
|
267
|
-
|
|
276
|
+
image.onload = () => finish(resolve, image);
|
|
277
|
+
image.onerror = () => finish(reject, fail("image-unreadable", "The picture could not be loaded. If it is hosted elsewhere, upload it to the presentation first.", { src: src.slice(0, 80) }));
|
|
278
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
279
|
+
if (signal?.aborted) abort();
|
|
280
|
+
else image.src = src;
|
|
268
281
|
});
|
|
269
282
|
// A picture on another site is read with CORS first (so its pixels can be cut); without it the picture still shows and
|
|
270
283
|
// cropImagePixels reports why it cannot be cut.
|
|
271
284
|
const hosted = !/^(?:data|blob):/.test(src);
|
|
272
|
-
const image = hosted ? await attempt(true).catch(() => attempt(false)) : await attempt(false);
|
|
285
|
+
const image = hosted ? await attempt(true).catch((error) => { if (signal?.aborted) throw error; return attempt(false); }) : await attempt(false);
|
|
273
286
|
if (!image.naturalWidth || !image.naturalHeight) throw fail("image-unreadable", "The picture has no size.", {});
|
|
274
287
|
return { image, width: image.naturalWidth, height: image.naturalHeight };
|
|
275
288
|
}
|
|
@@ -324,12 +337,20 @@ export async function cropImagePixels(loaded, rect, { mediaType, maxBytes = DEFA
|
|
|
324
337
|
* `{ assetId, reference, width, height }`.
|
|
325
338
|
*/
|
|
326
339
|
export async function applyCrop(editor, path, rect, options = {}) {
|
|
327
|
-
const
|
|
340
|
+
const source = editor.snapshot();
|
|
341
|
+
const current = () => {
|
|
342
|
+
options.signal?.throwIfAborted();
|
|
343
|
+
if (editor.snapshot() !== source) throw fail("stale-image-crop", "The presentation changed while the picture was being cropped; open the current picture and crop it again.", { path });
|
|
344
|
+
};
|
|
345
|
+
current();
|
|
346
|
+
const image = describeImage(source.presentation, path);
|
|
328
347
|
if (image.error) throw fail("not-croppable", image.error, { path });
|
|
329
|
-
const loaded = options.loaded ?? await loadImagePixels(image.assetSrc);
|
|
348
|
+
const loaded = options.loaded ?? await loadImagePixels(image.assetSrc, { signal: options.signal });
|
|
349
|
+
current();
|
|
330
350
|
const pixels = await cropImagePixels(loaded, rect, { mediaType: image.mediaType, maxBytes: options.maxBytes });
|
|
331
|
-
//
|
|
332
|
-
|
|
351
|
+
// Never apply old pixels to a newer document or to a cancelled crop session.
|
|
352
|
+
current();
|
|
353
|
+
const prepared = prepareCrop(source.presentation, path, pixels);
|
|
333
354
|
const change = editor.applyPatch(prepared.patches, { ...options.meta, source: options.meta?.source ?? "image-crop", path, assetId: prepared.assetId });
|
|
334
355
|
return { ...change, assetId: prepared.assetId, reference: prepared.reference, width: pixels.width, height: pixels.height };
|
|
335
356
|
}
|
package/dist/image-cropper.js
CHANGED
|
@@ -143,6 +143,8 @@ export function createImageCropper(root, overlay, options) {
|
|
|
143
143
|
const opener = doc.activeElement;
|
|
144
144
|
const s = (session = {
|
|
145
145
|
path,
|
|
146
|
+
source: editor.snapshot(),
|
|
147
|
+
controller: new win.AbortController(),
|
|
146
148
|
opener,
|
|
147
149
|
image,
|
|
148
150
|
loaded: null,
|
|
@@ -210,6 +212,7 @@ export function createImageCropper(root, overlay, options) {
|
|
|
210
212
|
const close = (result) => {
|
|
211
213
|
if (session !== s) return;
|
|
212
214
|
session = null;
|
|
215
|
+
s.controller.abort();
|
|
213
216
|
resizeObserver.disconnect();
|
|
214
217
|
layer.remove();
|
|
215
218
|
if (s.scrollLock !== null) doc.documentElement.style.overflow = s.scrollLock;
|
|
@@ -447,7 +450,8 @@ export function createImageCropper(root, overlay, options) {
|
|
|
447
450
|
applyButton.disabled = true;
|
|
448
451
|
applyButton.textContent = "Applying…";
|
|
449
452
|
try {
|
|
450
|
-
|
|
453
|
+
if (editor.snapshot() !== s.source) throw new Error("The presentation changed; close this crop and open the current picture again.");
|
|
454
|
+
const changed = await applyCrop(editor, path, s.rect, { loaded: s.loaded, signal: s.controller.signal, meta: { tool: s.tool } });
|
|
451
455
|
if (session !== s) return;
|
|
452
456
|
close("done");
|
|
453
457
|
options.onCommit?.({ path, assetId: changed.assetId });
|
|
@@ -469,7 +473,7 @@ export function createImageCropper(root, overlay, options) {
|
|
|
469
473
|
for (const control of controls) control.disabled = true;
|
|
470
474
|
layer.setAttribute("aria-busy", "true");
|
|
471
475
|
try {
|
|
472
|
-
s.loaded = await loadImagePixels(image.assetSrc, { document: doc });
|
|
476
|
+
s.loaded = await loadImagePixels(image.assetSrc, { document: doc, signal: s.controller.signal });
|
|
473
477
|
} catch (error) {
|
|
474
478
|
if (session !== s) return false;
|
|
475
479
|
close("error");
|
package/dist/index.js
CHANGED
|
@@ -269,7 +269,8 @@ export function createEditorSession(input, options = {}) {
|
|
|
269
269
|
return () => listeners.delete(listener);
|
|
270
270
|
},
|
|
271
271
|
get(path, fallback) {
|
|
272
|
-
|
|
272
|
+
const found = readPointer(presentation, splitOpfPath(path));
|
|
273
|
+
return found.found ? clone(found.value) : fallback;
|
|
273
274
|
},
|
|
274
275
|
set(path, value, meta = {}) {
|
|
275
276
|
return commitPatch(createValuePatch(presentation, path, value), {
|
package/dist/persistence.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface AutosaveStatus {
|
|
|
38
38
|
savedAt?: number;
|
|
39
39
|
/** A sentence to show the user for `unavailable` and `error` (and a note on `saved`, such as "history too large to keep"). */
|
|
40
40
|
message?: string;
|
|
41
|
-
error?: "quota" | "write";
|
|
41
|
+
error?: "quota" | "write" | "draft";
|
|
42
42
|
/** The adapter in use: "indexedDB", "localStorage", "memory" or the host adapter's name. */
|
|
43
43
|
storage?: string;
|
|
44
44
|
offer?: RestoreOffer;
|
|
@@ -63,8 +63,8 @@ export interface PersistenceOptions {
|
|
|
63
63
|
onRestorePrompt?: (offer: RestoreOffer, actions: { restore(): Promise<boolean>; discard(): Promise<boolean> }) => RestoreChoice | Promise<RestoreChoice>;
|
|
64
64
|
/** Called on every status change, for an indicator. */
|
|
65
65
|
onStatus?: (status: AutosaveStatus) => void;
|
|
66
|
-
/** Called before
|
|
67
|
-
beforeFlush?: () => void;
|
|
66
|
+
/** Called synchronously before each explicit, debounced or lifecycle flush, even before any session edit. Commit the active draft; returning false or throwing refuses the write and reports a draft error. */
|
|
67
|
+
beforeFlush?: () => boolean | void;
|
|
68
68
|
/** Warn before the page closes while the document is `dirty` (default true). */
|
|
69
69
|
warnOnUnload?: boolean;
|
|
70
70
|
/** For tests and frames: the window that receives `beforeunload`, `pagehide` and `visibilitychange` (default the global window; null for none). */
|
package/dist/persistence.js
CHANGED
|
@@ -167,6 +167,8 @@ export function createPersistence(editor, options = {}) {
|
|
|
167
167
|
let chain = Promise.resolve();
|
|
168
168
|
let lastWritten = "";
|
|
169
169
|
let destroyed = false;
|
|
170
|
+
let preparingFlush = false;
|
|
171
|
+
let flushRefused = false;
|
|
170
172
|
let status = { state: "starting", available: false, dirty: false };
|
|
171
173
|
const setStatus = (patch) => {
|
|
172
174
|
status = { ...status, ...patch, dirty: isDirty() };
|
|
@@ -227,6 +229,24 @@ export function createPersistence(editor, options = {}) {
|
|
|
227
229
|
function flush() {
|
|
228
230
|
clearTimeout(timer);
|
|
229
231
|
firstDirtyAt = 0;
|
|
232
|
+
if (destroyed) return Promise.resolve(false);
|
|
233
|
+
// Commit synchronously, including a draft in an otherwise untouched session. The lifecycle
|
|
234
|
+
// warning below must see that edit before it decides whether leaving would lose work.
|
|
235
|
+
// A commit emits a change; clear its debounce again and avoid recursive host flush hooks.
|
|
236
|
+
if (preparingFlush) return chain;
|
|
237
|
+
preparingFlush = true;
|
|
238
|
+
try {
|
|
239
|
+
if (options.beforeFlush?.() === false) throw new Error("Commit or cancel the active draft first.");
|
|
240
|
+
flushRefused = false;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
flushRefused = true;
|
|
243
|
+
setStatus({ state: "error", error: "draft", message: `Autosave could not commit the active draft (${String(error?.message ?? error)}). Keep this page open to recover your work.` });
|
|
244
|
+
return Promise.resolve(false);
|
|
245
|
+
} finally {
|
|
246
|
+
preparingFlush = false;
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
firstDirtyAt = 0;
|
|
249
|
+
}
|
|
230
250
|
if (!touched) return chain.then(() => true);
|
|
231
251
|
chain = chain.then(writeRecord, writeRecord);
|
|
232
252
|
return chain;
|
|
@@ -342,15 +362,14 @@ export function createPersistence(editor, options = {}) {
|
|
|
342
362
|
schedule();
|
|
343
363
|
});
|
|
344
364
|
const beforeUnload = (event) => {
|
|
345
|
-
try { options.beforeFlush?.(); } catch { /* an uncommitted draft that cannot commit must not block leaving */ }
|
|
346
365
|
// Best effort: start the final write; a browser finishes an IndexedDB transaction that began before the page went away.
|
|
347
|
-
|
|
348
|
-
if (options.warnOnUnload === false || !isDirty()) return undefined;
|
|
366
|
+
void flush();
|
|
367
|
+
if (options.warnOnUnload === false || (!isDirty() && !flushRefused)) return undefined;
|
|
349
368
|
event.preventDefault();
|
|
350
369
|
event.returnValue = "";
|
|
351
370
|
return "";
|
|
352
371
|
};
|
|
353
|
-
const pageHide = () => {
|
|
372
|
+
const pageHide = () => { void flush(); };
|
|
354
373
|
const visibility = () => { if (win?.document?.visibilityState === "hidden") pageHide(); };
|
|
355
374
|
win?.addEventListener?.("beforeunload", beforeUnload);
|
|
356
375
|
win?.addEventListener?.("pagehide", pageHide);
|
package/dist/slides.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Catalog } from "@openpresentation/opf";
|
|
1
2
|
import type { EditorChange, EditorSession, JsonPatchOperation } from "./index.js";
|
|
2
3
|
|
|
3
4
|
/** One section of the deck: a maximal run of consecutive slides sharing a `section` label (or none). */
|
|
@@ -57,8 +58,11 @@ export interface AddSlideOptions {
|
|
|
57
58
|
section?: string | null;
|
|
58
59
|
/** A ready-made slide to insert instead of a blank one. */
|
|
59
60
|
slide?: Record<string, unknown>;
|
|
60
|
-
catalogs
|
|
61
|
+
/** Host catalogs, as for DimensionSwitchOptions. */
|
|
62
|
+
catalogs?: readonly Catalog[];
|
|
61
63
|
record?: Record<string, unknown>;
|
|
64
|
+
/** Catalog source of record, as for DimensionSwitchOptions. */
|
|
65
|
+
recordSource?: string;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
export declare function listSections(presentation: unknown): SlideSection[];
|
package/dist/table-options.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export declare function describeTableCell(table: unknown, cell: TableCellAddress
|
|
|
62
62
|
/** Merge `span.colSpan` x `span.rowSpan` cells from `cell`. `join` keeps the text of covered cells by joining it into the anchor; without it, covered text refuses with `merge-would-lose-content`. */
|
|
63
63
|
export declare function prepareTableMerge(presentation: unknown, tablePath: string, cell: TableCellAddress, span: { colSpan?: number; rowSpan?: number }, options?: { join?: boolean }): PreparedTableChange;
|
|
64
64
|
export declare function prepareTableSplit(presentation: unknown, tablePath: string, cell: TableCellAddress): PreparedTableChange;
|
|
65
|
-
/** Merge style fields into the cells' styles (null removes a field, `style: null` clears). */
|
|
65
|
+
/** Merge style fields into the cells' styles (null removes a field, `style: null` clears). Fields include `fill`, `color`, `align`, `verticalAlign`, `padding`, `borders` and `bold` (boolean; keeps a numeric cell's number format and alignment, which rich runs would drop). */
|
|
66
66
|
export declare function prepareTableCellStyle(presentation: unknown, tablePath: string, cells: TableCellAddress | TableCellAddress[], style: Record<string, unknown> | null): PreparedTableChange;
|
|
67
67
|
/** Apply `{ header, banding, borders }` (missing fields mean theme/false) or a named preset: sets the header fill, banded-row fill and borders of every cell and clears them elsewhere; `theme` removes a previous style. */
|
|
68
68
|
export declare function prepareTableStyle(presentation: unknown, tablePath: string, preset: keyof typeof TABLE_STYLE_PRESETS | Partial<TableStyle>): PreparedTableChange;
|
package/dist/table-options.js
CHANGED
|
@@ -256,14 +256,16 @@ export function prepareTableSplit(presentation, tablePath, cell) {
|
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
258
|
* Compute the patch that changes the style of the cells. `style` fields merge into each cell's style
|
|
259
|
-
* (fill, color, align, verticalAlign, padding, borders); a `null` field removes it, and `style: null`
|
|
260
|
-
* clears the whole style.
|
|
259
|
+
* (fill, color, bold, align, verticalAlign, padding, borders); a `null` field removes it, and `style: null`
|
|
260
|
+
* clears the whole style. `bold` is a boolean (RR-93): it bolds the whole cell and, unlike rich runs, keeps a
|
|
261
|
+
* numeric column's number `format` and alignment. Covered (merged) positions are skipped. A cell left with no style or spans
|
|
261
262
|
* becomes a plain value again. Style is appearance only: values and spans are untouched.
|
|
262
263
|
*/
|
|
263
264
|
export function prepareTableCellStyle(presentation, tablePath, cells, style) {
|
|
264
265
|
const list = Array.isArray(cells) ? cells : [cells];
|
|
265
266
|
if (!list.length) throw fail("table-cell-not-found", "Choose at least one cell.", {});
|
|
266
267
|
if (style !== null && !isObject(style)) throw fail("invalid-table-style", "A cell style is an object, or null to clear it.", { style });
|
|
268
|
+
if (style && Object.hasOwn(style, "bold") && style.bold !== null && style.bold !== undefined && typeof style.bold !== "boolean") throw fail("invalid-table-style", "Cell bold is true or false (null removes it).", { bold: style.bold });
|
|
267
269
|
return transaction(presentation, tablePath, (table) => {
|
|
268
270
|
let skipped = 0;
|
|
269
271
|
for (const cell of list) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -266,11 +266,11 @@
|
|
|
266
266
|
"@codemirror/state": "6.7.6",
|
|
267
267
|
"@codemirror/view": "6.43.14",
|
|
268
268
|
"@lezer/highlight": "1.2.5",
|
|
269
|
-
"@openpresentation/opf": "^0.
|
|
269
|
+
"@openpresentation/opf": "^0.22.1",
|
|
270
270
|
"jsonc-parser": "3.3.1"
|
|
271
271
|
},
|
|
272
272
|
"peerDependencies": {
|
|
273
|
-
"@openpresentation/opf-render": "^0.
|
|
273
|
+
"@openpresentation/opf-render": "^0.22.2"
|
|
274
274
|
},
|
|
275
275
|
"peerDependenciesMeta": {
|
|
276
276
|
"@openpresentation/opf-render": {
|
|
@@ -323,8 +323,8 @@
|
|
|
323
323
|
"@expo-google-fonts/roboto-mono": "0.4.2",
|
|
324
324
|
"@expo-google-fonts/stix-two-math": "0.4.0",
|
|
325
325
|
"@expo-google-fonts/tinos": "0.4.2",
|
|
326
|
-
"@openpresentation/opf-pptx": "^0.
|
|
327
|
-
"@openpresentation/opf-render": "^0.
|
|
326
|
+
"@openpresentation/opf-pptx": "^0.22.0",
|
|
327
|
+
"@openpresentation/opf-render": "^0.22.2",
|
|
328
328
|
"@resvg/resvg-js": "2.6.2",
|
|
329
329
|
"axe-core": "4.14.0",
|
|
330
330
|
"esbuild": "0.28.2",
|