@openpresentation/opf-editor 0.29.0 → 0.29.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.
@@ -383,6 +383,15 @@ export interface OpfEditorSetDocumentOptions {
383
383
  rebase?: boolean;
384
384
  }
385
385
 
386
+ /**
387
+ * The handle of a mounted editor. It is returned at once, before the parts have mounted (they wait for the fonts and, in a layout that shows
388
+ * slides, for the slide chunk), and every method works from the first moment: a setting (`setDensity`, `setExportMode`, `setView`, `setSlide`,
389
+ * `setReadOnly`, `setTheme`, `setName`) is kept and read when the parts mount, and a call that acts on a part (`select`, `goTo`, `showPanel`,
390
+ * `focus`, `openFind`, `present`, and the Studio's `setLayout`, `setZoom`, `fit`, `openLayout` and `commands.register`) is queued and runs, in
391
+ * order, as soon as the editor is mounted, before `mounted` is emitted; a later call to the same method replaces an earlier one. A queued call
392
+ * returns `true` (accepted) or, where it needs a part the editor does not have, `false`. Only `setDocument` and `setSourceText`, whose result
393
+ * depends on the document, throw before `ready`.
394
+ */
386
395
  export interface OpfEditor {
387
396
  /** The mount element and the node the editor built inside it. */
388
397
  readonly element: HTMLElement;
package/dist/workspace.js CHANGED
@@ -266,6 +266,31 @@ export function createOpfEditor(element, options = {}) {
266
266
  let statusText = "";
267
267
  let density = "comfortable";
268
268
  let exportMode = "menu";
269
+ // The parts are bound once the slide chunk has arrived and the fonts are at hand; `partsReady` is true from then on. A call on the handle made
270
+ // before that (a React host's effects run right after `createOpfEditor` returns) must neither reach a factory that is not bound yet nor be lost:
271
+ // a setting is remembered in `requested` and read when the parts mount; a call that acts on a mounted part waits in `queued` and runs, in order,
272
+ // once the editor is mounted (a later call with the same key replaces the earlier one).
273
+ let partsReady = false;
274
+ const requested = {};
275
+ const queued = [];
276
+ const waiting = () => !mounted && !destroyed;
277
+ const queue = (key, task) => {
278
+ const at = queued.findIndex((entry) => entry.key === key);
279
+ if (at >= 0) queued.splice(at, 1);
280
+ const entry = { key, task };
281
+ queued.push(entry);
282
+ return entry;
283
+ };
284
+ function flushQueued() {
285
+ for (const { task } of queued.splice(0)) {
286
+ if (destroyed) return;
287
+ try {
288
+ task();
289
+ } catch (error) {
290
+ report(error);
291
+ }
292
+ }
293
+ }
269
294
  let frameSize = { width: 0, height: 0 };
270
295
  const draftMemory = createSourceMemory();
271
296
  const variablesMode = options.variables === "resolved" ? "resolved" : "authored";
@@ -392,6 +417,11 @@ export function createOpfEditor(element, options = {}) {
392
417
  return false;
393
418
  }
394
419
  function goTo(index, path) {
420
+ if (waiting()) {
421
+ go(index);
422
+ queue("goTo", () => goTo(index, path));
423
+ return true;
424
+ }
395
425
  if (index !== slideIndex && !go(index)) return false;
396
426
  if (studioCtl) studioCtl.revealPath(path ?? `slides.${index}`);
397
427
  else if (path) canvas?.reveal(path);
@@ -454,6 +484,7 @@ export function createOpfEditor(element, options = {}) {
454
484
  if (!views.includes(view)) return false;
455
485
  if (!mounted) {
456
486
  activeView = view;
487
+ requested.view = view;
457
488
  return true;
458
489
  }
459
490
  if (view !== "slide" && canvas && !canvas.commit()) return false;
@@ -496,6 +527,15 @@ export function createOpfEditor(element, options = {}) {
496
527
  return true;
497
528
  }
498
529
  function showPanel(name, focus = false) {
530
+ if (!mounted) {
531
+ // The tabs of the shell are known now, the Studio's when it mounts; the panel is shown after the default one.
532
+ if (destroyed || (!studioLayout && !controls.tabs?.[name])) return false;
533
+ queue("panel", () => showPanel(name, focus));
534
+ return true;
535
+ }
536
+ return showPanelNow(name, focus);
537
+ }
538
+ function showPanelNow(name, focus = false) {
499
539
  if (studioCtl) return studioCtl.showTab(name, { focus });
500
540
  const tabs = controls.tabs;
501
541
  if (!tabs?.[name]) return false;
@@ -513,6 +553,9 @@ export function createOpfEditor(element, options = {}) {
513
553
 
514
554
  // ---- the parts --------------------------------------------------------------------------------------------------------------
515
555
  function mountSlideRow() {
556
+ // The row is built from the slide chunk's factories: before they are bound (and after destroy) there is nothing to rebuild; the first
557
+ // mount reads the density and the layout as they are by then.
558
+ if (!partsReady || destroyed) return;
516
559
  unbindKeys?.();
517
560
  slideNav?.destroy();
518
561
  strip?.destroy();
@@ -590,7 +633,7 @@ export function createOpfEditor(element, options = {}) {
590
633
  ...(configured.cursorDelay !== undefined ? { cursorDelay: configured.cursorDelay } : {}),
591
634
  ...(configured.completions !== undefined ? { completions: configured.completions } : {}),
592
635
  ...(configured.catalogs !== undefined ? { catalogs: configured.catalogs } : {}),
593
- ...(configured.name !== undefined ? { name: configured.name } : {}),
636
+ ...((requested.name ?? configured.name) !== undefined ? { name: requested.name ?? configured.name } : {}),
594
637
  ...(text !== undefined ? { source: text } : {}),
595
638
  editor: ui,
596
639
  format: configured.format ?? "json",
@@ -847,6 +890,10 @@ export function createOpfEditor(element, options = {}) {
847
890
  }
848
891
 
849
892
  function present(from) {
893
+ if (waiting() && parts.present) {
894
+ queue("present", () => present(from));
895
+ return true;
896
+ }
850
897
  if (destroyed || !mounted || !presenter || presenter.isRunning() || !inSync || slides.length === 0) return false;
851
898
  // 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.
852
899
  if (canvas?.commit() === false) return false;
@@ -1008,17 +1055,17 @@ export function createOpfEditor(element, options = {}) {
1008
1055
  controls.redo?.addEventListener("click", () => redo());
1009
1056
  for (const [name, button] of Object.entries(controls.views ?? {})) button.addEventListener("click", () => setView(name, { focus: true }));
1010
1057
  for (const [name, tab] of Object.entries(controls.tabs ?? {})) {
1011
- tab.addEventListener("click", () => showPanel(name));
1058
+ tab.addEventListener("click", () => showPanelNow(name));
1012
1059
  tab.addEventListener("keydown", (event) => {
1013
1060
  if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
1014
1061
  event.preventDefault();
1015
1062
  const names = Object.keys(controls.tabs);
1016
1063
  const at = names.indexOf(name);
1017
1064
  const next = event.key === "Home" ? 0 : event.key === "End" ? names.length - 1 : (at + (event.key === "ArrowRight" ? 1 : -1) + names.length) % names.length;
1018
- showPanel(names[next], true);
1065
+ showPanelNow(names[next], true);
1019
1066
  });
1020
1067
  }
1021
- if (controls.tabs) showPanel(options.panel && controls.tabs[options.panel] ? options.panel : Object.keys(controls.tabs)[0]);
1068
+ if (controls.tabs) showPanelNow(options.panel && controls.tabs[options.panel] ? options.panel : Object.keys(controls.tabs)[0]);
1022
1069
  if (options.shortcuts !== false) {
1023
1070
  // Ctrl/Cmd+Z and +Shift+Z undo and redo when nothing that edits text has focus (the code editor and the canvas's inputs keep their own).
1024
1071
  const onKey = (event) => {
@@ -1055,8 +1102,8 @@ export function createOpfEditor(element, options = {}) {
1055
1102
  media.addEventListener("change", change);
1056
1103
  cleanups.push(() => media.removeEventListener("change", change));
1057
1104
  };
1058
- follow(options.density, "(pointer: coarse)", setDensity);
1059
- follow(options.exportMode, "(max-width: 900px)", setExportMode);
1105
+ follow("density" in requested ? requested.density : options.density, "(pointer: coarse)", setDensity);
1106
+ follow("exportMode" in requested ? requested.exportMode : options.exportMode, "(max-width: 900px)", setExportMode);
1060
1107
  }
1061
1108
 
1062
1109
  // ---- the handle -------------------------------------------------------------------------------------------------------------
@@ -1092,6 +1139,14 @@ export function createOpfEditor(element, options = {}) {
1092
1139
  studioCtl?.refresh();
1093
1140
  }
1094
1141
  function setDensity(setting) {
1142
+ if (destroyed) return;
1143
+ // Before the parts mount the setting is kept; the mount reads it instead of the `density` option.
1144
+ if (!partsReady) {
1145
+ requested.density = setting;
1146
+ density = densityFor(setting);
1147
+ root.setAttribute("data-density", density);
1148
+ return;
1149
+ }
1095
1150
  const next = densityFor(setting);
1096
1151
  if (next === density) return;
1097
1152
  density = next;
@@ -1103,6 +1158,8 @@ export function createOpfEditor(element, options = {}) {
1103
1158
  mountSlideRow();
1104
1159
  }
1105
1160
  function setExportMode(setting) {
1161
+ if (destroyed) return;
1162
+ if (!partsReady) requested.exportMode = setting;
1106
1163
  exportMode = exportModeFor(setting);
1107
1164
  exportMenu?.setMode(exportMode);
1108
1165
  }
@@ -1151,6 +1208,26 @@ export function createOpfEditor(element, options = {}) {
1151
1208
  return applied;
1152
1209
  }
1153
1210
 
1211
+ // The Studio's command registry is not there before mount: `register` keeps the command and adds it when the Studio mounts (the function it
1212
+ // returns removes it either way); `list` is empty and `run` finds nothing yet.
1213
+ const preMountCommands = {
1214
+ list: () => [],
1215
+ run: () => false,
1216
+ register(command) {
1217
+ if (!waiting() || !studioLayout) return () => {};
1218
+ let stop;
1219
+ let withdrawn = false;
1220
+ const entry = queue(Symbol("command"), () => {
1221
+ if (!withdrawn) stop = studioCtl?.commands.register(command);
1222
+ });
1223
+ return () => {
1224
+ withdrawn = true;
1225
+ const at = queued.indexOf(entry);
1226
+ if (at >= 0) queued.splice(at, 1);
1227
+ stop?.();
1228
+ };
1229
+ },
1230
+ };
1154
1231
  let fontsRef = { value: undefined };
1155
1232
  let releaseFonts;
1156
1233
  let readyPromise;
@@ -1220,24 +1297,33 @@ export function createOpfEditor(element, options = {}) {
1220
1297
  },
1221
1298
  /** CE-20: switch the Studio between "studio" and "playground" in place (same session, history, slide and selection). */
1222
1299
  setLayout(next) {
1223
- if (!studioCtl) return false;
1224
- return studioCtl.setLayout(next);
1300
+ if (studioCtl) return studioCtl.setLayout(next);
1301
+ // Before mount the switch is queued (true), as the Studio's own check of the name would refuse it later.
1302
+ if (!waiting() || !studioLayout) return false;
1303
+ if (next !== "studio" && next !== "playground") throw new RangeError('setLayout takes "studio" or "playground".');
1304
+ queue("layout", () => studioCtl?.setLayout(next));
1305
+ return true;
1225
1306
  },
1226
1307
  get zoom() {
1227
1308
  return studioCtl?.zoom ?? 1;
1228
1309
  },
1229
1310
  setZoom(value) {
1230
- studioCtl?.setZoom(value);
1311
+ if (studioCtl) studioCtl.setZoom(value);
1312
+ else if (waiting() && studioLayout) queue("zoom", () => studioCtl?.setZoom(value));
1231
1313
  },
1232
1314
  fit(mode) {
1233
- studioCtl?.fit(mode);
1315
+ if (studioCtl) studioCtl.fit(mode);
1316
+ else if (waiting() && studioLayout) queue("zoom", () => studioCtl?.fit(mode));
1234
1317
  },
1235
1318
  openLayout(layoutId, openOptions = {}) {
1236
- return studioCtl ? studioCtl.openLayout(layoutId, openOptions) : false;
1319
+ if (studioCtl) return studioCtl.openLayout(layoutId, openOptions);
1320
+ if (!waiting() || !studioLayout) return false;
1321
+ queue("openLayout", () => studioCtl?.openLayout(layoutId, openOptions));
1322
+ return true;
1237
1323
  },
1238
- /** CE-20: the Studio's command registry (`list`, `run`, `register`); empty outside the Studio. */
1324
+ /** CE-20: the Studio's command registry (`list`, `run`, `register`); empty outside the Studio. Commands registered before mount are added when it mounts. */
1239
1325
  get commands() {
1240
- return studioCtl?.commands ?? { list: () => [], run: () => false, register: () => () => {} };
1326
+ return studioCtl?.commands ?? preMountCommands;
1241
1327
  },
1242
1328
  get view() {
1243
1329
  return activeView;
@@ -1273,6 +1359,10 @@ export function createOpfEditor(element, options = {}) {
1273
1359
  setSlide: go,
1274
1360
  goTo,
1275
1361
  select(path, selectOptions = {}) {
1362
+ if (waiting()) {
1363
+ queue("select", () => handle.select(path, selectOptions));
1364
+ return studioLayout ? true : null;
1365
+ }
1276
1366
  if (studioCtl) return studioCtl.selectTargets(path, selectOptions);
1277
1367
  return canvas?.reveal(path, { focus: Boolean(selectOptions.focus) }) ?? null;
1278
1368
  },
@@ -1290,6 +1380,10 @@ export function createOpfEditor(element, options = {}) {
1290
1380
  return canvas?.commit() !== false;
1291
1381
  },
1292
1382
  focus(target) {
1383
+ if (waiting()) {
1384
+ queue("focus", () => handle.focus(target));
1385
+ return true;
1386
+ }
1293
1387
  if ((target ?? (pane ? "source" : "canvas")) === "source") {
1294
1388
  pane?.editor?.api?.focus?.();
1295
1389
  return Boolean(pane);
@@ -1299,6 +1393,10 @@ export function createOpfEditor(element, options = {}) {
1299
1393
  return Boolean(node);
1300
1394
  },
1301
1395
  openFind(openOptions) {
1396
+ if (waiting() && parts.find) {
1397
+ queue("find", () => handle.openFind(openOptions));
1398
+ return true;
1399
+ }
1302
1400
  if (!find) return false;
1303
1401
  find.openPanel(openOptions ?? {});
1304
1402
  return true;
@@ -1311,10 +1409,12 @@ export function createOpfEditor(element, options = {}) {
1311
1409
  return { text, format: pane.getFormat(), draft: draftInvalid };
1312
1410
  },
1313
1411
  setName(name) {
1412
+ if (waiting()) requested.name = name;
1314
1413
  pane?.setName(name);
1315
1414
  },
1316
1415
  getName() {
1317
- return pane?.getName();
1416
+ if (pane) return pane.getName();
1417
+ return waiting() ? (requested.name ?? options.source?.name) : undefined;
1318
1418
  },
1319
1419
  setReadOnly(value) {
1320
1420
  readOnly = Boolean(value);
@@ -1371,16 +1471,21 @@ export function createOpfEditor(element, options = {}) {
1371
1471
  function mount(resolved) {
1372
1472
  if (studioLayout && !studioShell) buildStudio();
1373
1473
  fonts = resolved;
1374
- density = densityFor(options.density);
1375
- exportMode = exportModeFor(options.exportMode);
1474
+ // A density, an export mode or a view the host chose before the parts mounted wins over the option.
1475
+ density = densityFor("density" in requested ? requested.density : options.density);
1476
+ exportMode = exportModeFor("exportMode" in requested ? requested.exportMode : options.exportMode);
1376
1477
  root.setAttribute("data-density", density);
1478
+ partsReady = true;
1377
1479
  mountParts();
1378
1480
  mounted = true;
1379
1481
  if (readOnly) applyReadOnly();
1380
1482
  say(MESSAGES.ready);
1381
- if (options.view && options.view !== "slide") setView(options.view);
1483
+ const wantedView = "view" in requested ? requested.view : options.view;
1484
+ activeView = "slide";
1485
+ if (wantedView && wantedView !== "slide") setView(wantedView);
1382
1486
  else markView("slide");
1383
1487
  syncChrome();
1488
+ flushQueued();
1384
1489
  }
1385
1490
  readyPromise = new Promise((resolve, reject) => {
1386
1491
  const start = (arrived) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-editor",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
4
4
  "description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -302,12 +302,12 @@
302
302
  "@lezer/highlight": "^1.2.5",
303
303
  "jsonc-parser": "3.3.1",
304
304
  "yaml": "^2.9.1",
305
- "@openpresentation/opf": "^0.29.0"
305
+ "@openpresentation/opf": "^0.29.1"
306
306
  },
307
307
  "peerDependencies": {
308
308
  "react": ">=18",
309
309
  "react-dom": ">=18",
310
- "@openpresentation/opf-render": "^0.29.0"
310
+ "@openpresentation/opf-render": "^0.29.1"
311
311
  },
312
312
  "peerDependenciesMeta": {
313
313
  "@openpresentation/opf-render": {
@@ -376,9 +376,9 @@
376
376
  "react-dom": "19.3.1",
377
377
  "react-18": "npm:react@18.3.1",
378
378
  "react-dom-18": "npm:react-dom@18.3.1",
379
- "@openpresentation/gallery": "0.29.0",
380
- "@openpresentation/opf-pptx": "0.29.0",
381
- "@openpresentation/opf-render": "0.29.0"
379
+ "@openpresentation/opf-pptx": "0.29.1",
380
+ "@openpresentation/gallery": "0.29.1",
381
+ "@openpresentation/opf-render": "0.29.1"
382
382
  },
383
383
  "scripts": {
384
384
  "build": "node scripts/build.mjs",
@@ -431,5 +431,5 @@
431
431
  "test:workspace-browser": "node test/workspace-browser.mjs",
432
432
  "test:workspace-react-browser": "node test/workspace-react-browser.mjs"
433
433
  },
434
- "gitHead": "464e10d3c15647b0e43326ac7e6761e34789991a"
434
+ "gitHead": "e9f46c24606ce0f474347b984eacea5dce7fd839"
435
435
  }