@openpresentation/opf-editor 0.26.10 → 0.28.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 +60 -6
- package/dist/canvas-variables.d.ts +15 -0
- package/dist/canvas-variables.js +52 -0
- package/dist/canvas.js +33 -5
- package/dist/design-controls.js +3 -2
- package/dist/design-options.js +1 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +50 -3
- package/dist/json-editor.d.ts +33 -0
- package/dist/json-editor.js +83 -2
- package/dist/layout-adapter.js +96 -0
- package/dist/layout-canvas.js +122 -232
- package/dist/layout-editor-ui.js +46 -185
- package/dist/layout-inspector.js +36 -99
- package/dist/layout-layers.js +75 -92
- package/dist/layout-model.js +62 -103
- package/dist/react.d.ts +38 -0
- package/dist/react.js +127 -0
- package/dist/slide-retention.js +13 -0
- package/dist/source-color-scheme.js +60 -0
- package/dist/source-pane.d.ts +85 -0
- package/dist/source-pane.js +151 -9
- package/dist/studio-kit/commands.d.ts +38 -0
- package/dist/studio-kit/commands.js +148 -0
- package/dist/studio-kit/dom.d.ts +36 -0
- package/dist/studio-kit/dom.js +162 -0
- package/dist/studio-kit/drag.d.ts +26 -0
- package/dist/studio-kit/drag.js +127 -0
- package/dist/studio-kit/gesture.d.ts +29 -0
- package/dist/studio-kit/gesture.js +87 -0
- package/dist/studio-kit/index.d.ts +29 -0
- package/dist/studio-kit/index.js +12 -0
- package/dist/studio-kit/inspector.d.ts +22 -0
- package/dist/studio-kit/inspector.js +119 -0
- package/dist/studio-kit/overlay.d.ts +29 -0
- package/dist/studio-kit/overlay.js +182 -0
- package/dist/studio-kit/selection.d.ts +30 -0
- package/dist/studio-kit/selection.js +106 -0
- package/dist/studio-kit/tree.d.ts +9 -0
- package/dist/studio-kit/tree.js +82 -0
- package/dist/studio-kit/viewport.d.ts +29 -0
- package/dist/studio-kit/viewport.js +153 -0
- package/dist/styles.css +325 -7
- package/dist/workspace-layouts.d.ts +31 -0
- package/dist/workspace-layouts.js +84 -0
- package/dist/workspace-measure.js +35 -0
- package/dist/workspace-shell.js +96 -0
- package/dist/workspace-source.js +83 -0
- package/dist/workspace-theme.js +142 -0
- package/dist/workspace.d.ts +413 -0
- package/dist/workspace.js +1274 -0
- package/package.json +29 -11
package/README.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
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
|
+
## Embed the editor (CE-06)
|
|
6
|
+
|
|
7
|
+
One call mounts the whole editor, over one session and one fonts handle: the source pane, the canvas, the slide row, Present, Export, the sorter, the outline, Layout Studio, Review, Find and autosave, each part optional.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import "@openpresentation/opf-editor/styles.css";
|
|
11
|
+
import { createOpfEditor } from "@openpresentation/opf-editor/workspace";
|
|
12
|
+
|
|
13
|
+
const editor = createOpfEditor(element, {
|
|
14
|
+
document, // an OPF object or its JSON text, or `session` to adopt one
|
|
15
|
+
catalogs: [gallery],
|
|
16
|
+
fonts, // the renderer's fonts handle, a lease { fonts, release }, a promise or a function of either
|
|
17
|
+
persistence: { key: "my-deck" }, // optional autosave and restore prompt
|
|
18
|
+
theme: { colorScheme: "auto", tokens: { accent: "hsl(262 60% 38%)" } },
|
|
19
|
+
onChange: ({ document }) => save(document),
|
|
20
|
+
});
|
|
21
|
+
await editor.ready;
|
|
22
|
+
editor.setView("outline");
|
|
23
|
+
editor.destroy();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
From React 18 or 19 (StrictMode-safe, renders an empty container on the server):
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
import { OpfEditor, useOpfDocument } from "@openpresentation/opf-editor/react";
|
|
30
|
+
|
|
31
|
+
<OpfEditor document={deck} fonts={loadFonts} catalogs={[gallery]} theme={theme} onEditor={setEditor} />;
|
|
32
|
+
const document = useOpfDocument(editor); // also useOpfSelection(editor), useOpfCursor(editor), useOpfEditor(options)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
[docs/embedding.md](docs/embedding.md) lists the layouts, `panels`, `slots` (your own arrangement), every option, the handle, the events and the React bindings; [docs/theming.md](docs/theming.md) is the token contract (surface, text, accent, border, radius and font, light and dark) and the `theme` option. `examples/playground.js` is a thin user of it.
|
|
36
|
+
|
|
5
37
|
## Install the editor and optional export dependencies (RR-63)
|
|
6
38
|
|
|
7
39
|
`@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:
|
|
@@ -110,7 +142,7 @@ const pane = createSourcePane(container, {
|
|
|
110
142
|
pane.setFormat('yaml'); pane.focusPath('/slides/1/title'); pane.getText(); pane.destroy();
|
|
111
143
|
```
|
|
112
144
|
|
|
113
|
-
Methods: `replaceDocument(document, { source?, format?, name?, meta? })` (below), `setFindings(list | null)`, `setFormat(format)`, `focusPath(pointer)` (moves the cursor to the line of a JSON Pointer in any format, found with core's `locate`), `getText()`, `refresh()`, `setName`, `setDensity`, `setReadOnly` and `destroy()`. A finding is `{ path, message, severity, line? }` (core `Finding` objects fit). Rows show the path in mono and the line at the right; a click moves the cursor and calls `onFindingSelect`.
|
|
145
|
+
Methods: `replaceDocument(document, { source?, format?, name?, meta? })` (below), `setFindings(list | null)`, `setFormat(format)`, `focusPath(pointer)` (moves the cursor to the line of a JSON Pointer in any format, found with core's `locate`), `getText()`, `getCursor()` and the `onCursor` option (the path under the caret), `revealPath(pointer | range, { focus?, select?, align?, flash?, part? })` (scrolls without moving the caret or the focus), `setDecorations(list, { group? })`, `clearDecorations(group?)` and `getDecorations(group?)` (a host's own marks, by path or range; see "The source pane's hooks" in [docs/embedding.md](docs/embedding.md)), `refresh()`, `setName`, `setDensity`, `setReadOnly` and `destroy()`. A finding is `{ path, message, severity, line? }` (core `Finding` objects fit). Rows show the path in mono and the line at the right; a click moves the cursor and calls `onFindingSelect`.
|
|
114
146
|
|
|
115
147
|
- **Opening a document: `pane.replaceDocument(document, { source, format, name })`.** Loading an example or a file into the pane (never by writing its text into the code editor, which the pane applies as a typed edit and forks the catalog records the two documents share) calls `editor.replaceDocument`: one undoable change, no catalog record forked. `document` is a parsed document or JSON text; `name` is the file name. `source` is the text the document was read from, in any of the three formats: the pane shows it as written (the JSON's formatting, the YAML's comments, the Markdown's layout) when the pane is in that format, and core's rewrite of the document otherwise. `format` (`"json"`, `"yaml"` or `"md"`) says which format `source` is; left out, it is inferred as the first of JSON, the pane's format, YAML and Markdown that reads as exactly this document (a `source` that reads as no format, or as another document, is ignored; an unknown `format` throws a `TypeError` before anything changes). The original text of a format other than the one shown is kept too: switching the pane to it shows those bytes, not a rewrite. Undo and Redo bring back each document's text exactly in the format shown (the opened text as written, the replaced document's text as it was, including YAML comments), so one Undo returns to the document and the text the visitor had. The pane keeps the last 8 such texts, which are also what a later load of the same document shows. A refused document throws (`invalid-opf-edit`, with `issues`) and changes nothing, and works in a read-only pane. A diff typed or pasted into the pane is still an edit, and editing a catalog record in it forks the record.
|
|
116
148
|
- **Keyboard Undo and Redo in the pane (one model).** Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z (Ctrl+Y) work in two layers, in this order. First CodeMirror's history, which holds only the text typed in the pane since its last whole-document change (a `replaceDocument`, or a session Undo, Redo or restore, each of which clears it because the typed edits no longer fit the text). When that has nothing left, the key steps the session over a whole-document replace, exactly as its Undo and Redo do, so the text comes back byte for byte: Undo steps back over the nearest `replaceDocument` together with the pane's own edits typed on it (the typing was already undone, step by step, by CodeMirror's history); Redo steps forward over the replace. The key never reaches over an edit made elsewhere (the canvas, a panel): Undo stops at it, and the host's Undo button (or `editor.undo()`) undoes that edit, after which the key reaches the load again. A read-only pane takes neither key. `mountJsonCodeEditor` exposes the same seam as `historyFallback: { undo(), redo() }` (answer `true` when you did something) plus `resetHistory()` and `undoDepth()` on the control.
|
|
@@ -356,6 +388,25 @@ editor.redo();
|
|
|
356
388
|
|
|
357
389
|
`createSvgTraceBinding` only reads `data-opf-path`; it does not require a hosted service, a second renderer, storage, analytics, auth, or product chrome. Host applications decide how selected paths appear in their own inspector UI.
|
|
358
390
|
|
|
391
|
+
### Ids, id-addressed events and `expectHash` (CE-04)
|
|
392
|
+
|
|
393
|
+
Slides and blocks can carry an `id` (core's `assignIds` fills the missing ones; `editor.assignIds()` does it as one undoable edit and returns `null` when none is missing). A path may name an element by id instead of by index, `slides.@intro.title` or `/slides/@intro/blocks/@kpi/text`, in `get`, `set` and `applyPatch`; the session resolves it against the open document.
|
|
394
|
+
|
|
395
|
+
By default the events are unchanged and carry index paths. Create the session with `idPaths: true` and every change and every `subscribe` event also carries the same patches addressed by id, each written against the document it applies to:
|
|
396
|
+
|
|
397
|
+
```js
|
|
398
|
+
const editor = createEditorSession(doc, { idPaths: true });
|
|
399
|
+
editor.subscribe((event) => {
|
|
400
|
+
event.patches; // [{ op: "replace", path: "/slides/1/blocks/1/text", value: "Edited" }]
|
|
401
|
+
event.idPatches; // [{ op: "replace", path: "/slides/@kpis/blocks/@note/text", value: "Edited" }]
|
|
402
|
+
event.idInversePatches; // patch events; undo events have idPatches and idRedoPatches
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
The id form still means the same thing on a replica whose slides are in another order, so a host can send it to a sync server or attach comments to it. Elements without an `id` keep their indexes, and an insertion keeps its index (or `-`) as the last token. It costs one extra walk of each patch.
|
|
407
|
+
|
|
408
|
+
`editor.documentHash()` is core's `documentHash` of the open document (`sha256:` and 64 hex digits, independent of key order). Pass it to `editor.applyPatch(ops, meta, { expectHash })` and the edit applies only while the document is still that one; otherwise it throws an `OPFEditorError` with code `patch-hash-mismatch` (`details.expected`, `details.actual`), changes nothing and emits nothing.
|
|
409
|
+
|
|
359
410
|
## Catalog Controls
|
|
360
411
|
|
|
361
412
|
Catalog helpers commit catalog references (`id` or `name:id`) that resolve in the document or the session's registered catalogs, instead of accepting freeform incompatible values:
|
|
@@ -486,7 +537,7 @@ insertZoneLogo(editor, "header", "left"); // th
|
|
|
486
537
|
| `titleAlignment`, `contentAlignment` | `design.titleAlignment`, `design.contentAlignment` | `left`, `center`, `right` |
|
|
487
538
|
| `contentDirection` | `design.contentDirection` | `horizontal`, `vertical` |
|
|
488
539
|
| `chartPrimary` | `design.chartPrimary` | `none`, `top`, `bottom`, `left`, `right` |
|
|
489
|
-
| `listBullet` | `design.listBullet` | `character`, `image` (picture bullets draw the logo) |
|
|
540
|
+
| `listBullet` | `design.listBullet` | `character`, `image` (picture bullets draw the logo), `none` (no marker and no indent) |
|
|
490
541
|
| `contentBox` | `design.contentBox` | `true`, `false` |
|
|
491
542
|
| `imageFit` | `design.imageFit` | `cover`, `contain`, `stretch`: the fit of image blocks that choose none |
|
|
492
543
|
| `accentFont` | `design.fontScheme.accent` | a family name; the font scheme becomes its object form and collapses back to the bare id when the accent is cleared |
|
|
@@ -767,10 +818,11 @@ import "@openpresentation/opf-editor/styles.css";
|
|
|
767
818
|
|
|
768
819
|
`pnpm run build` writes `dist/styles.css`: the token layer (`src/styles/tokens.css`) first, then every other `src/styles/*.css` in alphabetical order. It is plain CSS with no preprocessor, no `@import` and no `url()`: nothing is fetched. The package bundles no fonts either; load Inter and Geist Mono yourself (or leave the fallbacks: `system-ui` and `ui-monospace`). The panels that existed before it (design controls, review, slide manager, find, fill template, numbering, chart options, data grid, crop, the custom layout editor, the JSON editor and the restore banner) are styled with the tokens and nothing else. The find, review, crop and data-grid components still inject a small fallback stylesheet of their own, so they look as before when you do not import `styles.css`; with it, its rules win whichever `<style>` comes last.
|
|
769
820
|
|
|
770
|
-
**Tokens.** Every rule reads only `--opf-ui-*` custom properties, defined on `:root` and on `.opf-ui`, light by default.
|
|
821
|
+
**Tokens.** Every rule reads only `--opf-ui-*` custom properties, defined on `:root` and on `.opf-ui`, light by default. The whole contract, with light and dark values and what each token is for, is in [docs/theming.md](docs/theming.md). `createOpfEditor({ theme })` sets them for you; by hand, set them on the `.opf-ui` elements themselves (every component root carries the class and declares its own defaults, so a value set on a wrapper does not reach it) with a selector that outranks `.dark .opf-ui`:
|
|
771
822
|
|
|
772
823
|
```css
|
|
773
|
-
.
|
|
824
|
+
html .opf-ui,
|
|
825
|
+
html.dark .opf-ui {
|
|
774
826
|
--opf-ui-accent: hsl(262 60% 38%);
|
|
775
827
|
--opf-ui-accent-strong: hsl(262 60% 38%);
|
|
776
828
|
--opf-ui-ring: hsl(262 60% 38%);
|
|
@@ -792,7 +844,7 @@ import "@openpresentation/opf-editor/styles.css";
|
|
|
792
844
|
|
|
793
845
|
Put the class `opf-ui` on an element that wraps a mounted component to get the base rules: the font, the text color, `box-sizing: border-box`, a 2px `--opf-ui-ring` outline with a 2px offset on `:focus-visible`, and no transitions under `prefers-reduced-motion: reduce`.
|
|
794
846
|
|
|
795
|
-
**Dark mode.** The dark values apply under `.dark .opf-ui`, `.opf-ui.dark`, `[data-theme="dark"]` (on the root or any ancestor) and `:root.dark
|
|
847
|
+
**Dark mode.** The dark values apply under `.dark .opf-ui`, `.opf-ui.dark`, `[data-theme="dark"]` (on the root or any ancestor) and `:root.dark`; `[data-theme="light"]` on an ancestor or on the surface forces light inside a dark page. The host decides when: toggle `class="dark"` or `data-theme="dark"` on `<html>`, or on the wrapper. The stylesheet does not read `prefers-color-scheme`; `createOpfEditor({ theme: { colorScheme: "auto" } })` follows it for you, and a host that mounts components itself sets the attribute from `matchMedia("(prefers-color-scheme: dark)")`. Dark also sets `color-scheme: dark`, so native controls follow.
|
|
796
848
|
|
|
797
849
|
**Touch.** `data-density="touch"` on an `.opf-ui` element raises `--opf-ui-control-h` from 32px to 40px for everything inside it (the mobile design). The new components take the same value as a `density: "comfortable" | "touch"` option.
|
|
798
850
|
|
|
@@ -973,7 +1025,9 @@ Put the opened document in the session with `editor.replaceDocument(document)` (
|
|
|
973
1025
|
|
|
974
1026
|
## Optional React Bindings
|
|
975
1027
|
|
|
976
|
-
React bindings are isolated under `@openpresentation/opf-editor/react
|
|
1028
|
+
React bindings are isolated under `@openpresentation/opf-editor/react`; React (18 or later) is an optional peer, needed only by that entry point. The core package does not add React to the critical path.
|
|
1029
|
+
|
|
1030
|
+
`<OpfEditor>`, `useOpfEditor`, `useOpfDocument`, `useOpfSelection` and `useOpfCursor` mount and read the embeddable editor ([Embed the editor](#embed-the-editor-ce-06)). The older factory below builds two controls over a session you already have, with the React you pass:
|
|
977
1031
|
|
|
978
1032
|
```jsx
|
|
979
1033
|
import { createOPFReactComponents } from "@openpresentation/opf-editor/react";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EditorSession } from "./index.js";
|
|
2
|
+
import type { CanvasEditor } from "./canvas.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* For a host that mounts the canvas itself (`createCanvasEditor`) and draws `{{variables}}` resolved (`renderOptions.variables: {}`): a press on text
|
|
6
|
+
* that holds a variable token opens the field's properties form (which edits the authored value) instead of an inline edit of the resolved glyphs,
|
|
7
|
+
* and a press on text with no field of its own calls `focusSource`. `resolved()` says whether the canvas draws variables resolved now (a template deck
|
|
8
|
+
* draws them as authored). Returns a function that removes the listeners. `createOpfEditor({ variables: "resolved" })` (`@openpresentation/opf-editor/workspace`) does this for you.
|
|
9
|
+
*/
|
|
10
|
+
export declare function installCanvasVariableEditing(
|
|
11
|
+
host: HTMLElement,
|
|
12
|
+
editor: EditorSession,
|
|
13
|
+
canvas: CanvasEditor,
|
|
14
|
+
options?: { resolved?: () => boolean; focusSource?: () => void },
|
|
15
|
+
): () => void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// CE-06 (promoted from openpresentation.org's playground runtime): editing a document whose `{{variables}}` the canvas draws resolved.
|
|
2
|
+
// With `variables: "resolved"` the canvas shows a deck's variables filled in, and a glyph offset of the resolved text does not address
|
|
3
|
+
// the authored `{{token}}`. A press on such text opens the field's properties form (which edits the authored value) instead of an
|
|
4
|
+
// inline text edit, and a press on text that has no field of its own focuses the source pane.
|
|
5
|
+
|
|
6
|
+
function hasSourceToken(value) {
|
|
7
|
+
if (typeof value === "string") return value.includes("{{") || value.startsWith("var:");
|
|
8
|
+
if (Array.isArray(value)) return value.some(hasSourceToken);
|
|
9
|
+
return value !== null && typeof value === "object" && Object.values(value).some(hasSourceToken);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Route inline edits of text that holds a variable token to the canvas's properties form. `resolved()` says whether the canvas currently
|
|
14
|
+
* draws variables resolved (a template deck draws them as authored). Returns a function that removes the listeners.
|
|
15
|
+
*/
|
|
16
|
+
export function installCanvasVariableEditing(host, editor, canvas, { resolved = () => true, focusSource } = {}) {
|
|
17
|
+
function route(event) {
|
|
18
|
+
if (!resolved() || !(event.target instanceof Element)) return;
|
|
19
|
+
const target = event.target.closest('svg [data-opf-path][role="button"]');
|
|
20
|
+
if (!target || !host.contains(target) || !target.querySelector("text")) return;
|
|
21
|
+
const path = target.getAttribute("data-opf-path");
|
|
22
|
+
const value = editor.get(path);
|
|
23
|
+
const inline = ((typeof value === "string" || typeof value === "number") && !/\.(image|video)$/.test(path))
|
|
24
|
+
|| (Array.isArray(value) && target.hasAttribute("data-opf-rich-text"));
|
|
25
|
+
if (value !== undefined && (!inline || !hasSourceToken(value))) return;
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
event.stopImmediatePropagation();
|
|
28
|
+
if (value === undefined) {
|
|
29
|
+
focusSource?.();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// The click following pointerdown must not commit and reopen the same form.
|
|
33
|
+
if (canvas.editingPath !== path) canvas.editProperties(path);
|
|
34
|
+
}
|
|
35
|
+
function pointer(event) {
|
|
36
|
+
// A touch press may begin scrolling. Its actual click opens the form.
|
|
37
|
+
if (event.pointerType !== "touch" && event.button === 0) route(event);
|
|
38
|
+
}
|
|
39
|
+
function key(event) {
|
|
40
|
+
if (!event.isComposing && event.keyCode !== 229 && ["Enter", " ", "F2"].includes(event.key)) route(event);
|
|
41
|
+
}
|
|
42
|
+
host.addEventListener("pointerdown", pointer, true);
|
|
43
|
+
host.addEventListener("click", route, true);
|
|
44
|
+
host.addEventListener("dblclick", route, true);
|
|
45
|
+
host.addEventListener("keydown", key, true);
|
|
46
|
+
return () => {
|
|
47
|
+
host.removeEventListener("pointerdown", pointer, true);
|
|
48
|
+
host.removeEventListener("click", route, true);
|
|
49
|
+
host.removeEventListener("dblclick", route, true);
|
|
50
|
+
host.removeEventListener("keydown", key, true);
|
|
51
|
+
};
|
|
52
|
+
}
|
package/dist/canvas.js
CHANGED
|
@@ -113,7 +113,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
113
113
|
notice.hidden = true;
|
|
114
114
|
const style = doc.createElement("style");
|
|
115
115
|
style.textContent =
|
|
116
|
-
".opf-canvas-preview>svg{display:block;width:100%;height:auto}.opf-canvas [data-canvas-target]{cursor:text}.opf-canvas .opf-inline-input::selection{background:#8975d933;color:var(--opf-inline-selection-color,transparent)}.opf-canvas [data-canvas-target]:focus{outline:none}.opf-canvas [data-canvas-target]:hover>.opf-selection{stroke:#9c90df}.opf-canvas [data-canvas-selected]>.opf-selection,.opf-canvas [data-canvas-target]:focus-visible>.opf-selection{stroke:#7765d0}.opf-canvas [data-canvas-target][data-canvas-editing]>.opf-selection{stroke:none}.opf-canvas button:focus-visible,.opf-canvas textarea:focus-visible,.opf-canvas input:focus-visible{outline:2px solid #8170d5;outline-offset:2px}";
|
|
116
|
+
".opf-canvas-preview>svg{display:block;width:100%;height:auto}.opf-canvas [data-canvas-target]{cursor:text}.opf-canvas .opf-inline-input::selection{background:#8975d933;color:var(--opf-inline-selection-color,transparent)}.opf-canvas [data-canvas-target]:focus,.opf-canvas .opf-selection:focus{outline:none}.opf-canvas [data-canvas-target]:hover>.opf-selection{stroke:#9c90df}.opf-canvas [data-canvas-selected]>.opf-selection,.opf-canvas [data-canvas-target]:focus-visible>.opf-selection,.opf-canvas .opf-selection:focus-visible{stroke:#7765d0}.opf-canvas [data-canvas-target][data-canvas-editing]>.opf-selection{stroke:none}.opf-canvas button:focus-visible,.opf-canvas textarea:focus-visible,.opf-canvas input:focus-visible{outline:2px solid #8170d5;outline-offset:2px}";
|
|
117
117
|
root.append(style, preview, overlay, notice);
|
|
118
118
|
container.replaceChildren(root);
|
|
119
119
|
const report = (error) => {
|
|
@@ -167,6 +167,14 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
167
167
|
(node) => node.getAttribute("data-opf-path") === path,
|
|
168
168
|
);
|
|
169
169
|
}
|
|
170
|
+
// A target that holds other targets (a table with its cells, a chart with its labels) is a plain group, so it is never focusable
|
|
171
|
+
// itself (axe nested-interactive); keyboard focus for it lives on its selection handle, the first child. A leaf target takes focus.
|
|
172
|
+
function handleOf(node) {
|
|
173
|
+
return node?.getAttribute("role") === "group" ? node.querySelector(":scope > .opf-selection[tabindex]") : null;
|
|
174
|
+
}
|
|
175
|
+
function focusTarget(node, options = { preventScroll: false }) {
|
|
176
|
+
(handleOf(node) ?? node)?.focus?.(options);
|
|
177
|
+
}
|
|
170
178
|
function choose(path) {
|
|
171
179
|
selectedPath = path;
|
|
172
180
|
imageCropper?.sync(path);
|
|
@@ -350,7 +358,12 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
350
358
|
// A touch tap has no pointerdown entry (a touch press may start a scroll): it enters on the click itself.
|
|
351
359
|
const kind = editKind(path);
|
|
352
360
|
if (textEntry === "click" && event.detail > 0 && (kind === "text" || kind === "rich-text")) startEdit(path, { point: event });
|
|
353
|
-
else
|
|
361
|
+
else {
|
|
362
|
+
choose(path);
|
|
363
|
+
// A press on a container's own drawing (not on a child target) used to focus the container; now it focuses its handle.
|
|
364
|
+
const handle = handleOf(node);
|
|
365
|
+
if (handle && !event.target.closest?.(".opf-selection")) handle.focus({ preventScroll: true });
|
|
366
|
+
}
|
|
354
367
|
});
|
|
355
368
|
node.addEventListener("dblclick", (event) => {
|
|
356
369
|
event.preventDefault();
|
|
@@ -368,6 +381,21 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
368
381
|
});
|
|
369
382
|
if (path === selectedPath) node.setAttribute("data-canvas-selected", "");
|
|
370
383
|
}
|
|
384
|
+
// WCAG 4.1.2 / axe nested-interactive: an interactive element must not contain interactive descendants. A target that holds
|
|
385
|
+
// other targets is a named group; the button and the tab stop move to its selection handle (the transparent rect that already
|
|
386
|
+
// covers it and takes its pointer presses), so the container stays selectable by click, Tab and Enter, and its children stay buttons.
|
|
387
|
+
for (const node of svg.querySelectorAll("g[data-canvas-target]")) {
|
|
388
|
+
if (!node.querySelector("[data-canvas-target]")) continue;
|
|
389
|
+
const handle = node.querySelector(":scope > .opf-selection");
|
|
390
|
+
if (!handle) continue;
|
|
391
|
+
const field = node.getAttribute("data-opf-path").split(".").at(-1);
|
|
392
|
+
handle.setAttribute("role", "button");
|
|
393
|
+
handle.setAttribute("tabindex", "0");
|
|
394
|
+
handle.setAttribute("aria-label", node.getAttribute("aria-label"));
|
|
395
|
+
node.setAttribute("role", "group");
|
|
396
|
+
node.removeAttribute("tabindex");
|
|
397
|
+
node.setAttribute("aria-label", `${field.charAt(0).toUpperCase()}${field.slice(1)}`);
|
|
398
|
+
}
|
|
371
399
|
layoutHandles.update(presentation, geometry);
|
|
372
400
|
blockControls.update(presentation, geometry);
|
|
373
401
|
imageCropper?.update();
|
|
@@ -924,7 +952,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
924
952
|
options.propertiesContainer?.replaceChildren();
|
|
925
953
|
clearNotice();
|
|
926
954
|
render();
|
|
927
|
-
getTarget(edit.path)
|
|
955
|
+
focusTarget(getTarget(edit.path));
|
|
928
956
|
options.onCommit?.({ path: edit.path, editor });
|
|
929
957
|
return true;
|
|
930
958
|
} catch (error) {
|
|
@@ -949,7 +977,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
949
977
|
options.propertiesContainer?.replaceChildren();
|
|
950
978
|
if (clear) clearNotice();
|
|
951
979
|
render();
|
|
952
|
-
getTarget(path)
|
|
980
|
+
focusTarget(getTarget(path));
|
|
953
981
|
options.onCancel?.({ path });
|
|
954
982
|
}
|
|
955
983
|
function setSlide(index) {
|
|
@@ -984,7 +1012,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
984
1012
|
if (!node) continue;
|
|
985
1013
|
choose(candidate);
|
|
986
1014
|
node.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
987
|
-
if (focus) node
|
|
1015
|
+
if (focus) focusTarget(node, { preventScroll: true });
|
|
988
1016
|
return candidate;
|
|
989
1017
|
}
|
|
990
1018
|
return null;
|
package/dist/design-controls.js
CHANGED
|
@@ -1018,12 +1018,13 @@ export function createDesignControls(container, options = {}) {
|
|
|
1018
1018
|
};
|
|
1019
1019
|
const bullet = selectField("list-bullet", "List bullets", {
|
|
1020
1020
|
empty: "Default (character)",
|
|
1021
|
-
help: "Picture bullets draw the logo as the marker and need a logo.",
|
|
1022
|
-
onChange: (value) => run(() => setDesignOption(editor, "listBullet", value === "" ? null : value, scoped()), value === "image" ? "Picture bullets on." : "Character bullets."),
|
|
1021
|
+
help: "Picture bullets draw the logo as the marker and need a logo. No bullets draws no marker and no indent, for label-and-description lists.",
|
|
1022
|
+
onChange: (value) => run(() => setDesignOption(editor, "listBullet", value === "" ? null : value, scoped()), value === "image" ? "Picture bullets on." : value === "none" ? "No bullets." : "Character bullets."),
|
|
1023
1023
|
});
|
|
1024
1024
|
bullet.setOptions([
|
|
1025
1025
|
{ value: "character", label: "Character" },
|
|
1026
1026
|
{ value: "image", label: "Picture (the logo)" },
|
|
1027
|
+
{ value: "none", label: "None (no bullets)" },
|
|
1027
1028
|
]);
|
|
1028
1029
|
const accent = textField("accent-font", "Accent font", {
|
|
1029
1030
|
placeholder: "Font family name",
|
package/dist/design-options.js
CHANGED
|
@@ -62,7 +62,7 @@ const ENUMS = Object.freeze({
|
|
|
62
62
|
contentAlignment: ["left", "center", "right"],
|
|
63
63
|
contentDirection: ["horizontal", "vertical"],
|
|
64
64
|
chartPrimary: ["none", "top", "bottom", "left", "right"],
|
|
65
|
-
listBullet: ["character", "image"],
|
|
65
|
+
listBullet: ["character", "image", "none"],
|
|
66
66
|
imageFit: ["cover", "contain", "stretch"],
|
|
67
67
|
});
|
|
68
68
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PaginationOptions, PaginationResult } from "@openpresentation/opf/pagination";
|
|
2
2
|
import type { Composition, ComposeSlideOptions, SlideComposition } from "@openpresentation/opf/composition";
|
|
3
3
|
import type { Catalog, Fonts, PresentationStats, SlideContextDiagnostic, StatsOptions, ValidationReport } from "@openpresentation/opf";
|
|
4
|
+
import type { AssignIdsOptions } from "@openpresentation/opf/ids";
|
|
4
5
|
export * from "./catalogs.js";
|
|
5
6
|
/**
|
|
6
7
|
* What the editor adds to the options of `composeSlide` and `paginateSlide`: `fonts` is the renderer's fonts handle (`loadFonts()`), whose
|
|
@@ -31,7 +32,10 @@ export declare const runtimePolicy: Readonly<{
|
|
|
31
32
|
deterministicLocalExecution: true;
|
|
32
33
|
}>;
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* RFC 6902 operations, executed by core's `@openpresentation/opf/patch`. Paths are JSON Pointers or dotted OPF paths. In an array, a
|
|
37
|
+
* token `@id` names the element whose `id` is `id` (`/slides/@intro/title`, or `slides.@intro.title` as an OPF path).
|
|
38
|
+
*/
|
|
35
39
|
export type JsonPatchOperation =
|
|
36
40
|
| { op: "add"; path: string; value: unknown }
|
|
37
41
|
| { op: "replace"; path: string; value: unknown }
|
|
@@ -54,6 +58,14 @@ export interface EditorChange {
|
|
|
54
58
|
patches: JsonPatchOperation[];
|
|
55
59
|
inversePatches?: JsonPatchOperation[];
|
|
56
60
|
redoPatches?: JsonPatchOperation[];
|
|
61
|
+
/**
|
|
62
|
+
* With the session option `idPaths`: `patches`, `inversePatches` and `redoPatches` addressed by id (`/slides/@intro/title`), each
|
|
63
|
+
* written against the document it applies to (core's `toIdPatch`). Elements without an `id` keep their indexes; an insertion keeps
|
|
64
|
+
* its index (or `-`) as the last token.
|
|
65
|
+
*/
|
|
66
|
+
idPatches?: JsonPatchOperation[];
|
|
67
|
+
idInversePatches?: JsonPatchOperation[];
|
|
68
|
+
idRedoPatches?: JsonPatchOperation[];
|
|
57
69
|
validation: ValidationReport;
|
|
58
70
|
}
|
|
59
71
|
|
|
@@ -63,6 +75,10 @@ export interface EditorEvent {
|
|
|
63
75
|
patches: JsonPatchOperation[];
|
|
64
76
|
inversePatches?: JsonPatchOperation[];
|
|
65
77
|
redoPatches?: JsonPatchOperation[];
|
|
78
|
+
/** With the session option `idPaths`: the id-addressed form of `patches`, `inversePatches` and `redoPatches` (see `EditorChange`). `[]` for `restore` and `catalogs`. */
|
|
79
|
+
idPatches?: JsonPatchOperation[];
|
|
80
|
+
idInversePatches?: JsonPatchOperation[];
|
|
81
|
+
idRedoPatches?: JsonPatchOperation[];
|
|
66
82
|
validation: ValidationReport;
|
|
67
83
|
meta?: Record<string, unknown>;
|
|
68
84
|
snapshot: EditorSnapshot;
|
|
@@ -79,6 +95,12 @@ export interface PrecomputedValidation {
|
|
|
79
95
|
}
|
|
80
96
|
export interface ApplyPatchOptions {
|
|
81
97
|
validated?: PrecomputedValidation;
|
|
98
|
+
/**
|
|
99
|
+
* Apply the edit only while the open document is the one this hash (`session.documentHash()` or core's `documentHash`) was taken
|
|
100
|
+
* from. A document that changed since throws an `OPFEditorError` with code `patch-hash-mismatch` and `details.expected` and
|
|
101
|
+
* `details.actual`; nothing is applied. A value that is not a hash throws `invalid-expect-hash`.
|
|
102
|
+
*/
|
|
103
|
+
expectHash?: string;
|
|
82
104
|
}
|
|
83
105
|
|
|
84
106
|
export interface EditorSession {
|
|
@@ -107,6 +129,14 @@ export interface EditorSession {
|
|
|
107
129
|
* current `catalogs` list (the same object); otherwise the session validates the document itself. It is never stored in `meta` or the history.
|
|
108
130
|
*/
|
|
109
131
|
applyPatch(operations: JsonPatchOperation[], meta?: Record<string, unknown>, options?: ApplyPatchOptions): EditorChange;
|
|
132
|
+
/** The content hash of the open document (core's `documentHash`: `sha256:` and 64 hex digits, independent of key order). Use it as `applyPatch`'s `expectHash`. */
|
|
133
|
+
documentHash(): string;
|
|
134
|
+
/**
|
|
135
|
+
* Give every slide and block without an `id` one (core's `assignIds`) as one undoable edit. Returns the change, or `null` when every
|
|
136
|
+
* slide and block already has an id. `meta.source` defaults to `"assign-ids"`. A `generate` that returns an unusable id throws an
|
|
137
|
+
* `OPFEditorError` with code `invalid-id` or `duplicate-id`.
|
|
138
|
+
*/
|
|
139
|
+
assignIds(options?: AssignIdsOptions, meta?: Record<string, unknown>): EditorChange | null;
|
|
110
140
|
/**
|
|
111
141
|
* Open a different document in the session: the whole document is replaced as one undoable change, and the replacement is never
|
|
112
142
|
* read as an edit of the catalog records the two documents share, so nothing under `catalogs.default` or a named group is forked
|
|
@@ -128,6 +158,12 @@ export interface CreateEditorSessionOptions {
|
|
|
128
158
|
rejectInvalid?: boolean;
|
|
129
159
|
/** The host's catalogs (for example `[gallery]` from `@openpresentation/gallery`): resolution, validation, pickers, embedding. */
|
|
130
160
|
catalogs?: readonly Catalog[];
|
|
161
|
+
/**
|
|
162
|
+
* Also report every change and event by id: `idPatches`, `idInversePatches` and `idRedoPatches` next to `patches`,
|
|
163
|
+
* `inversePatches` and `redoPatches`, so a host can sync, comment or merge by `/slides/@intro/blocks/@kpi/text`
|
|
164
|
+
* instead of by indexes that move. Default false (the events are unchanged); it costs one extra walk of each patch.
|
|
165
|
+
*/
|
|
166
|
+
idPaths?: boolean;
|
|
131
167
|
}
|
|
132
168
|
|
|
133
169
|
export interface CatalogOption {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { commitCatalogControl } from "./catalog-control.js";
|
|
2
2
|
import { paginateSlide } from "@openpresentation/opf/pagination";
|
|
3
3
|
import { OPFPatchError, applyPatch as applyCorePatch, applyPatchWithInverse, formatPointer, invertPatch, jsonEqual, parsePointer, readPointer } from "@openpresentation/opf/patch";
|
|
4
|
+
import { OPFIdError, assignIds, documentHash, toIdPatch } from "@openpresentation/opf/ids";
|
|
4
5
|
import { collectReservedPresentationIds } from "./presentation-ids.js";
|
|
5
6
|
import { composeSlide } from "@openpresentation/opf/composition";
|
|
6
7
|
import { catalogKinds, resolveSlideContext, stats } from "@openpresentation/opf";
|
|
@@ -157,6 +158,18 @@ export function createEditorSession(input, options = {}) {
|
|
|
157
158
|
const redoStack = [];
|
|
158
159
|
const listeners = new Set();
|
|
159
160
|
const rejectInvalid = Boolean(options.rejectInvalid);
|
|
161
|
+
// CE-04: with `idPaths`, every event and change also carries the same patches addressed by id (`/slides/@intro/title`), which stay
|
|
162
|
+
// true when slides and blocks move, for hosts that sync or comment by id. Each is written against the document it applies to.
|
|
163
|
+
// It costs one walk of each patch over a copy of the document, so it is off by default.
|
|
164
|
+
const idPaths = Boolean(options.idPaths);
|
|
165
|
+
const idForms = (fields) => {
|
|
166
|
+
if (!idPaths) return {};
|
|
167
|
+
const out = {};
|
|
168
|
+
if (fields.patches) out.idPatches = toIdPatch(fields.patches[0], fields.patches[1]);
|
|
169
|
+
if (fields.inversePatches) out.idInversePatches = toIdPatch(fields.inversePatches[0], fields.inversePatches[1]);
|
|
170
|
+
if (fields.redoPatches) out.idRedoPatches = toIdPatch(fields.redoPatches[0], fields.redoPatches[1]);
|
|
171
|
+
return out;
|
|
172
|
+
};
|
|
160
173
|
let snapshotCache;
|
|
161
174
|
|
|
162
175
|
function emit(event) {
|
|
@@ -178,7 +191,8 @@ export function createEditorSession(input, options = {}) {
|
|
|
178
191
|
const before = presentation;
|
|
179
192
|
let next, inversePatches;
|
|
180
193
|
try {
|
|
181
|
-
(
|
|
194
|
+
// `expectHash` (CE-04) is the precondition of this edit: the session's document must still be the one the caller hashed.
|
|
195
|
+
({ presentation: next, inverse: inversePatches } = applyPatchWithInverse(before, patches, applyOptions?.expectHash === undefined ? {} : { expectHash: applyOptions.expectHash }));
|
|
182
196
|
} catch (error) {
|
|
183
197
|
throw editorPatchError(error);
|
|
184
198
|
}
|
|
@@ -215,10 +229,12 @@ export function createEditorSession(input, options = {}) {
|
|
|
215
229
|
validation = nextValidation;
|
|
216
230
|
undoStack.push({ patches: clonePatchOperations(patches), inversePatches, meta });
|
|
217
231
|
redoStack.length = 0;
|
|
232
|
+
const ids = idForms({ patches: [before, patches], inversePatches: [next, inversePatches] });
|
|
218
233
|
emit({
|
|
219
234
|
type: "patch",
|
|
220
235
|
patches: clonePatchOperations(patches),
|
|
221
236
|
inversePatches: clonePatchOperations(inversePatches),
|
|
237
|
+
...ids,
|
|
222
238
|
validation,
|
|
223
239
|
meta
|
|
224
240
|
});
|
|
@@ -227,6 +243,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
227
243
|
presentation: clone(presentation),
|
|
228
244
|
patches: clonePatchOperations(patches),
|
|
229
245
|
inversePatches: clonePatchOperations(inversePatches),
|
|
246
|
+
...ids,
|
|
230
247
|
validation,
|
|
231
248
|
...(forks.length ? { forked: forks, notice: meta.notice } : {})
|
|
232
249
|
};
|
|
@@ -250,7 +267,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
250
267
|
setCatalogs(next, meta = {}) {
|
|
251
268
|
catalogs = mergeCatalogs(next);
|
|
252
269
|
validation = check(presentation);
|
|
253
|
-
emit({ type: "catalogs", patches: [], validation, meta });
|
|
270
|
+
emit({ type: "catalogs", patches: [], ...(idPaths ? { idPatches: [] } : {}), validation, meta });
|
|
254
271
|
return catalogs;
|
|
255
272
|
},
|
|
256
273
|
get canUndo() {
|
|
@@ -322,6 +339,27 @@ export function createEditorSession(input, options = {}) {
|
|
|
322
339
|
return setCatalogId(editor, path, catalogKind, id, meta);
|
|
323
340
|
},
|
|
324
341
|
applyPatch: commitPatch,
|
|
342
|
+
/**
|
|
343
|
+
* The content hash of the open document (core's `documentHash`): pass it as `applyPatch(ops, meta, { expectHash })` to apply an edit
|
|
344
|
+
* only while the document is still the one the hash was taken from. Reads the session's document without copying it.
|
|
345
|
+
*/
|
|
346
|
+
documentHash() {
|
|
347
|
+
return documentHash(presentation);
|
|
348
|
+
},
|
|
349
|
+
/**
|
|
350
|
+
* Give every slide and block without an `id` one (core's `assignIds`) as ONE undoable edit, and return the change, or `null` when
|
|
351
|
+
* every slide and block already has an id. `options` are core's (`generate`); `meta.source` defaults to `"assign-ids"`.
|
|
352
|
+
*/
|
|
353
|
+
assignIds(assignOptions = {}, meta = {}) {
|
|
354
|
+
let result;
|
|
355
|
+
try {
|
|
356
|
+
result = assignIds(presentation, assignOptions);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
throw error instanceof OPFIdError ? new OPFEditorError(error.code, error.message, { path: error.path }) : error;
|
|
359
|
+
}
|
|
360
|
+
if (!result.patch.length) return null;
|
|
361
|
+
return commitPatch(result.patch, { ...meta, source: meta.source ?? "assign-ids" });
|
|
362
|
+
},
|
|
325
363
|
/**
|
|
326
364
|
* Open a different document in the session (a host's example menu, File > Open, "start over"): the whole document is replaced as ONE
|
|
327
365
|
* undoable change (Undo restores the previous document, Redo the new one). Unlike an `applyPatch` that replaces the root, this is
|
|
@@ -386,19 +424,22 @@ export function createEditorSession(input, options = {}) {
|
|
|
386
424
|
redoStack.length = 0;
|
|
387
425
|
undoStack.push(...undo.map(entry));
|
|
388
426
|
redoStack.push(...redo.map(entry));
|
|
389
|
-
emit({ type: "restore", patches: [], validation, meta });
|
|
427
|
+
emit({ type: "restore", patches: [], ...(idPaths ? { idPatches: [] } : {}), validation, meta });
|
|
390
428
|
return { presentation: clone(presentation), patches: [], validation };
|
|
391
429
|
},
|
|
392
430
|
undo(meta = {}) {
|
|
393
431
|
const entry = undoStack.pop();
|
|
394
432
|
if (!entry) return null;
|
|
433
|
+
const before = presentation;
|
|
395
434
|
presentation = applyJsonPatch(presentation, entry.inversePatches);
|
|
396
435
|
validation = check(presentation);
|
|
397
436
|
redoStack.push(entry);
|
|
437
|
+
const ids = idForms({ patches: [before, entry.inversePatches], redoPatches: [presentation, entry.patches] });
|
|
398
438
|
emit({
|
|
399
439
|
type: "undo",
|
|
400
440
|
patches: clonePatchOperations(entry.inversePatches),
|
|
401
441
|
redoPatches: clonePatchOperations(entry.patches),
|
|
442
|
+
...ids,
|
|
402
443
|
validation,
|
|
403
444
|
meta
|
|
404
445
|
});
|
|
@@ -406,19 +447,23 @@ export function createEditorSession(input, options = {}) {
|
|
|
406
447
|
presentation: clone(presentation),
|
|
407
448
|
patches: clonePatchOperations(entry.inversePatches),
|
|
408
449
|
redoPatches: clonePatchOperations(entry.patches),
|
|
450
|
+
...ids,
|
|
409
451
|
validation
|
|
410
452
|
};
|
|
411
453
|
},
|
|
412
454
|
redo(meta = {}) {
|
|
413
455
|
const entry = redoStack.pop();
|
|
414
456
|
if (!entry) return null;
|
|
457
|
+
const before = presentation;
|
|
415
458
|
presentation = applyJsonPatch(presentation, entry.patches);
|
|
416
459
|
validation = check(presentation);
|
|
417
460
|
undoStack.push(entry);
|
|
461
|
+
const ids = idForms({ patches: [before, entry.patches], inversePatches: [presentation, entry.inversePatches] });
|
|
418
462
|
emit({
|
|
419
463
|
type: "redo",
|
|
420
464
|
patches: clonePatchOperations(entry.patches),
|
|
421
465
|
inversePatches: clonePatchOperations(entry.inversePatches),
|
|
466
|
+
...ids,
|
|
422
467
|
validation,
|
|
423
468
|
meta
|
|
424
469
|
});
|
|
@@ -426,6 +471,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
426
471
|
presentation: clone(presentation),
|
|
427
472
|
patches: clonePatchOperations(entry.patches),
|
|
428
473
|
inversePatches: clonePatchOperations(entry.inversePatches),
|
|
474
|
+
...ids,
|
|
429
475
|
validation
|
|
430
476
|
};
|
|
431
477
|
}
|
|
@@ -725,6 +771,7 @@ function editorPatchError(error) {
|
|
|
725
771
|
if (error.operation !== undefined) details.operation = error.operation;
|
|
726
772
|
if (error.index !== undefined) details.index = error.index;
|
|
727
773
|
if (error.validation) details.issues = errorFindings(error.validation);
|
|
774
|
+
if (error.code === "patch-hash-mismatch") Object.assign(details, { expected: error.expected, actual: error.actual });
|
|
728
775
|
return new OPFEditorError(error.code, error.message, details);
|
|
729
776
|
}
|
|
730
777
|
|
package/dist/json-editor.d.ts
CHANGED
|
@@ -29,6 +29,18 @@ export interface EditorProblem {
|
|
|
29
29
|
fixes?: EditorFix[];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** One host decoration of the code editor (CE-17): a mark over `from..to`, or the class on every line it touches. Offsets are source offsets. */
|
|
33
|
+
export interface EditorDecoration {
|
|
34
|
+
from: number;
|
|
35
|
+
to?: number;
|
|
36
|
+
className: string;
|
|
37
|
+
/** The browser tooltip of a mark. */
|
|
38
|
+
title?: string;
|
|
39
|
+
line?: boolean;
|
|
40
|
+
/** Kept as given, for `getDecorations`. */
|
|
41
|
+
data?: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
/** All offsets address the original source in UTF-16, including CRLF pairs. */
|
|
33
45
|
export interface JsonEditorApi {
|
|
34
46
|
/** Say `message` in the editor's live region (screen readers). */
|
|
@@ -37,6 +49,8 @@ export interface JsonEditorApi {
|
|
|
37
49
|
applyEdits(edits: SourceEdit[], options?: { caret?: number; select?: [number, number]; userEvent?: string }): void;
|
|
38
50
|
getValue(): string;
|
|
39
51
|
getSelection(): [number, number];
|
|
52
|
+
/** The end of the main selection the caret is at (equal to both ends of `getSelection()` for a plain caret), in source offsets. */
|
|
53
|
+
getHead(): number;
|
|
40
54
|
hasFocus(): boolean;
|
|
41
55
|
focus(): void;
|
|
42
56
|
setSelection(from: number, to?: number): void;
|
|
@@ -115,6 +129,10 @@ export interface JsonCodeEditorOptions {
|
|
|
115
129
|
/** Cmd/Ctrl+Shift+M: move focus to the host's problems list. Answer true when it did. */
|
|
116
130
|
onFocusProblems?(): boolean;
|
|
117
131
|
onFocus?(): void;
|
|
132
|
+
/** The selection or the text changed (every change, also one the host made). */
|
|
133
|
+
onSelectionChange?(): void;
|
|
134
|
+
/** The text changed (also by `update`). */
|
|
135
|
+
onDocChange?(): void;
|
|
118
136
|
onHoverField?(path: string | null): void;
|
|
119
137
|
onError?(error: unknown): void;
|
|
120
138
|
}
|
|
@@ -153,6 +171,21 @@ export interface JsonCodeEditor {
|
|
|
153
171
|
resetHistory(): void;
|
|
154
172
|
/** How many typed edit groups CodeMirror can still undo. */
|
|
155
173
|
undoDepth(): number;
|
|
174
|
+
/**
|
|
175
|
+
* CE-17: scroll the start of `from..to` (source offsets) into view without moving the selection or the focus. `align` is "center" (default),
|
|
176
|
+
* "start" or "nearest" (the least scrolling). `select` also selects the range and `focus` also focuses the editor. Folded text around the start is opened.
|
|
177
|
+
*/
|
|
178
|
+
reveal(from: number, to?: number, options?: { align?: "center" | "start" | "nearest"; select?: boolean; focus?: boolean }): void;
|
|
179
|
+
/**
|
|
180
|
+
* CE-17: draw decorations a host owns, kept apart from the editor's own. `key` names a set and a call replaces that set; an empty list
|
|
181
|
+
* removes it. Each item is a mark over `from..to` (source offsets), or with `line` the class on every line it touches. The decorations
|
|
182
|
+
* move with the text as it is edited.
|
|
183
|
+
*/
|
|
184
|
+
setDecorations(key: string, list: readonly EditorDecoration[]): void;
|
|
185
|
+
/** CE-17: remove the decoration set `key`, or every set when `key` is omitted. */
|
|
186
|
+
clearDecorations(key?: string): void;
|
|
187
|
+
/** CE-17: the decorations drawn now, at their current source offsets, in position order. A `line` decoration is one entry over all its lines. */
|
|
188
|
+
getDecorations(key?: string): (EditorDecoration & { key: string; to: number })[];
|
|
156
189
|
/** The CodeMirror `EditorView`. */
|
|
157
190
|
readonly view: import("@codemirror/view").EditorView;
|
|
158
191
|
openOptions(left?: number, top?: number): Promise<void>;
|