@openpresentation/opf-editor 0.24.3 → 0.25.1
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 +69 -6
- package/dist/galleries.js +2 -2
- package/dist/index.d.ts +8 -0
- package/dist/index.js +17 -0
- package/dist/layout-canvas.js +748 -0
- package/dist/layout-diagram.js +25 -0
- package/dist/layout-editor-ui.js +232 -0
- package/dist/layout-editor.d.ts +212 -0
- package/dist/layout-editor.js +197 -0
- package/dist/layout-inspector.js +339 -0
- package/dist/layout-layers.js +331 -0
- package/dist/layout-model.js +299 -0
- package/dist/layout-palette.js +77 -0
- package/dist/layout-samples.js +84 -0
- package/dist/layout-templates.d.ts +32 -8
- package/dist/layout-templates.js +89 -37
- package/dist/layout-tree.d.ts +87 -0
- package/dist/layout-tree.js +704 -0
- package/dist/persistence.js +2 -2
- package/dist/presenter.d.ts +4 -1
- package/dist/presenter.js +5 -3
- package/dist/slide-manager.js +3 -7
- package/dist/source-pane.d.ts +19 -1
- package/dist/source-pane.js +77 -1
- package/dist/styles.css +852 -31
- package/package.json +15 -10
- package/dist/custom-layout-editor.d.ts +0 -7
- package/dist/custom-layout-editor.js +0 -105
package/dist/persistence.js
CHANGED
|
@@ -268,7 +268,7 @@ export function createPersistence(editor, options = {}) {
|
|
|
268
268
|
}
|
|
269
269
|
} else {
|
|
270
270
|
// The user already worked in this session: bring the copy back as one undoable change instead of replacing their history.
|
|
271
|
-
editor.
|
|
271
|
+
editor.replaceDocument(structuredClone(record.presentation), { source: "restore" });
|
|
272
272
|
}
|
|
273
273
|
touched = true;
|
|
274
274
|
baseline = record.dirty === false ? jsonOf(editor.presentation) : null;
|
|
@@ -315,7 +315,7 @@ export function createPersistence(editor, options = {}) {
|
|
|
315
315
|
let record;
|
|
316
316
|
try { record = await adapter.get(earlierKey); } catch { return false; }
|
|
317
317
|
if (!validRecord(record)) return false;
|
|
318
|
-
try { editor.
|
|
318
|
+
try { editor.replaceDocument(structuredClone(record.presentation), { source: "restore" }); }
|
|
319
319
|
catch { return false; }
|
|
320
320
|
pending = undefined;
|
|
321
321
|
touched = true;
|
package/dist/presenter.d.ts
CHANGED
|
@@ -83,16 +83,19 @@ export interface PresentButtonOptions {
|
|
|
83
83
|
density?: "comfortable" | "touch";
|
|
84
84
|
/** The accessible name (default "Present"). */
|
|
85
85
|
label?: string;
|
|
86
|
+
/** `"quiet"` (default): a transparent icon button like the toolbar's undo and redo. `"primary"`: the filled accent look of the Export trigger. */
|
|
87
|
+
variant?: "quiet" | "primary";
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
export interface PresentButton {
|
|
89
91
|
readonly element: HTMLButtonElement;
|
|
90
92
|
setDisabled(reason?: boolean | string): void;
|
|
91
93
|
setDensity(density: "comfortable" | "touch"): void;
|
|
94
|
+
setVariant(variant: "quiet" | "primary"): void;
|
|
92
95
|
destroy(): void;
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
/** The icon-only Present button: a Lucide Presentation icon, `aria-label="Present"`, title "Present (F5)"
|
|
98
|
+
/** The icon-only Present button: a Lucide Presentation icon, `aria-label="Present"`, title "Present (F5)". Quiet by default (a transparent icon button like undo and redo); `variant: "primary"` styles it like the Export trigger. */
|
|
96
99
|
export declare function createPresentButton(container: HTMLElement, options?: PresentButtonOptions): PresentButton;
|
|
97
100
|
|
|
98
101
|
/**
|
package/dist/presenter.js
CHANGED
|
@@ -372,9 +372,10 @@ function isInteractiveTarget(target) {
|
|
|
372
372
|
* Mount the Present button in `container`: an icon-only button (Lucide Presentation) named "Present", titled "Present (F5)".
|
|
373
373
|
*
|
|
374
374
|
* Options: `onPresent(event)`, `disabled` (`true`, or a string that says why and is the title), `density` (`"comfortable"` 32px, or `"touch"` 40px)
|
|
375
|
-
*
|
|
375
|
+
* `label` (the accessible name, default "Present") and `variant` (`"quiet"`, the default: a transparent icon button like the toolbar's undo and redo,
|
|
376
|
+
* or `"primary"`: the filled accent look of the Export trigger). A disabled button stays focusable (`aria-disabled`) and does nothing.
|
|
376
377
|
*
|
|
377
|
-
* Returns `{ element, setDisabled(reason), setDensity(density), destroy() }`.
|
|
378
|
+
* Returns `{ element, setDisabled(reason), setDensity(density), setVariant(variant), destroy() }`.
|
|
378
379
|
*/
|
|
379
380
|
export function createPresentButton(container, options = {}) {
|
|
380
381
|
if (!container?.ownerDocument) throw new TypeError("A DOM container is required.");
|
|
@@ -384,7 +385,7 @@ export function createPresentButton(container, options = {}) {
|
|
|
384
385
|
let destroyed = false;
|
|
385
386
|
const button = el(doc, "button", {
|
|
386
387
|
type: "button", class: "opf-present-button opf-ui", "aria-label": label, title: "Present (F5)", "aria-keyshortcuts": "F5",
|
|
387
|
-
"data-density": options.density === "touch" ? "touch" : "comfortable",
|
|
388
|
+
"data-density": options.density === "touch" ? "touch" : "comfortable", "data-variant": options.variant === "primary" ? "primary" : "quiet",
|
|
388
389
|
}, icon(doc, "Presentation", 16));
|
|
389
390
|
const paint = () => {
|
|
390
391
|
if (disabledReason) { button.setAttribute("aria-disabled", "true"); button.title = disabledReason; }
|
|
@@ -404,6 +405,7 @@ export function createPresentButton(container, options = {}) {
|
|
|
404
405
|
paint();
|
|
405
406
|
},
|
|
406
407
|
setDensity(next) { button.dataset.density = next === "touch" ? "touch" : "comfortable"; },
|
|
408
|
+
setVariant(next) { button.dataset.variant = next === "primary" ? "primary" : "quiet"; },
|
|
407
409
|
destroy() { destroyed = true; button.remove(); },
|
|
408
410
|
};
|
|
409
411
|
}
|
package/dist/slide-manager.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { listSwitchOptions } from "./switches.js";
|
|
10
10
|
import { layoutRegionSummary } from "./layout-templates.js";
|
|
11
11
|
import { groupLayoutChoices } from "./layout-templates.js";
|
|
12
|
+
import { buildLayoutDiagram } from "./layout-diagram.js";
|
|
12
13
|
import { switchDimension } from "./switches.js";
|
|
13
14
|
|
|
14
15
|
let instances = 0;
|
|
@@ -521,13 +522,8 @@ export function createSlideManager(container, options) {
|
|
|
521
522
|
template.replaceChildren(); currentPreview.replaceChildren();
|
|
522
523
|
if (choice?.record?.preview && typeof choice.record.preview === "string") {
|
|
523
524
|
template.append(el("img", { src: choice.record.preview, alt: `${choice.label} template preview` }));
|
|
524
|
-
} else if (Array.isArray(choice?.record?.
|
|
525
|
-
|
|
526
|
-
diagram.style.gridTemplateAreas = choice.record.areas.map((row) => `"${row.join(" ")}"`).join(" ");
|
|
527
|
-
diagram.style.gridTemplateColumns = `repeat(${choice.record.areas[0]?.length ?? 1}, 1fr)`;
|
|
528
|
-
const areas = [...new Set(choice.record.areas.flat())].filter((area) => area !== ".");
|
|
529
|
-
for (const area of areas) { const part = el("span", { text: area }); part.style.gridArea = area; diagram.append(part); }
|
|
530
|
-
template.append(diagram);
|
|
525
|
+
} else if (Array.isArray(choice?.record?.regions)) {
|
|
526
|
+
template.append(buildLayoutDiagram(document, choice.record));
|
|
531
527
|
}
|
|
532
528
|
if (choice && options.renderLayoutPreview) {
|
|
533
529
|
try { currentPreview.innerHTML = options.renderLayoutPreview(deck(), current(), choice) ?? ""; }
|
package/dist/source-pane.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EditorSession } from "./index.js";
|
|
1
|
+
import type { EditorChange, EditorSession } from "./index.js";
|
|
2
2
|
import type { JsonCatalogContext } from "./json-options.js";
|
|
3
3
|
import type { JsonCodeEditor } from "./json-editor.js";
|
|
4
4
|
|
|
@@ -64,8 +64,26 @@ export interface SourcePaneOptions {
|
|
|
64
64
|
onError?(error: unknown): void;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export interface ReplaceDocumentOptions {
|
|
68
|
+
/** The JSON text the document was read from. Used when it parses to the document, so its spelling is kept. */
|
|
69
|
+
source?: string;
|
|
70
|
+
/** The file name without extension, as `setName` (the document's own name is not used). */
|
|
71
|
+
name?: string;
|
|
72
|
+
/** Extra `meta` for the session change (`source` defaults to `"replace-document"`). */
|
|
73
|
+
meta?: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
export interface SourcePane {
|
|
68
77
|
element: HTMLElement;
|
|
78
|
+
/**
|
|
79
|
+
* Open a different document in the session and the pane (a host's example menu, File > Open): `editor.replaceDocument`, so it is one
|
|
80
|
+
* undoable change and no catalog record is forked into `catalogs.custom` (a diff typed or pasted into the pane is an edit; this is not).
|
|
81
|
+
* `document` is a parsed document or JSON text. The pane shows `options.source` (the JSON text the document was read from, kept byte for
|
|
82
|
+
* byte; `document` itself when it is text) in JSON and the document written in YAML or Markdown. Undo and Redo bring back each
|
|
83
|
+
* document's text exactly, in the format that is shown. A document the session refuses throws (`invalid-opf-edit` with `issues`) and
|
|
84
|
+
* changes nothing. Works in a read-only pane.
|
|
85
|
+
*/
|
|
86
|
+
replaceDocument(document: unknown, options?: ReplaceDocumentOptions): EditorChange;
|
|
69
87
|
/** Re-read the document from the session. */
|
|
70
88
|
refresh(): void;
|
|
71
89
|
/** Replace the host findings; `null` returns to the session's own validation. */
|
package/dist/source-pane.js
CHANGED
|
@@ -2,7 +2,7 @@ import { parse as parseJsonc, printParseErrorCode } from "jsonc-parser";
|
|
|
2
2
|
import { fromYaml, scanYamlComments, toYaml } from "@openpresentation/opf/yaml";
|
|
3
3
|
import { fromMarkdown, toMarkdown } from "@openpresentation/opf/markdown";
|
|
4
4
|
import { mountJsonCodeEditor } from "./json-editor.js";
|
|
5
|
-
import { createSourceMemory, findDuplicateKey, updateJsonSource } from "./exact-source.js";
|
|
5
|
+
import { MAX_EXACT_SOURCE_LENGTH, createSourceMemory, findDuplicateKey, updateJsonSource } from "./exact-source.js";
|
|
6
6
|
import { lineForPath, lineOfOffset, offsetOfLine } from "./source-lines.js";
|
|
7
7
|
import { errorText, normalizeFinding, rejectionFindings as findingsOfError } from "./source-findings.js";
|
|
8
8
|
|
|
@@ -127,6 +127,17 @@ export function createSourcePane(container, options = {}) {
|
|
|
127
127
|
// Whether the findings include an error, and what the editor's `aria-invalid` currently says (null: not yet set).
|
|
128
128
|
let invalid = false, shownInvalid = null;
|
|
129
129
|
const lineCache = { key: "", values: new Map() };
|
|
130
|
+
// The exact text of the documents `replaceDocument` replaced and opened, per format, so Undo and Redo show it again byte for byte
|
|
131
|
+
// (a YAML comment, the author's JSON spelling). Keyed by format and document; the oldest entries go first.
|
|
132
|
+
const exactTexts = new Map();
|
|
133
|
+
const rememberText = (target, value, text) => {
|
|
134
|
+
if (typeof text !== "string" || text.length > MAX_EXACT_SOURCE_LENGTH) return;
|
|
135
|
+
const key = `${target}|${JSON.stringify(value)}`;
|
|
136
|
+
exactTexts.delete(key);
|
|
137
|
+
exactTexts.set(key, text);
|
|
138
|
+
while (exactTexts.size > 8) exactTexts.delete(exactTexts.keys().next().value);
|
|
139
|
+
};
|
|
140
|
+
const exactText = (target, value) => exactTexts.get(`${target}|${JSON.stringify(value)}`);
|
|
130
141
|
|
|
131
142
|
// ---- DOM ----------------------------------------------------------------------------------------------------------
|
|
132
143
|
const root = el("div", "opf-source opf-ui", { "data-density": density });
|
|
@@ -315,6 +326,12 @@ export function createSourcePane(container, options = {}) {
|
|
|
315
326
|
const before = control.api.getValue(), comments = scanYamlComments(before).count;
|
|
316
327
|
if (comments > 0 && written.text !== before) transientNotes = [normalizeFinding({ message: `${plural(comments, "comment")} in the YAML ${comments === 1 ? "was" : "were"} dropped when the document changed outside the source.`, severity: "warning" }, "format")];
|
|
317
328
|
}
|
|
329
|
+
// A document `replaceDocument` opened or replaced comes back with the exact text it had.
|
|
330
|
+
const exact = exactText(format, deck());
|
|
331
|
+
if (exact !== undefined && exact !== written.text) {
|
|
332
|
+
try { if (same(readText(format, exact, catalogs()).deck, deck())) { written = { ...written, text: exact }; if (format === "json") jsonText = exact; } }
|
|
333
|
+
catch { /* The remembered text no longer reads as this document: use the generated one. */ }
|
|
334
|
+
}
|
|
318
335
|
noteFindings = written.notes;
|
|
319
336
|
if (format !== "json") syncJsonSafe();
|
|
320
337
|
if (written.text !== control.api.getValue()) show(written.text);
|
|
@@ -332,6 +349,60 @@ export function createSourcePane(container, options = {}) {
|
|
|
332
349
|
fromSession();
|
|
333
350
|
});
|
|
334
351
|
|
|
352
|
+
// ---- Replacing the whole document ----------------------------------------------------------------------------------
|
|
353
|
+
/**
|
|
354
|
+
* Open `input` (a document, or JSON text) as the whole document with `editor.replaceDocument`: one undoable change, no catalog record
|
|
355
|
+
* forked. The text shown is `options.source` (or `input` when it is text) in JSON, the document written in YAML or Markdown, and Undo
|
|
356
|
+
* and Redo bring each document's text back exactly.
|
|
357
|
+
*/
|
|
358
|
+
function replaceDocument(input, replaceOptions = {}) {
|
|
359
|
+
if (destroyed) throw new Error("The source pane was destroyed.");
|
|
360
|
+
flush();
|
|
361
|
+
const before = deck();
|
|
362
|
+
const beforeText = dirty ? undefined : control.api.getValue();
|
|
363
|
+
const authored = typeof input === "string" ? input : replaceOptions.source;
|
|
364
|
+
transientNotes = [];
|
|
365
|
+
let change;
|
|
366
|
+
applying = true;
|
|
367
|
+
try {
|
|
368
|
+
// Remember the spelling of the document being replaced, then open the new one.
|
|
369
|
+
syncJson(before);
|
|
370
|
+
change = editor.replaceDocument(input, { format, ...replaceOptions.meta });
|
|
371
|
+
} catch (error) {
|
|
372
|
+
rejectionFindings = error?.issues?.length ? findingsOfError(error) : [normalizeFinding({ message: errorText(error), kind: "rejected" })];
|
|
373
|
+
renderFindings();
|
|
374
|
+
throw error;
|
|
375
|
+
} finally { applying = false; }
|
|
376
|
+
clearTimeout(timer);
|
|
377
|
+
timer = null;
|
|
378
|
+
for (const key of Object.keys(drafts)) delete drafts[key];
|
|
379
|
+
syntaxFindings = [];
|
|
380
|
+
rejectionFindings = [];
|
|
381
|
+
dirty = false;
|
|
382
|
+
version++;
|
|
383
|
+
const opened = deck();
|
|
384
|
+
let written;
|
|
385
|
+
let authoredJson = null;
|
|
386
|
+
if (typeof authored === "string") { try { if (same(JSON.parse(authored), opened) && !findDuplicateKey(authored)) authoredJson = authored; } catch { /* Not the document's JSON: write it. */ } }
|
|
387
|
+
if (replaceOptions.name !== undefined) { name = sanitizeName(replaceOptions.name) || "deck"; updateBar(); }
|
|
388
|
+
try { written = format === "json" && authoredJson !== null ? { text: authoredJson, notes: [] } : write(format, opened); }
|
|
389
|
+
catch (error) {
|
|
390
|
+
syncJsonSafe();
|
|
391
|
+
rejectionFindings = [normalizeFinding({ message: `The document cannot be written as ${formatOf(format).label}: ${errorText(error)}. The text below is the last version that could be.`, kind: "rejected" })];
|
|
392
|
+
renderFindings();
|
|
393
|
+
return change;
|
|
394
|
+
}
|
|
395
|
+
if (authoredJson !== null) jsonText = authoredJson; else syncJsonSafe();
|
|
396
|
+
rememberText(format, opened, written.text);
|
|
397
|
+
rememberText(format, before, beforeText);
|
|
398
|
+
noteFindings = written.notes;
|
|
399
|
+
if (written.text !== control.api.getValue()) show(written.text); else baseline = written.text;
|
|
400
|
+
renderedKey = "";
|
|
401
|
+
renderFindings();
|
|
402
|
+
syncAttributes();
|
|
403
|
+
return change;
|
|
404
|
+
}
|
|
405
|
+
|
|
335
406
|
// ---- Format -------------------------------------------------------------------------------------------------------
|
|
336
407
|
function setFormatInternal(next, notify) {
|
|
337
408
|
const target = formatOf(next);
|
|
@@ -627,6 +698,11 @@ export function createSourcePane(container, options = {}) {
|
|
|
627
698
|
|
|
628
699
|
return {
|
|
629
700
|
element: root,
|
|
701
|
+
/**
|
|
702
|
+
* Open a different document: `editor.replaceDocument` as one undoable change that forks no catalog record, with the text shown in
|
|
703
|
+
* the current format. `options.source` is the JSON text the document was read from (kept byte for byte); `options.name` renames the file.
|
|
704
|
+
*/
|
|
705
|
+
replaceDocument,
|
|
630
706
|
/** Re-read the document from the session (for example after the host changed it without events). */
|
|
631
707
|
refresh() { version++; fromSession(false); },
|
|
632
708
|
/** Replace the host findings; `setFindings(null)` returns to the session's own validation. */
|