@openpresentation/opf-editor 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/dist/canvas.d.ts +13 -1
- package/dist/canvas.js +39 -9
- package/dist/chart-options-panel.js +16 -16
- package/dist/codemirror-guard.js +69 -0
- package/dist/content-controls.js +127 -74
- package/dist/data-grid.js +15 -7
- package/dist/design-controls.d.ts +13 -0
- package/dist/design-controls.js +134 -124
- package/dist/exact-source.d.ts +30 -0
- package/dist/font-gate.js +20 -1
- package/dist/json-editor.js +8 -2
- package/dist/json-options.d.ts +2 -1
- package/dist/json-options.js +7 -6
- package/dist/layout-templates.d.ts +8 -3
- package/dist/layout-templates.js +30 -16
- package/dist/numbering-panel.js +2 -3
- package/dist/react.d.ts +12 -4
- package/dist/react.js +39 -5
- package/dist/slide-adapter.d.ts +49 -0
- package/dist/slide-adapter.js +289 -0
- package/dist/slide-edits.d.ts +46 -0
- package/dist/slide-edits.js +629 -0
- package/dist/slide-manager.js +2 -1
- package/dist/slide-scene.d.ts +96 -0
- package/dist/slide-scene.js +413 -0
- package/dist/source-checker.js +45 -6
- package/dist/source-completions.js +6 -4
- package/dist/source-pane.d.ts +18 -6
- package/dist/source-pane.js +45 -0
- package/dist/source-worker.js +8 -4
- package/dist/studio-canvas.d.ts +34 -0
- package/dist/studio-canvas.js +586 -0
- package/dist/studio-commands.d.ts +15 -0
- package/dist/studio-commands.js +155 -0
- package/dist/studio-design.d.ts +16 -0
- package/dist/studio-design.js +354 -0
- package/dist/studio-kit/dom.d.ts +16 -0
- package/dist/studio-kit/dom.js +42 -6
- package/dist/studio-layers.d.ts +12 -0
- package/dist/studio-layers.js +218 -0
- package/dist/studio.d.ts +31 -0
- package/dist/studio.js +1608 -0
- package/dist/styles.css +1546 -117
- package/dist/thumbnail-cache.d.ts +2 -0
- package/dist/thumbnail-cache.js +10 -1
- package/dist/workspace-measure.js +6 -4
- package/dist/workspace-parts.js +20 -0
- package/dist/workspace-shell.js +25 -15
- package/dist/workspace-theme.js +3 -0
- package/dist/workspace.d.ts +88 -9
- package/dist/workspace.js +210 -49
- package/package.json +29 -18
package/dist/workspace.js
CHANGED
|
@@ -7,29 +7,13 @@
|
|
|
7
7
|
// A host picks a layout: `"full"` (a bar, the source, the slide, the slide row and a side panel are built for it), `"canvas"`, `"source"`,
|
|
8
8
|
// or `slots`, an object of its own elements, one per part, for a host that arranges the page itself. Each part is optional (`panels`).
|
|
9
9
|
// Nothing here fetches anything, stores anything on its own or talks to a service; the host supplies fonts, catalogs and storage.
|
|
10
|
-
import { toSvg } from "@openpresentation/opf-render/svg";
|
|
11
10
|
import { OPFEditorError, createEditorSession, mergeCatalogs } from "./index.js";
|
|
12
|
-
import { createCanvasEditor } from "./canvas.js";
|
|
13
11
|
import { whenFontsReady } from "./font-gate.js";
|
|
14
12
|
import { createSourcePane } from "./source-pane.js";
|
|
15
|
-
import { createSlideNavigator } from "./slide-navigator.js";
|
|
16
|
-
import { createThumbnailStrip } from "./thumbnail-strip.js";
|
|
17
|
-
import { createThumbnailCache } from "./thumbnail-cache.js";
|
|
18
|
-
import { createSlideManager } from "./slide-manager.js";
|
|
19
|
-
import { createOutlineView } from "./outline-view.js";
|
|
20
|
-
import { createExportMenu } from "./export-menu.js";
|
|
21
|
-
import { bindPresentShortcut, createPresentButton, createPresenter } from "./presenter.js";
|
|
22
|
-
import { createReviewPanel } from "./review-panel.js";
|
|
23
|
-
import { createFindPanel, installFindShortcuts } from "./find-panel.js";
|
|
24
|
-
import { createDesignControls } from "./design-controls.js";
|
|
25
13
|
import { createPersistence } from "./persistence.js";
|
|
26
14
|
import { createPersistenceUi } from "./persistence-ui.js";
|
|
27
|
-
import { prepareDimensionSwitch } from "./switches.js";
|
|
28
|
-
import { mergeSlides, splitSlideByBlocks } from "./content-actions.js";
|
|
29
|
-
import { installCanvasVariableEditing } from "./canvas-variables.js";
|
|
30
|
-
import { createLayoutStudio } from "./workspace-layouts.js";
|
|
31
|
-
import { fontSubstitutionNotes, layoutOptionsFor } from "./workspace-measure.js";
|
|
32
15
|
import { buildWorkspaceShell } from "./workspace-shell.js";
|
|
16
|
+
import { fontSubstitutionNotes, layoutOptionsFor } from "./workspace-measure.js";
|
|
33
17
|
import { SOURCE_CHECKS, copyText, downloadSourceFile, lf, reportSource } from "./workspace-source.js";
|
|
34
18
|
import { createThemeController } from "./workspace-theme.js";
|
|
35
19
|
import { retainedSlideIndex } from "./slide-retention.js";
|
|
@@ -39,6 +23,34 @@ export { THEME_TOKENS, createThemeController, normalizeTheme, normalizeThemeToke
|
|
|
39
23
|
// The script-aware measurement options for composing one slide: a host that calls `composeSlide` or `paginateSlide` on the session passes these (the playground does).
|
|
40
24
|
export { layoutOptionsFor } from "./workspace-measure.js";
|
|
41
25
|
|
|
26
|
+
// The slide and everything around it (the renderer, the canvas, the slide row, the presenter, Export, Review, Find, Design and the views) load
|
|
27
|
+
// on demand, in one chunk (`workspace-parts.js`): a host that mounts only the source pane never loads the renderer. Its factories are bound
|
|
28
|
+
// here once the chunk has arrived, before any part is mounted.
|
|
29
|
+
let toSvg, createCanvasEditor, createSlideNavigator, createThumbnailStrip, createThumbnailCache, createSlideManager, createOutlineView, createExportMenu;
|
|
30
|
+
let bindPresentShortcut, createPresentButton, createPresenter, createReviewPanel, createFindPanel, installFindShortcuts, createDesignControls;
|
|
31
|
+
let prepareDimensionSwitch, mergeSlides, splitSlideByBlocks, installCanvasVariableEditing, createLayoutStudio, buildStudioShell, createStudio;
|
|
32
|
+
let partsModule;
|
|
33
|
+
let partsLoaded = false;
|
|
34
|
+
function loadParts() {
|
|
35
|
+
partsModule ??= import("./workspace-parts.js").then((parts) => {
|
|
36
|
+
({ toSvg, createCanvasEditor, createSlideNavigator, createThumbnailStrip, createThumbnailCache, createSlideManager, createOutlineView, createExportMenu } = parts);
|
|
37
|
+
({ bindPresentShortcut, createPresentButton, createPresenter, createReviewPanel, createFindPanel, installFindShortcuts, createDesignControls } = parts);
|
|
38
|
+
({ prepareDimensionSwitch, mergeSlides, splitSlideByBlocks, installCanvasVariableEditing, createLayoutStudio, buildStudioShell, createStudio } = parts);
|
|
39
|
+
partsLoaded = true;
|
|
40
|
+
});
|
|
41
|
+
// A chunk that failed to load is asked for again by the next editor.
|
|
42
|
+
partsModule.catch(() => {
|
|
43
|
+
partsModule = undefined;
|
|
44
|
+
});
|
|
45
|
+
return partsModule;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether the parts to mount need the slide chunk: any part but the source pane, its autosave and the history buttons, that has an element to
|
|
49
|
+
* mount in (the presenter mounts in the overlay).
|
|
50
|
+
*/
|
|
51
|
+
const SOURCE_ONLY_PARTS = new Set(["source", "persistence", "history"]);
|
|
52
|
+
const needsSlideParts = (parts, slots) => Object.entries(parts).some(([name, on]) => on && !SOURCE_ONLY_PARTS.has(name) && (name === "present" || Boolean(slots[name])));
|
|
53
|
+
|
|
42
54
|
const VIEWS = Object.freeze(["slide", "sorter", "outline", "layouts"]);
|
|
43
55
|
const MUTATORS = new Set(["set", "setGroupComposition", "setCatalog", "setCatalogs", "applyPatch", "replaceDocument", "restoreState", "undo", "redo", "setComposition", "paginateSlide"]);
|
|
44
56
|
const STARTER = Object.freeze({ name: "Untitled presentation", slides: [{ id: "slide-1", title: "Untitled slide" }] });
|
|
@@ -57,7 +69,11 @@ const LAYOUT_PARTS = {
|
|
|
57
69
|
full: { source: true, navigator: true, thumbnails: true, history: true, find: true, present: true, export: true, review: true, layouts: true, sorter: true, outline: true },
|
|
58
70
|
canvas: { navigator: true, thumbnails: true, history: true },
|
|
59
71
|
source: { source: true },
|
|
72
|
+
// CE-20: the Studio and its Playground preset arrange the same parts; the Studio mounts its own Design panel.
|
|
73
|
+
studio: { source: true, navigator: true, thumbnails: true, history: true, find: true, present: true, export: true, review: true, layouts: true, slides: true },
|
|
74
|
+
playground: { source: true, navigator: true, thumbnails: true, history: true, find: true, present: true, export: true, review: true, layouts: true, slides: true },
|
|
60
75
|
};
|
|
76
|
+
const STUDIO_LAYOUTS = new Set(["studio", "playground"]);
|
|
61
77
|
|
|
62
78
|
const isThenable = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
|
|
63
79
|
|
|
@@ -110,7 +126,9 @@ function resolveParts(options, layout, slots) {
|
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
/**
|
|
113
|
-
* Mount the editor in `element`. Returns the handle at once;
|
|
129
|
+
* Mount the editor in `element`. Returns the handle at once; the parts mount once a `fonts` promise has resolved and, for a layout that shows
|
|
130
|
+
* slides, once the slide chunk has loaded (`mounted` is emitted then, and `ready` settles after the first update). A source-only editor whose
|
|
131
|
+
* fonts are at hand mounts before this returns.
|
|
114
132
|
* See `workspace.d.ts` for the options and the handle.
|
|
115
133
|
*/
|
|
116
134
|
export function createOpfEditor(element, options = {}) {
|
|
@@ -165,8 +183,9 @@ export function createOpfEditor(element, options = {}) {
|
|
|
165
183
|
|
|
166
184
|
// ---- layout, parts and slots ------------------------------------------------------------------------------------------------
|
|
167
185
|
const given = options.slots ?? {};
|
|
168
|
-
const layout = options.slots ? "slots" : (options.layout ?? "full");
|
|
169
|
-
if (!["full", "canvas", "source", "slots"].includes(layout)) throw new RangeError('layout must be "full", "canvas" or "
|
|
186
|
+
const layout = options.slots && !STUDIO_LAYOUTS.has(options.layout) ? "slots" : (options.layout ?? "full");
|
|
187
|
+
if (!["full", "canvas", "source", "slots", "studio", "playground"].includes(layout)) throw new RangeError('layout must be "full", "canvas", "source", "studio" or "playground", or pass `slots`.');
|
|
188
|
+
const studioLayout = STUDIO_LAYOUTS.has(layout);
|
|
170
189
|
const parts = resolveParts(options, layout, given);
|
|
171
190
|
const views = VIEWS.filter((view) => view === "slide" || parts[view]);
|
|
172
191
|
if (options.view !== undefined && !views.includes(options.view)) throw new RangeError(`view "${options.view}" is not available in this layout (${views.join(", ")}).`);
|
|
@@ -183,13 +202,27 @@ export function createOpfEditor(element, options = {}) {
|
|
|
183
202
|
const themeController = createThemeController(element, options.theme);
|
|
184
203
|
let slots = { ...given };
|
|
185
204
|
let controls = {};
|
|
205
|
+
let studioShell = null;
|
|
206
|
+
let overlay = root;
|
|
207
|
+
// CE-20: the Studio builds its own shell; a host's slots add parts to it (`topbarEnd`, `overlay`). Its code is in the slide chunk, so the Studio
|
|
208
|
+
// shell is built once that has loaded, right before the parts mount.
|
|
209
|
+
function buildStudio() {
|
|
210
|
+
const built = buildStudioShell(root, { parts, layout, labels: options.labels, studio: options.studio, slots: given });
|
|
211
|
+
const { topbarEnd: _placed, ...rest } = given;
|
|
212
|
+
slots = { ...built.slots, ...rest };
|
|
213
|
+
controls = built.controls;
|
|
214
|
+
studioShell = built.shell;
|
|
215
|
+
overlay = slots.overlay ?? root;
|
|
216
|
+
}
|
|
186
217
|
if (layout === "slots") root.setAttribute("data-opf-layout", "slots");
|
|
187
|
-
else {
|
|
218
|
+
else if (!studioLayout) {
|
|
188
219
|
const built = buildWorkspaceShell(root, { parts, views, layout, labels: options.labels });
|
|
189
220
|
slots = { ...built.slots, ...given };
|
|
190
221
|
controls = built.controls;
|
|
191
222
|
}
|
|
192
|
-
|
|
223
|
+
overlay = slots.overlay ?? root;
|
|
224
|
+
// The slide chunk is loaded before the parts mount, unless only the source pane (with its autosave) is mounted. The Studio always needs it.
|
|
225
|
+
const slideParts = studioLayout || needsSlideParts(parts, slots);
|
|
193
226
|
|
|
194
227
|
// ---- state ------------------------------------------------------------------------------------------------------------------
|
|
195
228
|
let fonts;
|
|
@@ -201,6 +234,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
201
234
|
let sorter;
|
|
202
235
|
let outline;
|
|
203
236
|
let studio;
|
|
237
|
+
let studioCtl;
|
|
238
|
+
let fontsPending = false;
|
|
204
239
|
let presenter;
|
|
205
240
|
let presentButton;
|
|
206
241
|
let exportMenu;
|
|
@@ -234,7 +269,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
234
269
|
let frameSize = { width: 0, height: 0 };
|
|
235
270
|
const draftMemory = createSourceMemory();
|
|
236
271
|
const variablesMode = options.variables === "resolved" ? "resolved" : "authored";
|
|
237
|
-
const fitMode = options.fit === undefined ? (layout === "slots" ? false : "contain") : options.fit;
|
|
272
|
+
const fitMode = studioLayout ? false : options.fit === undefined ? (layout === "slots" ? false : "contain") : options.fit;
|
|
238
273
|
|
|
239
274
|
const say = (message) => {
|
|
240
275
|
statusText = message;
|
|
@@ -255,7 +290,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
255
290
|
|
|
256
291
|
// ---- navigation and the refresh loop ----------------------------------------------------------------------------------------
|
|
257
292
|
function renderViews(deck) {
|
|
258
|
-
|
|
293
|
+
// The Studio's filmstrip draws the thumbnails it shows itself, as they scroll in.
|
|
294
|
+
if (!studioLayout) thumbnailCache?.prepare(deck);
|
|
259
295
|
slideManager?.render();
|
|
260
296
|
if (activeView === "sorter") sorter?.render();
|
|
261
297
|
if (activeView === "outline") outline?.render();
|
|
@@ -283,7 +319,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
283
319
|
}
|
|
284
320
|
function sync() {
|
|
285
321
|
fonts?.registry?.clearSubstitutions?.();
|
|
286
|
-
|
|
322
|
+
// The frozen snapshot (one object per version): the session's `presentation` copies the whole deck on every read.
|
|
323
|
+
const deck = session.snapshot().presentation;
|
|
287
324
|
slideIndex = Math.max(0, Math.min(slideIndex, deck.slides.length - 1));
|
|
288
325
|
renderViews(deck);
|
|
289
326
|
slideNav?.setSlide(slideIndex);
|
|
@@ -300,16 +337,18 @@ export function createOpfEditor(element, options = {}) {
|
|
|
300
337
|
if (!mounted || destroyed) return Promise.resolve();
|
|
301
338
|
const token = ++refreshToken;
|
|
302
339
|
let loaded = false;
|
|
303
|
-
lastRefresh = whenFontsReady(fonts, session.presentation, {
|
|
340
|
+
lastRefresh = whenFontsReady(fonts, session.snapshot().presentation, {
|
|
304
341
|
renderOptions: renderOptions(),
|
|
305
342
|
isCurrent: () => token === refreshToken && !destroyed,
|
|
306
343
|
loading: () => {
|
|
307
344
|
loaded = true;
|
|
345
|
+
fontsPending = true;
|
|
308
346
|
emit("fonts", { state: "loading" });
|
|
309
347
|
say(MESSAGES.loadingFonts);
|
|
310
348
|
},
|
|
311
349
|
ready: () => {
|
|
312
350
|
fontsFailure = undefined;
|
|
351
|
+
fontsPending = false;
|
|
313
352
|
if (loaded) {
|
|
314
353
|
thumbnailCache?.clear();
|
|
315
354
|
emit("fonts", { state: "ready" });
|
|
@@ -354,7 +393,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
354
393
|
}
|
|
355
394
|
function goTo(index, path) {
|
|
356
395
|
if (index !== slideIndex && !go(index)) return false;
|
|
357
|
-
if (
|
|
396
|
+
if (studioCtl) studioCtl.revealPath(path ?? `slides.${index}`);
|
|
397
|
+
else if (path) canvas?.reveal(path);
|
|
358
398
|
return true;
|
|
359
399
|
}
|
|
360
400
|
|
|
@@ -367,7 +407,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
367
407
|
if (controls.redo) controls.redo.disabled = !inSync || readOnly || !session.canRedo;
|
|
368
408
|
// Text in the source pane that is not a valid deck: the slide shows the last valid one and is not editable until the text is fixed.
|
|
369
409
|
const frame = slots.canvas && (slots.slide ?? slots.canvas);
|
|
370
|
-
|
|
410
|
+
// The Studio keeps selection, zoom and Present while read-only (its commands say why they are off); only invalid text locks the slide.
|
|
411
|
+
if (frame && (!inSync || (readOnly && !studioLayout))) frame.setAttribute("inert", "");
|
|
371
412
|
else frame?.removeAttribute("inert");
|
|
372
413
|
}
|
|
373
414
|
function applyFit() {
|
|
@@ -393,7 +434,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
393
434
|
setSlideIndex: (index) => {
|
|
394
435
|
go(index);
|
|
395
436
|
},
|
|
396
|
-
|
|
437
|
+
// The Studio's filmstrip draws only the thumbnails it shows (an IntersectionObserver fills the rest as they scroll in).
|
|
438
|
+
renderThumbnail: studioLayout ? (deck, index) => thumbnailCache.peek(deck, index) ?? "" : (deck, index) => thumbnailCache.html(deck, index),
|
|
397
439
|
renderLayoutPreview: (deck, index, choice) => {
|
|
398
440
|
const candidate = prepareDimensionSwitch(deck, "layouts", choice.id, { slideIndex: index, catalogs: catalogs() }).presentation;
|
|
399
441
|
return toSvg(candidate, index + 1, { ...renderOptions(), fonts, text: "system", trace: false });
|
|
@@ -454,6 +496,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
454
496
|
return true;
|
|
455
497
|
}
|
|
456
498
|
function showPanel(name, focus = false) {
|
|
499
|
+
if (studioCtl) return studioCtl.showTab(name, { focus });
|
|
457
500
|
const tabs = controls.tabs;
|
|
458
501
|
if (!tabs?.[name]) return false;
|
|
459
502
|
for (const [tabName, tab] of Object.entries(tabs)) {
|
|
@@ -475,6 +518,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
475
518
|
strip?.destroy();
|
|
476
519
|
slideNav = undefined;
|
|
477
520
|
strip = undefined;
|
|
521
|
+
// The Studio shows its filmstrip; the slide row is the Playground preset's, mounted only while that preset is on.
|
|
522
|
+
if (studioLayout && (studioCtl?.layout ?? layout) !== "playground") return;
|
|
478
523
|
if (parts.navigator && slots.navigator) {
|
|
479
524
|
slideNav = createSlideNavigator(slots.navigator, {
|
|
480
525
|
editor: ui,
|
|
@@ -625,6 +670,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
625
670
|
);
|
|
626
671
|
canvas = createCanvasEditor(slots.canvas, {
|
|
627
672
|
...configured,
|
|
673
|
+
...(studioLayout ? { textEntry: layout === "playground" ? "click" : "dblclick", targets: layout === "playground" ? "interactive" : "passive", escape: "commit" } : {}),
|
|
628
674
|
editor: ui,
|
|
629
675
|
fonts,
|
|
630
676
|
renderOptions: renderOptions(),
|
|
@@ -635,17 +681,22 @@ export function createOpfEditor(element, options = {}) {
|
|
|
635
681
|
if (event.state === "error") {
|
|
636
682
|
fontsFailure = event.error;
|
|
637
683
|
say(event.error.message);
|
|
638
|
-
} else if (event.state === "loading")
|
|
684
|
+
} else if (event.state === "loading") {
|
|
685
|
+
fontsPending = true;
|
|
686
|
+
say(MESSAGES.loadingFonts);
|
|
687
|
+
}
|
|
639
688
|
else if (fontsFailure) {
|
|
640
689
|
fontsFailure = undefined;
|
|
641
690
|
refresh();
|
|
642
691
|
}
|
|
692
|
+
if (event.state === "ready") fontsPending = false;
|
|
643
693
|
emit("fonts", event);
|
|
644
694
|
configured.onFonts?.(event);
|
|
645
695
|
},
|
|
646
696
|
onSelect: (value) => {
|
|
647
697
|
selection = { path: value.path, value: value.value };
|
|
648
|
-
|
|
698
|
+
// The Studio emits its own selection (a list of nodes); the canvas's single selection is internal there.
|
|
699
|
+
if (!studioLayout) emit("selection", { path: value.path, value: value.value, slideIndex, element: value.element });
|
|
649
700
|
design?.refresh();
|
|
650
701
|
configured.onSelect?.(value);
|
|
651
702
|
},
|
|
@@ -783,7 +834,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
783
834
|
onFocusField: ({ fix, target }) => emit("review-focus", { fix, target }),
|
|
784
835
|
onStatus: say,
|
|
785
836
|
onChange: (counts) => {
|
|
786
|
-
const tab = controls.tabs?.review;
|
|
837
|
+
const tab = controls.tabs?.review ?? studioShell?.tabs.review?.tab;
|
|
787
838
|
if (tab) {
|
|
788
839
|
const name = options.labels?.review ?? "Review";
|
|
789
840
|
const found = !counts.unavailable && counts.total;
|
|
@@ -795,13 +846,13 @@ export function createOpfEditor(element, options = {}) {
|
|
|
795
846
|
});
|
|
796
847
|
}
|
|
797
848
|
|
|
798
|
-
function present() {
|
|
849
|
+
function present(from) {
|
|
799
850
|
if (destroyed || !mounted || !presenter || presenter.isRunning() || !inSync || slides.length === 0) return false;
|
|
800
851
|
// An open canvas edit is committed, and text typed a moment ago is applied, before the document is shown; a source that is not a valid deck waits.
|
|
801
852
|
if (canvas?.commit() === false) return false;
|
|
802
853
|
pane?.getText();
|
|
803
854
|
if (draftInvalid) return false;
|
|
804
|
-
void presenter.start(canvas?.slideIndex ?? slideIndex);
|
|
855
|
+
void presenter.start(Number.isInteger(from) ? from : (canvas?.slideIndex ?? slideIndex));
|
|
805
856
|
return true;
|
|
806
857
|
}
|
|
807
858
|
|
|
@@ -809,7 +860,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
809
860
|
slides = session.presentation.slides ?? [];
|
|
810
861
|
authoredTemplate = session.presentation.template === true;
|
|
811
862
|
if (parts.canvas && slots.canvas) mountCanvas();
|
|
812
|
-
thumbnailCache = createThumbnailCache({ toSvg, getOptions: () => ({ ...renderOptions(), fonts, text: "system", trace: false }) });
|
|
863
|
+
if (slideParts) thumbnailCache = createThumbnailCache({ toSvg, getOptions: () => ({ ...renderOptions(), fonts, text: "system", trace: false }) });
|
|
813
864
|
mountSlideRow();
|
|
814
865
|
if (parts.slides && slots.slides) slideManager = createSlideManager(slots.slides, { ...slideManagerOptions(), ...(slots.slideToolbar ? { toolbar: slots.slideToolbar } : {}) });
|
|
815
866
|
if (parts.source && slots.source) {
|
|
@@ -867,6 +918,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
867
918
|
if (parts.persistence) mountPersistence();
|
|
868
919
|
mountControls();
|
|
869
920
|
mountEnvironment();
|
|
921
|
+
if (studioShell && canvas) mountStudio();
|
|
870
922
|
// These come last, so a host hears a change after every part has. The refresh runs for every event, the catalogs' included.
|
|
871
923
|
cleanups.push(
|
|
872
924
|
session.subscribe((event) => {
|
|
@@ -890,6 +942,66 @@ export function createOpfEditor(element, options = {}) {
|
|
|
890
942
|
);
|
|
891
943
|
}
|
|
892
944
|
|
|
945
|
+
// CE-20: the Studio over the parts mounted above (src/studio.js).
|
|
946
|
+
function mountStudio() {
|
|
947
|
+
studioCtl = createStudio({
|
|
948
|
+
root,
|
|
949
|
+
element,
|
|
950
|
+
shell: studioShell,
|
|
951
|
+
slots,
|
|
952
|
+
session,
|
|
953
|
+
ui,
|
|
954
|
+
canvas,
|
|
955
|
+
options,
|
|
956
|
+
layout,
|
|
957
|
+
hostCatalogs,
|
|
958
|
+
get pane() {
|
|
959
|
+
return pane;
|
|
960
|
+
},
|
|
961
|
+
get find() {
|
|
962
|
+
return find;
|
|
963
|
+
},
|
|
964
|
+
get exportMenu() {
|
|
965
|
+
return exportMenu;
|
|
966
|
+
},
|
|
967
|
+
get slideManager() {
|
|
968
|
+
return slideManager;
|
|
969
|
+
},
|
|
970
|
+
get thumbnailCache() {
|
|
971
|
+
return thumbnailCache;
|
|
972
|
+
},
|
|
973
|
+
get persistence() {
|
|
974
|
+
return persistence;
|
|
975
|
+
},
|
|
976
|
+
get handle() {
|
|
977
|
+
return handle;
|
|
978
|
+
},
|
|
979
|
+
slideIndex: () => slideIndex,
|
|
980
|
+
go: (index) => go(index),
|
|
981
|
+
present: (from) => present(from),
|
|
982
|
+
say,
|
|
983
|
+
emit,
|
|
984
|
+
report,
|
|
985
|
+
on,
|
|
986
|
+
setView: (view) => setView(view),
|
|
987
|
+
readOnly: () => readOnly,
|
|
988
|
+
inSync: () => inSync,
|
|
989
|
+
fontsLoading: () => fontsPending,
|
|
990
|
+
catalogs,
|
|
991
|
+
layoutFor: (deck, index) => layoutFor(deck, index),
|
|
992
|
+
setDocument: (document, openOptions) => setDocument(document, openOptions),
|
|
993
|
+
openFind: (openOptions) => {
|
|
994
|
+
if (!find) return false;
|
|
995
|
+
find.openPanel(openOptions ?? {});
|
|
996
|
+
return true;
|
|
997
|
+
},
|
|
998
|
+
getName: () => pane?.getName(),
|
|
999
|
+
setName: (name) => pane?.setName(name),
|
|
1000
|
+
layoutStudio: () => studio,
|
|
1001
|
+
mountSlideRow: () => mountSlideRow(),
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
893
1005
|
// The shell's own controls: history, views, tabs.
|
|
894
1006
|
function mountControls() {
|
|
895
1007
|
controls.undo?.addEventListener("click", () => undo());
|
|
@@ -977,6 +1089,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
977
1089
|
studio?.setReadOnly(readOnly);
|
|
978
1090
|
root.toggleAttribute("data-readonly", readOnly);
|
|
979
1091
|
syncChrome();
|
|
1092
|
+
studioCtl?.refresh();
|
|
980
1093
|
}
|
|
981
1094
|
function setDensity(setting) {
|
|
982
1095
|
const next = densityFor(setting);
|
|
@@ -1014,16 +1127,29 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1014
1127
|
} catch (error) {
|
|
1015
1128
|
if (error?.code !== "invalid-opf-edit") throw error;
|
|
1016
1129
|
// A document the session refuses is no document to open: show its text with its findings, as a draft the person can fix.
|
|
1017
|
-
if (pane && text !== undefined) {
|
|
1018
|
-
if (openOptions.format && pane.getFormat() !== openOptions.format) pane.setFormat(openOptions.format);
|
|
1019
|
-
const api = pane.editor.api;
|
|
1020
|
-
api.replace(0, api.getValue().length, text);
|
|
1021
|
-
pane.getText();
|
|
1022
|
-
if (openOptions.name !== undefined) pane.setName(openOptions.name);
|
|
1023
|
-
}
|
|
1130
|
+
if (pane && text !== undefined) pane.setSourceText(text, { ...(openOptions.format ? { format: openOptions.format } : {}), ...(openOptions.name !== undefined ? { name: openOptions.name } : {}) });
|
|
1024
1131
|
return false;
|
|
1025
1132
|
}
|
|
1026
1133
|
}
|
|
1134
|
+
// Raw text in the source pane (a file as it was read, a draft that does not parse), without converting it: what the person would get by
|
|
1135
|
+
// pasting it over everything. True when the session now holds its document; false when it stays a draft with its problems.
|
|
1136
|
+
function setSourceText(text, openOptions = {}) {
|
|
1137
|
+
if (destroyed || !mounted) throw new OPFEditorError("not-ready", "The editor is not mounted yet. Await `ready` first.");
|
|
1138
|
+
if (!pane) throw new OPFEditorError("no-source-pane", "This editor has no source pane.");
|
|
1139
|
+
if (readOnly) throw new OPFEditorError("read-only", "This editor is read-only.");
|
|
1140
|
+
if (typeof text !== "string") throw new TypeError("setSourceText needs the source text as a string.");
|
|
1141
|
+
// An open canvas edit is committed first; the text then replaces it.
|
|
1142
|
+
if (canvas?.commit() === false) return false;
|
|
1143
|
+
drafting = false;
|
|
1144
|
+
openedText = undefined;
|
|
1145
|
+
const applied = pane.setSourceText(text, {
|
|
1146
|
+
...(openOptions.format ? { format: openOptions.format } : {}),
|
|
1147
|
+
...(openOptions.name !== undefined ? { name: openOptions.name } : {}),
|
|
1148
|
+
});
|
|
1149
|
+
// Text the host loaded is not the person's work: it is not autosaved or warned about until it is edited.
|
|
1150
|
+
if (applied && openOptions.rebase !== false) persistence?.rebase();
|
|
1151
|
+
return applied;
|
|
1152
|
+
}
|
|
1027
1153
|
|
|
1028
1154
|
let fontsRef = { value: undefined };
|
|
1029
1155
|
let releaseFonts;
|
|
@@ -1084,8 +1210,34 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1084
1210
|
get slideIndex() {
|
|
1085
1211
|
return slideIndex;
|
|
1086
1212
|
},
|
|
1213
|
+
/** The selection: the canvas's `{ path, value }` (or null); in the Studio, the list `[{ key, path, idPath, kind }]`. */
|
|
1087
1214
|
get selection() {
|
|
1088
|
-
return selection;
|
|
1215
|
+
return studioCtl ? studioCtl.selection : selection;
|
|
1216
|
+
},
|
|
1217
|
+
/** CE-20: the arrangement ("full", "canvas", "source", "slots", or the Studio's "studio" and "playground"). */
|
|
1218
|
+
get layout() {
|
|
1219
|
+
return studioCtl?.layout ?? layout;
|
|
1220
|
+
},
|
|
1221
|
+
/** CE-20: switch the Studio between "studio" and "playground" in place (same session, history, slide and selection). */
|
|
1222
|
+
setLayout(next) {
|
|
1223
|
+
if (!studioCtl) return false;
|
|
1224
|
+
return studioCtl.setLayout(next);
|
|
1225
|
+
},
|
|
1226
|
+
get zoom() {
|
|
1227
|
+
return studioCtl?.zoom ?? 1;
|
|
1228
|
+
},
|
|
1229
|
+
setZoom(value) {
|
|
1230
|
+
studioCtl?.setZoom(value);
|
|
1231
|
+
},
|
|
1232
|
+
fit(mode) {
|
|
1233
|
+
studioCtl?.fit(mode);
|
|
1234
|
+
},
|
|
1235
|
+
openLayout(layoutId, openOptions = {}) {
|
|
1236
|
+
return studioCtl ? studioCtl.openLayout(layoutId, openOptions) : false;
|
|
1237
|
+
},
|
|
1238
|
+
/** CE-20: the Studio's command registry (`list`, `run`, `register`); empty outside the Studio. */
|
|
1239
|
+
get commands() {
|
|
1240
|
+
return studioCtl?.commands ?? { list: () => [], run: () => false, register: () => () => {} };
|
|
1089
1241
|
},
|
|
1090
1242
|
get view() {
|
|
1091
1243
|
return activeView;
|
|
@@ -1116,11 +1268,13 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1116
1268
|
return session.snapshot().presentation;
|
|
1117
1269
|
},
|
|
1118
1270
|
setDocument,
|
|
1271
|
+
setSourceText,
|
|
1119
1272
|
setView,
|
|
1120
1273
|
setSlide: go,
|
|
1121
1274
|
goTo,
|
|
1122
|
-
select(path,
|
|
1123
|
-
return
|
|
1275
|
+
select(path, selectOptions = {}) {
|
|
1276
|
+
if (studioCtl) return studioCtl.selectTargets(path, selectOptions);
|
|
1277
|
+
return canvas?.reveal(path, { focus: Boolean(selectOptions.focus) }) ?? null;
|
|
1124
1278
|
},
|
|
1125
1279
|
showPanel,
|
|
1126
1280
|
present,
|
|
@@ -1187,6 +1341,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1187
1341
|
/* already gone */
|
|
1188
1342
|
}
|
|
1189
1343
|
}
|
|
1344
|
+
studioCtl?.destroy();
|
|
1190
1345
|
unbindKeys?.();
|
|
1191
1346
|
checking?.disconnect();
|
|
1192
1347
|
persistence?.destroy();
|
|
@@ -1214,6 +1369,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1214
1369
|
|
|
1215
1370
|
// ---- start ------------------------------------------------------------------------------------------------------------------
|
|
1216
1371
|
function mount(resolved) {
|
|
1372
|
+
if (studioLayout && !studioShell) buildStudio();
|
|
1217
1373
|
fonts = resolved;
|
|
1218
1374
|
density = densityFor(options.density);
|
|
1219
1375
|
exportMode = exportModeFor(options.exportMode);
|
|
@@ -1254,15 +1410,20 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1254
1410
|
};
|
|
1255
1411
|
try {
|
|
1256
1412
|
fontsRef = fontsSource(options.fonts);
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1413
|
+
const chunk = slideParts && !partsLoaded ? loadParts() : null;
|
|
1414
|
+
if (isThenable(fontsRef.value) || chunk) {
|
|
1415
|
+
if (isThenable(fontsRef.value)) say(MESSAGES.loadingFonts);
|
|
1416
|
+
Promise.all([fontsRef.value, chunk]).then(([resolved]) => {
|
|
1260
1417
|
try {
|
|
1261
1418
|
start(resolved);
|
|
1262
1419
|
} catch (error) {
|
|
1263
1420
|
fail(error);
|
|
1264
1421
|
}
|
|
1265
|
-
},
|
|
1422
|
+
}, (error) => {
|
|
1423
|
+
// The slide chunk did not load: fonts that did arrive are given back.
|
|
1424
|
+
Promise.resolve(fontsRef.value).then((resolved) => fontsRef.use(resolved).release?.(), () => {});
|
|
1425
|
+
fail(error);
|
|
1426
|
+
});
|
|
1266
1427
|
} else start(fontsRef.value);
|
|
1267
1428
|
} catch (error) {
|
|
1268
1429
|
fail(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -239,6 +239,10 @@
|
|
|
239
239
|
"types": "./dist/source-pane.d.ts",
|
|
240
240
|
"default": "./dist/source-pane.js"
|
|
241
241
|
},
|
|
242
|
+
"./exact-source": {
|
|
243
|
+
"types": "./dist/exact-source.d.ts",
|
|
244
|
+
"default": "./dist/exact-source.js"
|
|
245
|
+
},
|
|
242
246
|
"./presenter": {
|
|
243
247
|
"types": "./dist/presenter.d.ts",
|
|
244
248
|
"default": "./dist/presenter.js"
|
|
@@ -281,25 +285,29 @@
|
|
|
281
285
|
}
|
|
282
286
|
},
|
|
283
287
|
"types": "./dist/index.d.ts",
|
|
284
|
-
"sideEffects":
|
|
288
|
+
"sideEffects": [
|
|
289
|
+
"./dist/source-worker.js",
|
|
290
|
+
"./dist/styles.css"
|
|
291
|
+
],
|
|
285
292
|
"dependencies": {
|
|
286
|
-
"@codemirror/autocomplete": "6.20.4",
|
|
287
|
-
"@codemirror/commands": "6.11.1",
|
|
288
|
-
"@codemirror/lang-json": "6.0.2",
|
|
289
|
-
"@codemirror/lang-yaml": "6.1.3",
|
|
290
|
-
"@codemirror/language": "6.13.1",
|
|
291
|
-
"@codemirror/lint": "6.9.7",
|
|
292
|
-
"@codemirror/search": "6.7.2",
|
|
293
|
-
"@codemirror/state": "6.7.6",
|
|
294
|
-
"@codemirror/view": "6.43.15",
|
|
295
|
-
"@lezer/highlight": "1.2.5",
|
|
293
|
+
"@codemirror/autocomplete": "^6.20.4",
|
|
294
|
+
"@codemirror/commands": "^6.11.1",
|
|
295
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
296
|
+
"@codemirror/lang-yaml": "^6.1.3",
|
|
297
|
+
"@codemirror/language": "^6.13.1",
|
|
298
|
+
"@codemirror/lint": "^6.9.7",
|
|
299
|
+
"@codemirror/search": "^6.7.2",
|
|
300
|
+
"@codemirror/state": "^6.7.6",
|
|
301
|
+
"@codemirror/view": "^6.43.15",
|
|
302
|
+
"@lezer/highlight": "^1.2.5",
|
|
296
303
|
"jsonc-parser": "3.3.1",
|
|
297
304
|
"yaml": "^2.9.1",
|
|
298
|
-
"@openpresentation/opf": "^0.
|
|
305
|
+
"@openpresentation/opf": "^0.29.0"
|
|
299
306
|
},
|
|
300
307
|
"peerDependencies": {
|
|
301
308
|
"react": ">=18",
|
|
302
|
-
"
|
|
309
|
+
"react-dom": ">=18",
|
|
310
|
+
"@openpresentation/opf-render": "^0.29.0"
|
|
303
311
|
},
|
|
304
312
|
"peerDependenciesMeta": {
|
|
305
313
|
"@openpresentation/opf-render": {
|
|
@@ -307,6 +315,9 @@
|
|
|
307
315
|
},
|
|
308
316
|
"react": {
|
|
309
317
|
"optional": true
|
|
318
|
+
},
|
|
319
|
+
"react-dom": {
|
|
320
|
+
"optional": true
|
|
310
321
|
}
|
|
311
322
|
},
|
|
312
323
|
"devDependencies": {
|
|
@@ -365,9 +376,9 @@
|
|
|
365
376
|
"react-dom": "19.3.1",
|
|
366
377
|
"react-18": "npm:react@18.3.1",
|
|
367
378
|
"react-dom-18": "npm:react-dom@18.3.1",
|
|
368
|
-
"@openpresentation/gallery": "0.
|
|
369
|
-
"@openpresentation/opf-pptx": "0.
|
|
370
|
-
"@openpresentation/opf-render": "0.
|
|
379
|
+
"@openpresentation/gallery": "0.29.0",
|
|
380
|
+
"@openpresentation/opf-pptx": "0.29.0",
|
|
381
|
+
"@openpresentation/opf-render": "0.29.0"
|
|
371
382
|
},
|
|
372
383
|
"scripts": {
|
|
373
384
|
"build": "node scripts/build.mjs",
|
|
@@ -420,5 +431,5 @@
|
|
|
420
431
|
"test:workspace-browser": "node test/workspace-browser.mjs",
|
|
421
432
|
"test:workspace-react-browser": "node test/workspace-react-browser.mjs"
|
|
422
433
|
},
|
|
423
|
-
"gitHead": "
|
|
434
|
+
"gitHead": "464e10d3c15647b0e43326ac7e6761e34789991a"
|
|
424
435
|
}
|