@openpresentation/opf-editor 0.27.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 +23 -9
- 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 +136 -125
- package/dist/design-options.js +1 -1
- package/dist/exact-source.d.ts +30 -0
- package/dist/font-gate.js +20 -1
- package/dist/json-editor.d.ts +33 -0
- package/dist/json-editor.js +91 -4
- package/dist/json-options.d.ts +2 -1
- package/dist/json-options.js +7 -6
- 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/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 +16 -4
- package/dist/react.js +47 -6
- 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-color-scheme.js +60 -0
- package/dist/source-completions.js +6 -4
- package/dist/source-pane.d.ts +103 -6
- package/dist/source-pane.js +196 -9
- 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/commands.d.ts +38 -0
- package/dist/studio-kit/commands.js +148 -0
- package/dist/studio-kit/dom.d.ts +52 -0
- package/dist/studio-kit/dom.js +198 -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/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 +1573 -123
- 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 +114 -10
- package/dist/workspace.js +223 -49
- package/package.json +30 -19
package/dist/workspace.js
CHANGED
|
@@ -7,35 +7,49 @@
|
|
|
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";
|
|
36
20
|
import { createSourceMemory, updateJsonSource } from "./exact-source.js";
|
|
37
21
|
|
|
38
22
|
export { THEME_TOKENS, createThemeController, normalizeTheme, normalizeThemeTokens } from "./workspace-theme.js";
|
|
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).
|
|
24
|
+
export { layoutOptionsFor } from "./workspace-measure.js";
|
|
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])));
|
|
39
53
|
|
|
40
54
|
const VIEWS = Object.freeze(["slide", "sorter", "outline", "layouts"]);
|
|
41
55
|
const MUTATORS = new Set(["set", "setGroupComposition", "setCatalog", "setCatalogs", "applyPatch", "replaceDocument", "restoreState", "undo", "redo", "setComposition", "paginateSlide"]);
|
|
@@ -55,7 +69,11 @@ const LAYOUT_PARTS = {
|
|
|
55
69
|
full: { source: true, navigator: true, thumbnails: true, history: true, find: true, present: true, export: true, review: true, layouts: true, sorter: true, outline: true },
|
|
56
70
|
canvas: { navigator: true, thumbnails: true, history: true },
|
|
57
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 },
|
|
58
75
|
};
|
|
76
|
+
const STUDIO_LAYOUTS = new Set(["studio", "playground"]);
|
|
59
77
|
|
|
60
78
|
const isThenable = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
|
|
61
79
|
|
|
@@ -108,7 +126,9 @@ function resolveParts(options, layout, slots) {
|
|
|
108
126
|
}
|
|
109
127
|
|
|
110
128
|
/**
|
|
111
|
-
* 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.
|
|
112
132
|
* See `workspace.d.ts` for the options and the handle.
|
|
113
133
|
*/
|
|
114
134
|
export function createOpfEditor(element, options = {}) {
|
|
@@ -150,6 +170,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
150
170
|
for (const [name, listener] of Object.entries(options.on ?? {})) on(name, listener);
|
|
151
171
|
if (options.onChange) on("change", options.onChange);
|
|
152
172
|
if (options.onSelection) on("selection", options.onSelection);
|
|
173
|
+
if (options.onCursor) on("cursor", options.onCursor);
|
|
153
174
|
if (options.onError) on("error", ({ error, ...detail }) => options.onError(error, detail));
|
|
154
175
|
if (options.onReady) on("ready", options.onReady);
|
|
155
176
|
|
|
@@ -162,8 +183,9 @@ export function createOpfEditor(element, options = {}) {
|
|
|
162
183
|
|
|
163
184
|
// ---- layout, parts and slots ------------------------------------------------------------------------------------------------
|
|
164
185
|
const given = options.slots ?? {};
|
|
165
|
-
const layout = options.slots ? "slots" : (options.layout ?? "full");
|
|
166
|
-
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);
|
|
167
189
|
const parts = resolveParts(options, layout, given);
|
|
168
190
|
const views = VIEWS.filter((view) => view === "slide" || parts[view]);
|
|
169
191
|
if (options.view !== undefined && !views.includes(options.view)) throw new RangeError(`view "${options.view}" is not available in this layout (${views.join(", ")}).`);
|
|
@@ -180,13 +202,27 @@ export function createOpfEditor(element, options = {}) {
|
|
|
180
202
|
const themeController = createThemeController(element, options.theme);
|
|
181
203
|
let slots = { ...given };
|
|
182
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
|
+
}
|
|
183
217
|
if (layout === "slots") root.setAttribute("data-opf-layout", "slots");
|
|
184
|
-
else {
|
|
218
|
+
else if (!studioLayout) {
|
|
185
219
|
const built = buildWorkspaceShell(root, { parts, views, layout, labels: options.labels });
|
|
186
220
|
slots = { ...built.slots, ...given };
|
|
187
221
|
controls = built.controls;
|
|
188
222
|
}
|
|
189
|
-
|
|
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);
|
|
190
226
|
|
|
191
227
|
// ---- state ------------------------------------------------------------------------------------------------------------------
|
|
192
228
|
let fonts;
|
|
@@ -198,6 +234,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
198
234
|
let sorter;
|
|
199
235
|
let outline;
|
|
200
236
|
let studio;
|
|
237
|
+
let studioCtl;
|
|
238
|
+
let fontsPending = false;
|
|
201
239
|
let presenter;
|
|
202
240
|
let presentButton;
|
|
203
241
|
let exportMenu;
|
|
@@ -220,6 +258,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
220
258
|
let drafting = false;
|
|
221
259
|
let openedText;
|
|
222
260
|
let sourceState = null;
|
|
261
|
+
let sourceCursor = null;
|
|
223
262
|
let fontsFailure;
|
|
224
263
|
let refreshToken = 0;
|
|
225
264
|
let lastRefresh = Promise.resolve();
|
|
@@ -230,7 +269,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
230
269
|
let frameSize = { width: 0, height: 0 };
|
|
231
270
|
const draftMemory = createSourceMemory();
|
|
232
271
|
const variablesMode = options.variables === "resolved" ? "resolved" : "authored";
|
|
233
|
-
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;
|
|
234
273
|
|
|
235
274
|
const say = (message) => {
|
|
236
275
|
statusText = message;
|
|
@@ -251,7 +290,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
251
290
|
|
|
252
291
|
// ---- navigation and the refresh loop ----------------------------------------------------------------------------------------
|
|
253
292
|
function renderViews(deck) {
|
|
254
|
-
|
|
293
|
+
// The Studio's filmstrip draws the thumbnails it shows itself, as they scroll in.
|
|
294
|
+
if (!studioLayout) thumbnailCache?.prepare(deck);
|
|
255
295
|
slideManager?.render();
|
|
256
296
|
if (activeView === "sorter") sorter?.render();
|
|
257
297
|
if (activeView === "outline") outline?.render();
|
|
@@ -279,7 +319,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
279
319
|
}
|
|
280
320
|
function sync() {
|
|
281
321
|
fonts?.registry?.clearSubstitutions?.();
|
|
282
|
-
|
|
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;
|
|
283
324
|
slideIndex = Math.max(0, Math.min(slideIndex, deck.slides.length - 1));
|
|
284
325
|
renderViews(deck);
|
|
285
326
|
slideNav?.setSlide(slideIndex);
|
|
@@ -296,16 +337,18 @@ export function createOpfEditor(element, options = {}) {
|
|
|
296
337
|
if (!mounted || destroyed) return Promise.resolve();
|
|
297
338
|
const token = ++refreshToken;
|
|
298
339
|
let loaded = false;
|
|
299
|
-
lastRefresh = whenFontsReady(fonts, session.presentation, {
|
|
340
|
+
lastRefresh = whenFontsReady(fonts, session.snapshot().presentation, {
|
|
300
341
|
renderOptions: renderOptions(),
|
|
301
342
|
isCurrent: () => token === refreshToken && !destroyed,
|
|
302
343
|
loading: () => {
|
|
303
344
|
loaded = true;
|
|
345
|
+
fontsPending = true;
|
|
304
346
|
emit("fonts", { state: "loading" });
|
|
305
347
|
say(MESSAGES.loadingFonts);
|
|
306
348
|
},
|
|
307
349
|
ready: () => {
|
|
308
350
|
fontsFailure = undefined;
|
|
351
|
+
fontsPending = false;
|
|
309
352
|
if (loaded) {
|
|
310
353
|
thumbnailCache?.clear();
|
|
311
354
|
emit("fonts", { state: "ready" });
|
|
@@ -350,7 +393,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
350
393
|
}
|
|
351
394
|
function goTo(index, path) {
|
|
352
395
|
if (index !== slideIndex && !go(index)) return false;
|
|
353
|
-
if (
|
|
396
|
+
if (studioCtl) studioCtl.revealPath(path ?? `slides.${index}`);
|
|
397
|
+
else if (path) canvas?.reveal(path);
|
|
354
398
|
return true;
|
|
355
399
|
}
|
|
356
400
|
|
|
@@ -363,7 +407,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
363
407
|
if (controls.redo) controls.redo.disabled = !inSync || readOnly || !session.canRedo;
|
|
364
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.
|
|
365
409
|
const frame = slots.canvas && (slots.slide ?? slots.canvas);
|
|
366
|
-
|
|
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", "");
|
|
367
412
|
else frame?.removeAttribute("inert");
|
|
368
413
|
}
|
|
369
414
|
function applyFit() {
|
|
@@ -389,7 +434,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
389
434
|
setSlideIndex: (index) => {
|
|
390
435
|
go(index);
|
|
391
436
|
},
|
|
392
|
-
|
|
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),
|
|
393
439
|
renderLayoutPreview: (deck, index, choice) => {
|
|
394
440
|
const candidate = prepareDimensionSwitch(deck, "layouts", choice.id, { slideIndex: index, catalogs: catalogs() }).presentation;
|
|
395
441
|
return toSvg(candidate, index + 1, { ...renderOptions(), fonts, text: "system", trace: false });
|
|
@@ -450,6 +496,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
450
496
|
return true;
|
|
451
497
|
}
|
|
452
498
|
function showPanel(name, focus = false) {
|
|
499
|
+
if (studioCtl) return studioCtl.showTab(name, { focus });
|
|
453
500
|
const tabs = controls.tabs;
|
|
454
501
|
if (!tabs?.[name]) return false;
|
|
455
502
|
for (const [tabName, tab] of Object.entries(tabs)) {
|
|
@@ -471,6 +518,8 @@ export function createOpfEditor(element, options = {}) {
|
|
|
471
518
|
strip?.destroy();
|
|
472
519
|
slideNav = undefined;
|
|
473
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;
|
|
474
523
|
if (parts.navigator && slots.navigator) {
|
|
475
524
|
slideNav = createSlideNavigator(slots.navigator, {
|
|
476
525
|
editor: ui,
|
|
@@ -538,6 +587,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
538
587
|
return {
|
|
539
588
|
...(configured.outline !== undefined ? { outline: configured.outline } : {}),
|
|
540
589
|
...(configured.applyDelay !== undefined ? { applyDelay: configured.applyDelay } : {}),
|
|
590
|
+
...(configured.cursorDelay !== undefined ? { cursorDelay: configured.cursorDelay } : {}),
|
|
541
591
|
...(configured.completions !== undefined ? { completions: configured.completions } : {}),
|
|
542
592
|
...(configured.catalogs !== undefined ? { catalogs: configured.catalogs } : {}),
|
|
543
593
|
...(configured.name !== undefined ? { name: configured.name } : {}),
|
|
@@ -553,6 +603,11 @@ export function createOpfEditor(element, options = {}) {
|
|
|
553
603
|
configured.onRename?.(name);
|
|
554
604
|
emit("rename", { name });
|
|
555
605
|
},
|
|
606
|
+
// CE-17: the path under the caret of the source pane, as `cursor` events.
|
|
607
|
+
onCursor: (detail) => {
|
|
608
|
+
sourceCursor = Object.freeze(detail);
|
|
609
|
+
emit("cursor", sourceCursor);
|
|
610
|
+
},
|
|
556
611
|
getFindings: ({ format, text: shown }) => {
|
|
557
612
|
reportedText = shown;
|
|
558
613
|
const made = reportSource(format, shown, { catalogs: catalogs(), checks: configured.checks ?? SOURCE_CHECKS });
|
|
@@ -615,6 +670,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
615
670
|
);
|
|
616
671
|
canvas = createCanvasEditor(slots.canvas, {
|
|
617
672
|
...configured,
|
|
673
|
+
...(studioLayout ? { textEntry: layout === "playground" ? "click" : "dblclick", targets: layout === "playground" ? "interactive" : "passive", escape: "commit" } : {}),
|
|
618
674
|
editor: ui,
|
|
619
675
|
fonts,
|
|
620
676
|
renderOptions: renderOptions(),
|
|
@@ -625,17 +681,22 @@ export function createOpfEditor(element, options = {}) {
|
|
|
625
681
|
if (event.state === "error") {
|
|
626
682
|
fontsFailure = event.error;
|
|
627
683
|
say(event.error.message);
|
|
628
|
-
} else if (event.state === "loading")
|
|
684
|
+
} else if (event.state === "loading") {
|
|
685
|
+
fontsPending = true;
|
|
686
|
+
say(MESSAGES.loadingFonts);
|
|
687
|
+
}
|
|
629
688
|
else if (fontsFailure) {
|
|
630
689
|
fontsFailure = undefined;
|
|
631
690
|
refresh();
|
|
632
691
|
}
|
|
692
|
+
if (event.state === "ready") fontsPending = false;
|
|
633
693
|
emit("fonts", event);
|
|
634
694
|
configured.onFonts?.(event);
|
|
635
695
|
},
|
|
636
696
|
onSelect: (value) => {
|
|
637
697
|
selection = { path: value.path, value: value.value };
|
|
638
|
-
|
|
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 });
|
|
639
700
|
design?.refresh();
|
|
640
701
|
configured.onSelect?.(value);
|
|
641
702
|
},
|
|
@@ -773,7 +834,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
773
834
|
onFocusField: ({ fix, target }) => emit("review-focus", { fix, target }),
|
|
774
835
|
onStatus: say,
|
|
775
836
|
onChange: (counts) => {
|
|
776
|
-
const tab = controls.tabs?.review;
|
|
837
|
+
const tab = controls.tabs?.review ?? studioShell?.tabs.review?.tab;
|
|
777
838
|
if (tab) {
|
|
778
839
|
const name = options.labels?.review ?? "Review";
|
|
779
840
|
const found = !counts.unavailable && counts.total;
|
|
@@ -785,13 +846,13 @@ export function createOpfEditor(element, options = {}) {
|
|
|
785
846
|
});
|
|
786
847
|
}
|
|
787
848
|
|
|
788
|
-
function present() {
|
|
849
|
+
function present(from) {
|
|
789
850
|
if (destroyed || !mounted || !presenter || presenter.isRunning() || !inSync || slides.length === 0) return false;
|
|
790
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.
|
|
791
852
|
if (canvas?.commit() === false) return false;
|
|
792
853
|
pane?.getText();
|
|
793
854
|
if (draftInvalid) return false;
|
|
794
|
-
void presenter.start(canvas?.slideIndex ?? slideIndex);
|
|
855
|
+
void presenter.start(Number.isInteger(from) ? from : (canvas?.slideIndex ?? slideIndex));
|
|
795
856
|
return true;
|
|
796
857
|
}
|
|
797
858
|
|
|
@@ -799,7 +860,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
799
860
|
slides = session.presentation.slides ?? [];
|
|
800
861
|
authoredTemplate = session.presentation.template === true;
|
|
801
862
|
if (parts.canvas && slots.canvas) mountCanvas();
|
|
802
|
-
thumbnailCache = createThumbnailCache({ toSvg, getOptions: () => ({ ...renderOptions(), fonts, text: "system", trace: false }) });
|
|
863
|
+
if (slideParts) thumbnailCache = createThumbnailCache({ toSvg, getOptions: () => ({ ...renderOptions(), fonts, text: "system", trace: false }) });
|
|
803
864
|
mountSlideRow();
|
|
804
865
|
if (parts.slides && slots.slides) slideManager = createSlideManager(slots.slides, { ...slideManagerOptions(), ...(slots.slideToolbar ? { toolbar: slots.slideToolbar } : {}) });
|
|
805
866
|
if (parts.source && slots.source) {
|
|
@@ -857,6 +918,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
857
918
|
if (parts.persistence) mountPersistence();
|
|
858
919
|
mountControls();
|
|
859
920
|
mountEnvironment();
|
|
921
|
+
if (studioShell && canvas) mountStudio();
|
|
860
922
|
// These come last, so a host hears a change after every part has. The refresh runs for every event, the catalogs' included.
|
|
861
923
|
cleanups.push(
|
|
862
924
|
session.subscribe((event) => {
|
|
@@ -880,6 +942,66 @@ export function createOpfEditor(element, options = {}) {
|
|
|
880
942
|
);
|
|
881
943
|
}
|
|
882
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
|
+
|
|
883
1005
|
// The shell's own controls: history, views, tabs.
|
|
884
1006
|
function mountControls() {
|
|
885
1007
|
controls.undo?.addEventListener("click", () => undo());
|
|
@@ -967,6 +1089,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
967
1089
|
studio?.setReadOnly(readOnly);
|
|
968
1090
|
root.toggleAttribute("data-readonly", readOnly);
|
|
969
1091
|
syncChrome();
|
|
1092
|
+
studioCtl?.refresh();
|
|
970
1093
|
}
|
|
971
1094
|
function setDensity(setting) {
|
|
972
1095
|
const next = densityFor(setting);
|
|
@@ -1004,16 +1127,29 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1004
1127
|
} catch (error) {
|
|
1005
1128
|
if (error?.code !== "invalid-opf-edit") throw error;
|
|
1006
1129
|
// A document the session refuses is no document to open: show its text with its findings, as a draft the person can fix.
|
|
1007
|
-
if (pane && text !== undefined) {
|
|
1008
|
-
if (openOptions.format && pane.getFormat() !== openOptions.format) pane.setFormat(openOptions.format);
|
|
1009
|
-
const api = pane.editor.api;
|
|
1010
|
-
api.replace(0, api.getValue().length, text);
|
|
1011
|
-
pane.getText();
|
|
1012
|
-
if (openOptions.name !== undefined) pane.setName(openOptions.name);
|
|
1013
|
-
}
|
|
1130
|
+
if (pane && text !== undefined) pane.setSourceText(text, { ...(openOptions.format ? { format: openOptions.format } : {}), ...(openOptions.name !== undefined ? { name: openOptions.name } : {}) });
|
|
1014
1131
|
return false;
|
|
1015
1132
|
}
|
|
1016
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
|
+
}
|
|
1017
1153
|
|
|
1018
1154
|
let fontsRef = { value: undefined };
|
|
1019
1155
|
let releaseFonts;
|
|
@@ -1074,8 +1210,34 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1074
1210
|
get slideIndex() {
|
|
1075
1211
|
return slideIndex;
|
|
1076
1212
|
},
|
|
1213
|
+
/** The selection: the canvas's `{ path, value }` (or null); in the Studio, the list `[{ key, path, idPath, kind }]`. */
|
|
1077
1214
|
get selection() {
|
|
1078
|
-
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: () => () => {} };
|
|
1079
1241
|
},
|
|
1080
1242
|
get view() {
|
|
1081
1243
|
return activeView;
|
|
@@ -1090,6 +1252,9 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1090
1252
|
return inSync;
|
|
1091
1253
|
},
|
|
1092
1254
|
/** The latest verdict on the text of the source pane (what the last `source` event carried), or `null` before the first. */
|
|
1255
|
+
get sourceCursor() {
|
|
1256
|
+
return sourceCursor;
|
|
1257
|
+
},
|
|
1093
1258
|
get sourceState() {
|
|
1094
1259
|
return sourceState;
|
|
1095
1260
|
},
|
|
@@ -1103,11 +1268,13 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1103
1268
|
return session.snapshot().presentation;
|
|
1104
1269
|
},
|
|
1105
1270
|
setDocument,
|
|
1271
|
+
setSourceText,
|
|
1106
1272
|
setView,
|
|
1107
1273
|
setSlide: go,
|
|
1108
1274
|
goTo,
|
|
1109
|
-
select(path,
|
|
1110
|
-
return
|
|
1275
|
+
select(path, selectOptions = {}) {
|
|
1276
|
+
if (studioCtl) return studioCtl.selectTargets(path, selectOptions);
|
|
1277
|
+
return canvas?.reveal(path, { focus: Boolean(selectOptions.focus) }) ?? null;
|
|
1111
1278
|
},
|
|
1112
1279
|
showPanel,
|
|
1113
1280
|
present,
|
|
@@ -1174,6 +1341,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1174
1341
|
/* already gone */
|
|
1175
1342
|
}
|
|
1176
1343
|
}
|
|
1344
|
+
studioCtl?.destroy();
|
|
1177
1345
|
unbindKeys?.();
|
|
1178
1346
|
checking?.disconnect();
|
|
1179
1347
|
persistence?.destroy();
|
|
@@ -1201,6 +1369,7 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1201
1369
|
|
|
1202
1370
|
// ---- start ------------------------------------------------------------------------------------------------------------------
|
|
1203
1371
|
function mount(resolved) {
|
|
1372
|
+
if (studioLayout && !studioShell) buildStudio();
|
|
1204
1373
|
fonts = resolved;
|
|
1205
1374
|
density = densityFor(options.density);
|
|
1206
1375
|
exportMode = exportModeFor(options.exportMode);
|
|
@@ -1241,15 +1410,20 @@ export function createOpfEditor(element, options = {}) {
|
|
|
1241
1410
|
};
|
|
1242
1411
|
try {
|
|
1243
1412
|
fontsRef = fontsSource(options.fonts);
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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]) => {
|
|
1247
1417
|
try {
|
|
1248
1418
|
start(resolved);
|
|
1249
1419
|
} catch (error) {
|
|
1250
1420
|
fail(error);
|
|
1251
1421
|
}
|
|
1252
|
-
},
|
|
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
|
+
});
|
|
1253
1427
|
} else start(fontsRef.value);
|
|
1254
1428
|
} catch (error) {
|
|
1255
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.
|
|
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.
|
|
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": {
|
|
@@ -360,14 +371,14 @@
|
|
|
360
371
|
"jszip": "3.10.2",
|
|
361
372
|
"pdf-lib": "1.17.1",
|
|
362
373
|
"pdfjs-dist": "6.4.299",
|
|
363
|
-
"playwright": "1.64.
|
|
374
|
+
"playwright": "1.64.1",
|
|
364
375
|
"react": "19.3.1",
|
|
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/
|
|
369
|
-
"@openpresentation/
|
|
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
|
}
|