@doenet/doenetml-iframe 0.7.20-dev.328 → 0.7.20-dev.329

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 CHANGED
@@ -140,7 +140,38 @@ either `flags.allowSaveState` (the wrapper snapshots the flushed
140
140
  `reportScoreAndState` and seeds `initialState` on restore) or
141
141
  `flags.allowLocalState` (IndexedDB restores on reboot). Windowed viewers
142
142
  with neither flag still mount lazily but, once booted, always stay live
143
- (a console warning points this out).
143
+ (a console warning — once per page — points this out). Hosts that consume
144
+ reports through the `reportScoreAndStateCallback` prop are fully supported:
145
+ the wrapper captures the flushed report for its park snapshot before
146
+ forwarding it to the callback.
147
+
148
+ Parking also requires a standalone bundle new enough to acknowledge the
149
+ flush (v0.7.21+). A viewer pinned to an older `doenetmlVersion` mounts
150
+ lazily and obeys the boot cap, but is never parked. A host-specified
151
+ `standaloneUrl` is assumed modern (hosts shipping a custom URL control both
152
+ sides — note a `-dev.N` prerelease *version string* compares as its base
153
+ release, so dev-channel hosts should pin `standaloneUrl` rather than
154
+ `doenetmlVersion`).
155
+
156
+ #### `keepLive`: prefetching hosts
157
+
158
+ A windowed viewer inside a hidden (`display:none`) or far-off-screen
159
+ container never intersects the viewport, so it would stay parked forever.
160
+ A host that knows the viewer is about to be shown — e.g. a paginator
161
+ prefetching the pages adjacent to the current one — sets the dynamic
162
+ `keepLive` prop to treat it as visible: it boots eagerly (still subject to
163
+ `maxConcurrentBoots`, visible-first) and is never parked while the hint is
164
+ set. Clear the hint and the viewer becomes an ordinary windowed citizen
165
+ again (parked when off-screen and over budget).
166
+
167
+ ```tsx
168
+ <DoenetViewer
169
+ doenetML={doenetML}
170
+ flags={{ allowSaveState: true }}
171
+ mountPolicy={{ mode: "windowed", maxLiveViewers: 3 }}
172
+ keepLive={Math.abs(itemIndex - currentIndex) <= 1}
173
+ />
174
+ ```
144
175
 
145
176
  Notes:
146
177
 
package/index.d.ts CHANGED
@@ -86,7 +86,7 @@ declare type DoenetEditorProps = Omit<React.ComponentProps<typeof DoenetEditor_2
86
86
  * v0.7.18 cannot re-render in place; for those the wrapper falls back to
87
87
  * reloading the iframe on any prop change, its historical behavior.)
88
88
  */
89
- export declare function DoenetViewer({ doenetML, standaloneUrl: specifiedStandaloneUrl, cssUrl: specifiedCssUrl, doenetmlVersion: specifiedDoenetmlVersion, autodetectVersion, useSharedCoreWorker, mountPolicy, ...doenetViewerProps }: DoenetViewerIframeProps): default_2.JSX.Element | null;
89
+ export declare function DoenetViewer({ doenetML, standaloneUrl: specifiedStandaloneUrl, cssUrl: specifiedCssUrl, doenetmlVersion: specifiedDoenetmlVersion, autodetectVersion, useSharedCoreWorker, mountPolicy, keepLive, ...doenetViewerProps }: DoenetViewerIframeProps): default_2.JSX.Element | null;
90
90
 
91
91
  export declare type DoenetViewerIframeProps = DoenetViewerProps & {
92
92
  doenetML: string;
@@ -134,12 +134,27 @@ export declare type DoenetViewerIframeProps = DoenetViewerProps & {
134
134
  * viewport. Parking requires a persistence path: it only activates for
135
135
  * viewers with `flags.allowSaveState` (the wrapper snapshots the
136
136
  * flushed `reportScoreAndState` and seeds `initialState` on restore) or
137
- * `flags.allowLocalState` (IndexedDB restores on reboot); otherwise the
138
- * viewer boots on visibility but then always stays live. The policy is
139
- * read at mount; changing it afterwards is not supported. Default off
140
- * (no prop = today's behavior).
137
+ * `flags.allowLocalState` (IndexedDB restores on reboot), and only when
138
+ * the selected bundle is new enough to acknowledge the flush (v0.7.21+,
139
+ * or a host-specified `standaloneUrl`); otherwise the viewer boots on
140
+ * visibility but then always stays live. The policy is read at mount;
141
+ * changing it afterwards is not supported. Default off (no prop =
142
+ * today's behavior).
141
143
  */
142
144
  mountPolicy?: MountPolicy;
145
+ /**
146
+ * Windowed-mounting hint: treat this viewer as visible regardless of
147
+ * viewport intersection — boot it eagerly (still subject to the
148
+ * page-wide `maxConcurrentBoots` slot queue) and never park it while
149
+ * set. For hosts that know an off-screen or `display:none` viewer is
150
+ * about to be shown, e.g. a paginator prefetching the pages adjacent to
151
+ * the current one. Dynamic, unlike `mountPolicy`; no effect without a
152
+ * windowed `mountPolicy`. The hint counts as visibility for boot
153
+ * ordering and the least-recently-visible eviction order, so keep it on
154
+ * a few viewers at a time (a page-wide `keepLive` would defeat the
155
+ * budget).
156
+ */
157
+ keepLive?: boolean;
143
158
  };
144
159
 
145
160
  declare type DoenetViewerProps = Omit<React.ComponentProps<typeof DoenetViewer_2>, "doenetML" | "externalVirtualKeyboardProvided">;
package/index.js CHANGED
@@ -25618,7 +25618,7 @@ function requireCssesc$1() {
25618
25618
  return cssesc_1$1;
25619
25619
  }
25620
25620
  requireCssesc$1();
25621
- const viewerIframeJsSource = '(function() {\n "use strict";\n let lastAugmentedProps = null;\n let currentFunctionProps = {};\n const functionPropDispatchers = {};\n function getFunctionPropDispatcher(key) {\n let dispatcher = functionPropDispatchers[key];\n if (!dispatcher) {\n dispatcher = (...callArgs) => currentFunctionProps[key]?.(...callArgs);\n functionPropDispatchers[key] = dispatcher;\n }\n return dispatcher;\n }\n function functionPropArgsToMap(args) {\n const map = {};\n for (let i = 0; i < args.length; i += 2) {\n const key = args[i];\n const fn = args[i + 1];\n if (typeof key === "string" && typeof fn === "function") {\n map[key] = fn;\n }\n }\n return map;\n }\n function releaseFunctionProxy(fn) {\n try {\n fn?.[ComlinkViewer.releaseProxy]?.();\n } catch (e) {\n console.warn(\n "iframe DoenetViewer: failed to release stale Comlink proxy",\n e\n );\n }\n }\n function setCurrentFunctionProps(incoming) {\n for (const [key, fn] of Object.entries(currentFunctionProps)) {\n if (incoming[key] !== fn) {\n releaseFunctionProxy(fn);\n }\n }\n currentFunctionProps = incoming;\n }\n function defaultRequestScrollTo(offset) {\n messageParentFromViewer({ type: "scrollTo", offset });\n }\n function syncAugmentedFunctionProps() {\n if (!lastAugmentedProps) {\n return;\n }\n const next = {};\n for (const [key, val] of Object.entries(lastAugmentedProps)) {\n if (typeof val !== "function") {\n next[key] = val;\n }\n }\n for (const key of Object.keys(currentFunctionProps)) {\n next[key] = getFunctionPropDispatcher(key);\n }\n if (!("requestScrollTo" in next)) {\n next.requestScrollTo = defaultRequestScrollTo;\n }\n lastAugmentedProps = next;\n }\n let currentDoenetMLSource = null;\n function resolveDoenetMLSource(root) {\n if (currentDoenetMLSource !== null) {\n return currentDoenetMLSource;\n }\n const scriptTag = root.querySelector(\'script[type="text/doenetml"]\');\n currentDoenetMLSource = scriptTag?.innerHTML ?? "";\n return currentDoenetMLSource;\n }\n async function waitForStandaloneBundle(timeoutMs) {\n if (typeof window.renderDoenetViewerToContainer === "function") {\n return true;\n }\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n if (typeof window.renderDoenetViewerToContainer === "function") {\n return true;\n }\n }\n return false;\n }\n ComlinkViewer.expose(\n {\n renderViewerWithFunctionProps,\n updateViewerProps(updatedSerializableProps) {\n if (!lastAugmentedProps) {\n console.warn(\n "iframe DoenetViewer: updateViewerProps arrived before renderViewerWithFunctionProps completed — likely a bug in the iframe wrapper\'s queue/replay sequencing."\n );\n return;\n }\n const { doenetML, ...rest } = updatedSerializableProps;\n if (typeof doenetML === "string") {\n currentDoenetMLSource = doenetML;\n }\n lastAugmentedProps = {\n ...lastAugmentedProps,\n ...rest\n };\n renderWithLastAugmentedProps();\n },\n updateViewerFunctionProps(...args) {\n if (!lastAugmentedProps) {\n console.warn(\n "iframe DoenetViewer: updateViewerFunctionProps arrived before renderViewerWithFunctionProps completed — likely a bug in the iframe wrapper\'s queue/replay sequencing."\n );\n return;\n }\n const incoming = functionPropArgsToMap(args);\n const prevKeys = Object.keys(currentFunctionProps).sort();\n const nextKeys = Object.keys(incoming).sort();\n const keysChanged = prevKeys.length !== nextKeys.length || prevKeys.some((key, i) => key !== nextKeys[i]);\n setCurrentFunctionProps(incoming);\n if (keysChanged) {\n syncAugmentedFunctionProps();\n renderWithLastAugmentedProps();\n }\n }\n },\n ComlinkViewer.windowEndpoint(globalThis.parent)\n );\n function renderViewerWithFunctionProps(...args) {\n setCurrentFunctionProps(functionPropArgsToMap(args));\n const augmentedDoenetViewerProps = { ...doenetViewerProps };\n augmentedDoenetViewerProps.externalVirtualKeyboardProvided = true;\n for (const propName of doenetViewerPropsSpecified) {\n if (!(propName in doenetViewerProps) && propName in currentFunctionProps) {\n augmentedDoenetViewerProps[propName] = getFunctionPropDispatcher(propName);\n }\n }\n if (!doenetViewerPropsSpecified.includes("requestScrollTo")) {\n augmentedDoenetViewerProps.requestScrollTo = defaultRequestScrollTo;\n }\n lastAugmentedProps = augmentedDoenetViewerProps;\n renderWithLastAugmentedProps();\n }\n function renderWithLastAugmentedProps() {\n const root = document.getElementById("root");\n window.renderDoenetViewerToContainer(\n root,\n resolveDoenetMLSource(root),\n lastAugmentedProps\n );\n }\n function installSharedCorePortProvider() {\n let coreCounter = 0;\n window.doenetGlobalConfig.createExternalCoreWorkerPort = () => {\n const coreId = `${viewerId}-core-${++coreCounter}`;\n const channel = new MessageChannel();\n window.parent.postMessage(\n { origin: viewerId, data: { type: "createSharedCore", coreId } },\n window.parent.origin,\n [channel.port2]\n );\n return {\n port: channel.port1,\n destroy: (suspectWedge) => {\n messageParentFromViewer({\n type: "destroySharedCore",\n coreId,\n suspectWedge: Boolean(suspectWedge)\n });\n }\n };\n };\n }\n (async () => {\n if (await waitForStandaloneBundle(6e4)) {\n if (typeof doenetSharedCoreWorker !== "undefined" && doenetSharedCoreWorker) {\n installSharedCorePortProvider();\n }\n messageParentFromViewer({ iframeReady: true });\n } else {\n messageParentFromViewer({\n error: "Invalid DoenetML version or DoenetML package not found"\n });\n }\n })().catch((err) => {\n console.error(\n "iframe DoenetViewer: unexpected failure while signalling iframeReady",\n err\n );\n try {\n messageParentFromViewer({\n error: "iframe viewer failed to initialize"\n });\n } catch {\n }\n });\n function messageParentFromViewer(data) {\n window.parent.postMessage(\n {\n origin: viewerId,\n data\n },\n window.parent.origin\n );\n }\n})();\n';
25621
+ const viewerIframeJsSource = '(function() {\n "use strict";\n let lastAugmentedProps = null;\n let currentFunctionProps = {};\n const functionPropDispatchers = {};\n function getFunctionPropDispatcher(key) {\n let dispatcher = functionPropDispatchers[key];\n if (!dispatcher) {\n if (key === "reportScoreAndStateCallback" && typeof doenetWindowedViewer !== "undefined" && doenetWindowedViewer) {\n dispatcher = (report) => messageParentFromViewer({\n type: "reportScoreAndState",\n report\n });\n } else {\n dispatcher = (...callArgs) => currentFunctionProps[key]?.(...callArgs);\n }\n functionPropDispatchers[key] = dispatcher;\n }\n return dispatcher;\n }\n function functionPropArgsToMap(args) {\n const map = {};\n for (let i = 0; i < args.length; i += 2) {\n const key = args[i];\n const fn = args[i + 1];\n if (typeof key === "string" && typeof fn === "function") {\n map[key] = fn;\n }\n }\n return map;\n }\n function releaseFunctionProxy(fn) {\n try {\n fn?.[ComlinkViewer.releaseProxy]?.();\n } catch (e) {\n console.warn(\n "iframe DoenetViewer: failed to release stale Comlink proxy",\n e\n );\n }\n }\n function setCurrentFunctionProps(incoming) {\n for (const [key, fn] of Object.entries(currentFunctionProps)) {\n if (incoming[key] !== fn) {\n releaseFunctionProxy(fn);\n }\n }\n currentFunctionProps = incoming;\n }\n function defaultRequestScrollTo(offset) {\n messageParentFromViewer({ type: "scrollTo", offset });\n }\n function syncAugmentedFunctionProps() {\n if (!lastAugmentedProps) {\n return;\n }\n const next = {};\n for (const [key, val] of Object.entries(lastAugmentedProps)) {\n if (typeof val !== "function") {\n next[key] = val;\n }\n }\n for (const key of Object.keys(currentFunctionProps)) {\n next[key] = getFunctionPropDispatcher(key);\n }\n if (!("requestScrollTo" in next)) {\n next.requestScrollTo = defaultRequestScrollTo;\n }\n lastAugmentedProps = next;\n }\n let currentDoenetMLSource = null;\n function resolveDoenetMLSource(root) {\n if (currentDoenetMLSource !== null) {\n return currentDoenetMLSource;\n }\n const scriptTag = root.querySelector(\'script[type="text/doenetml"]\');\n currentDoenetMLSource = scriptTag?.innerHTML ?? "";\n return currentDoenetMLSource;\n }\n async function waitForStandaloneBundle(timeoutMs) {\n if (typeof window.renderDoenetViewerToContainer === "function") {\n return true;\n }\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n if (typeof window.renderDoenetViewerToContainer === "function") {\n return true;\n }\n }\n return false;\n }\n ComlinkViewer.expose(\n {\n renderViewerWithFunctionProps,\n updateViewerProps(updatedSerializableProps) {\n if (!lastAugmentedProps) {\n console.warn(\n "iframe DoenetViewer: updateViewerProps arrived before renderViewerWithFunctionProps completed — likely a bug in the iframe wrapper\'s queue/replay sequencing."\n );\n return;\n }\n const { doenetML, ...rest } = updatedSerializableProps;\n if (typeof doenetML === "string") {\n currentDoenetMLSource = doenetML;\n }\n lastAugmentedProps = {\n ...lastAugmentedProps,\n ...rest\n };\n renderWithLastAugmentedProps();\n },\n updateViewerFunctionProps(...args) {\n if (!lastAugmentedProps) {\n console.warn(\n "iframe DoenetViewer: updateViewerFunctionProps arrived before renderViewerWithFunctionProps completed — likely a bug in the iframe wrapper\'s queue/replay sequencing."\n );\n return;\n }\n const incoming = functionPropArgsToMap(args);\n const prevKeys = Object.keys(currentFunctionProps).sort();\n const nextKeys = Object.keys(incoming).sort();\n const keysChanged = prevKeys.length !== nextKeys.length || prevKeys.some((key, i) => key !== nextKeys[i]);\n setCurrentFunctionProps(incoming);\n if (keysChanged) {\n syncAugmentedFunctionProps();\n renderWithLastAugmentedProps();\n }\n }\n },\n ComlinkViewer.windowEndpoint(globalThis.parent)\n );\n function renderViewerWithFunctionProps(...args) {\n setCurrentFunctionProps(functionPropArgsToMap(args));\n const augmentedDoenetViewerProps = { ...doenetViewerProps };\n augmentedDoenetViewerProps.externalVirtualKeyboardProvided = true;\n for (const propName of doenetViewerPropsSpecified) {\n if (!(propName in doenetViewerProps) && propName in currentFunctionProps) {\n augmentedDoenetViewerProps[propName] = getFunctionPropDispatcher(propName);\n }\n }\n if (!doenetViewerPropsSpecified.includes("requestScrollTo")) {\n augmentedDoenetViewerProps.requestScrollTo = defaultRequestScrollTo;\n }\n lastAugmentedProps = augmentedDoenetViewerProps;\n renderWithLastAugmentedProps();\n }\n function renderWithLastAugmentedProps() {\n const root = document.getElementById("root");\n window.renderDoenetViewerToContainer(\n root,\n resolveDoenetMLSource(root),\n lastAugmentedProps\n );\n }\n function installSharedCorePortProvider() {\n let coreCounter = 0;\n window.doenetGlobalConfig.createExternalCoreWorkerPort = () => {\n const coreId = `${viewerId}-core-${++coreCounter}`;\n const channel = new MessageChannel();\n window.parent.postMessage(\n { origin: viewerId, data: { type: "createSharedCore", coreId } },\n window.parent.origin,\n [channel.port2]\n );\n return {\n port: channel.port1,\n destroy: (suspectWedge) => {\n messageParentFromViewer({\n type: "destroySharedCore",\n coreId,\n suspectWedge: Boolean(suspectWedge)\n });\n }\n };\n };\n }\n (async () => {\n if (await waitForStandaloneBundle(6e4)) {\n if (typeof doenetSharedCoreWorker !== "undefined" && doenetSharedCoreWorker) {\n installSharedCorePortProvider();\n }\n messageParentFromViewer({ iframeReady: true });\n } else {\n messageParentFromViewer({\n error: "Invalid DoenetML version or DoenetML package not found"\n });\n }\n })().catch((err) => {\n console.error(\n "iframe DoenetViewer: unexpected failure while signalling iframeReady",\n err\n );\n try {\n messageParentFromViewer({\n error: "iframe viewer failed to initialize"\n });\n } catch {\n }\n });\n function messageParentFromViewer(data) {\n window.parent.postMessage(\n {\n origin: viewerId,\n data\n },\n window.parent.origin\n );\n }\n})();\n';
25622
25622
  const editorIframeJsSource = `(function() {
25623
25623
  "use strict";
25624
25624
  let editorControlHandle = null;
@@ -25856,7 +25856,7 @@ function createIframeHead(standaloneUrl, cssUrl) {
25856
25856
  function createIframeBodyOpenTag(darkMode) {
25857
25857
  return `<body style="margin:0" ${BODY_BACKGROUND_ATTRIBUTE}="${bodyBackgroundMode(darkMode)}">`;
25858
25858
  }
25859
- function createHtmlForDoenetViewer(id2, doenetML, doenetViewerProps, standaloneUrl, cssUrl, useSharedCoreWorker = false) {
25859
+ function createHtmlForDoenetViewer(id2, doenetML, doenetViewerProps, standaloneUrl, cssUrl, useSharedCoreWorker = false, windowed = false) {
25860
25860
  const doenetViewerPropsSpecified = Object.keys(doenetViewerProps);
25861
25861
  if (!doenetViewerPropsSpecified.includes("initializedCallback")) {
25862
25862
  doenetViewerPropsSpecified.push("initializedCallback");
@@ -25870,6 +25870,7 @@ function createHtmlForDoenetViewer(id2, doenetML, doenetViewerProps, standaloneU
25870
25870
  const doenetViewerProps = ${JSON.stringify(doenetViewerProps)};
25871
25871
  const doenetViewerPropsSpecified = ${JSON.stringify(doenetViewerPropsSpecified)};
25872
25872
  const doenetSharedCoreWorker = ${JSON.stringify(!!useSharedCoreWorker)};
25873
+ const doenetWindowedViewer = ${JSON.stringify(!!windowed)};
25873
25874
  import * as ComlinkViewer from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
25874
25875
 
25875
25876
  // This source code has been compiled by vite and should be directly included.
@@ -26039,6 +26040,14 @@ const records = /* @__PURE__ */ new Map();
26039
26040
  let visibleOrderCounter = 0;
26040
26041
  let effectiveMaxLive = null;
26041
26042
  let warnedMixedBudgets = false;
26043
+ let warnedWindowedWithoutPersistence = false;
26044
+ function shouldWarnWindowedWithoutPersistence() {
26045
+ if (warnedWindowedWithoutPersistence) {
26046
+ return false;
26047
+ }
26048
+ warnedWindowedWithoutPersistence = true;
26049
+ return true;
26050
+ }
26042
26051
  let evaluateTimerId = null;
26043
26052
  let evaluateTimerAt = Infinity;
26044
26053
  let effectiveMaxBoots = null;
@@ -26216,6 +26225,32 @@ function evaluate$1() {
26216
26225
  scheduleEvaluate(Math.max(0, nextDebounceExpiry - now));
26217
26226
  }
26218
26227
  }
26228
+ const LIVE_UPDATE_MIN_VERSION = [0, 7, 18];
26229
+ const PARK_MIN_VERSION = [0, 7, 21];
26230
+ function versionAtLeast(version2, min2) {
26231
+ const match2 = version2.replace(/^v/, "").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
26232
+ if (!match2) {
26233
+ return true;
26234
+ }
26235
+ const parts = [match2[1], match2[2], match2[3]].map(
26236
+ (p2) => p2 === void 0 ? Infinity : parseInt(p2, 10)
26237
+ );
26238
+ for (let i2 = 0; i2 < 3; i2++) {
26239
+ if (parts[i2] > min2[i2]) {
26240
+ return true;
26241
+ }
26242
+ if (parts[i2] < min2[i2]) {
26243
+ return false;
26244
+ }
26245
+ }
26246
+ return true;
26247
+ }
26248
+ function versionSupportsLiveUpdates(version2) {
26249
+ return versionAtLeast(version2, LIVE_UPDATE_MIN_VERSION);
26250
+ }
26251
+ function versionSupportsParking(version2) {
26252
+ return versionAtLeast(version2, PARK_MIN_VERSION);
26253
+ }
26219
26254
  function findNodesWithPositionInfo(nodes) {
26220
26255
  if (typeof nodes !== "object" || nodes === null) {
26221
26256
  return [];
@@ -63362,7 +63397,7 @@ function ExternalVirtualKeyboard({
63362
63397
  }
63363
63398
  );
63364
63399
  }
63365
- const version = "0.7.20-dev.328";
63400
+ const version = "0.7.20-dev.329";
63366
63401
  const latestDoenetmlVersion = version;
63367
63402
  function subscribeToPinnedTheme() {
63368
63403
  return () => {
@@ -63377,25 +63412,6 @@ function useResolvedTheme(setting) {
63377
63412
  );
63378
63413
  return setting === "system" ? systemTheme : setting;
63379
63414
  }
63380
- const LIVE_UPDATE_MIN_VERSION = [0, 7, 18];
63381
- function versionSupportsLiveUpdates(version2) {
63382
- const match2 = version2.replace(/^v/, "").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
63383
- if (!match2) {
63384
- return true;
63385
- }
63386
- const parts = [match2[1], match2[2], match2[3]].map(
63387
- (p2) => p2 === void 0 ? Infinity : parseInt(p2, 10)
63388
- );
63389
- for (let i2 = 0; i2 < 3; i2++) {
63390
- if (parts[i2] > LIVE_UPDATE_MIN_VERSION[i2]) {
63391
- return true;
63392
- }
63393
- if (parts[i2] < LIVE_UPDATE_MIN_VERSION[i2]) {
63394
- return false;
63395
- }
63396
- }
63397
- return true;
63398
- }
63399
63415
  function DoenetViewer({
63400
63416
  doenetML,
63401
63417
  standaloneUrl: specifiedStandaloneUrl,
@@ -63404,6 +63420,7 @@ function DoenetViewer({
63404
63420
  autodetectVersion = true,
63405
63421
  useSharedCoreWorker = false,
63406
63422
  mountPolicy,
63423
+ keepLive = false,
63407
63424
  ...doenetViewerProps
63408
63425
  }) {
63409
63426
  const [id2, _2] = React__default.useState(() => Math.random().toString(36).slice(2));
@@ -63421,7 +63438,8 @@ function DoenetViewer({
63421
63438
  const [parkGeneration, setParkGeneration] = React__default.useState(0);
63422
63439
  const parkedRef = React__default.useRef(windowed);
63423
63440
  const parkingRef = React__default.useRef(false);
63424
- const visibleRef = React__default.useRef(false);
63441
+ const intersectingRef = React__default.useRef(false);
63442
+ const keepLiveRef = React__default.useRef(false);
63425
63443
  const containerRef = React__default.useRef(null);
63426
63444
  const parkFlushCounterRef = React__default.useRef(0);
63427
63445
  const parkFlushIdRef = React__default.useRef(null);
@@ -63510,6 +63528,9 @@ function DoenetViewer({
63510
63528
  specifiedStandaloneUrl && !foundAutoVersion
63511
63529
  );
63512
63530
  const liveUpdatesSupported = usingCustomStandaloneUrl || versionSupportsLiveUpdates(selectedDoenetmlVersion);
63531
+ const parkingSupported = usingCustomStandaloneUrl || versionSupportsParking(selectedDoenetmlVersion);
63532
+ const parkingSupportedRef = React__default.useRef(parkingSupported);
63533
+ parkingSupportedRef.current = parkingSupported;
63513
63534
  const standaloneUrlRef = React__default.useRef(standaloneUrl);
63514
63535
  standaloneUrlRef.current = standaloneUrl;
63515
63536
  const propsSnapshotStr = serializePropsSnapshot(
@@ -63538,7 +63559,8 @@ function DoenetViewer({
63538
63559
  bakedProps,
63539
63560
  standaloneUrl,
63540
63561
  cssUrl,
63541
- useSharedCoreWorker
63562
+ useSharedCoreWorker,
63563
+ windowed
63542
63564
  );
63543
63565
  }, [
63544
63566
  id2,
@@ -63593,7 +63615,9 @@ function DoenetViewer({
63593
63615
  }
63594
63616
  if (windowed) {
63595
63617
  if (event.data.subject === "SPLICE.reportScoreAndState") {
63596
- lastCapturedReportRef.current = event.data;
63618
+ lastCapturedReportRef.current = {
63619
+ state: event.data.state
63620
+ };
63597
63621
  return;
63598
63622
  }
63599
63623
  if (event.data.subject === "SPLICE.flushState.response" && event.data.message_id === parkFlushIdRef.current) {
@@ -63605,6 +63629,24 @@ function DoenetViewer({
63605
63629
  return;
63606
63630
  }
63607
63631
  const data = event.data.data;
63632
+ if (windowed && data.type === "reportScoreAndState") {
63633
+ const report = data.report ?? {};
63634
+ lastCapturedReportRef.current = { state: report.state };
63635
+ const hostCallback = doenetViewerPropsRef.current.reportScoreAndStateCallback;
63636
+ if (typeof hostCallback === "function") {
63637
+ hostCallback(report);
63638
+ } else {
63639
+ window.postMessage({
63640
+ subject: "SPLICE.reportScoreAndState",
63641
+ state: report.state,
63642
+ score: report.score,
63643
+ activity_id: report.activityId,
63644
+ doc_id: report.docId,
63645
+ message_id: Math.random().toString(36).slice(2)
63646
+ });
63647
+ }
63648
+ return;
63649
+ }
63608
63650
  if (data.type === "createSharedCore" && event.ports[0]) {
63609
63651
  handleCreateSharedCore({
63610
63652
  viewerId: id2,
@@ -63751,7 +63793,7 @@ function DoenetViewer({
63751
63793
  return Boolean(flags?.allowSaveState || flags?.allowLocalState);
63752
63794
  }
63753
63795
  function canPark() {
63754
- return hasPersistencePath() && Boolean(ref.current?.contentWindow);
63796
+ return hasPersistencePath() && parkingSupportedRef.current && Boolean(ref.current?.contentWindow);
63755
63797
  }
63756
63798
  function beginPark() {
63757
63799
  if (parkedRef.current || parkingRef.current) {
@@ -63788,28 +63830,32 @@ function DoenetViewer({
63788
63830
  parkFlushCleanupRef.current?.();
63789
63831
  parkFlushIdRef.current = null;
63790
63832
  parkingRef.current = false;
63791
- if (visibleRef.current) {
63833
+ if (effectivelyVisible()) {
63792
63834
  notifyViewerState(id2, "live");
63793
63835
  return false;
63794
63836
  }
63795
63837
  return true;
63796
63838
  }
63839
+ function seedSnapshotFromCapturedReport() {
63840
+ if (lastCapturedReportRef.current?.state) {
63841
+ parkedSnapshotRef.current = lastCapturedReportRef.current.state;
63842
+ }
63843
+ }
63797
63844
  function completeParkFlush(ack) {
63798
63845
  if (!endParkFlush()) {
63799
63846
  return;
63800
63847
  }
63801
63848
  if (ack.hadState) {
63802
63849
  everHadStateRef.current = true;
63803
- if (lastCapturedReportRef.current?.state) {
63804
- parkedSnapshotRef.current = lastCapturedReportRef.current.state;
63805
- }
63806
63850
  }
63851
+ seedSnapshotFromCapturedReport();
63807
63852
  finishPark();
63808
63853
  }
63809
63854
  function onParkFlushTimeout() {
63810
63855
  if (!endParkFlush()) {
63811
63856
  return;
63812
63857
  }
63858
+ seedSnapshotFromCapturedReport();
63813
63859
  finishPark();
63814
63860
  }
63815
63861
  function finishPark() {
@@ -63825,7 +63871,7 @@ function DoenetViewer({
63825
63871
  return;
63826
63872
  }
63827
63873
  requestBootSlot(id2, () => {
63828
- if (!parkedRef.current || !visibleRef.current) {
63874
+ if (!parkedRef.current || !effectivelyVisible()) {
63829
63875
  releaseBootSlot(id2);
63830
63876
  return;
63831
63877
  }
@@ -63840,6 +63886,18 @@ function DoenetViewer({
63840
63886
  }, BOOT_SLOT_WATCHDOG_MS);
63841
63887
  });
63842
63888
  }
63889
+ function effectivelyVisible() {
63890
+ return intersectingRef.current || keepLiveRef.current;
63891
+ }
63892
+ function updateEffectiveVisibility() {
63893
+ const effective = effectivelyVisible();
63894
+ setViewerVisibility(id2, effective);
63895
+ if (effective) {
63896
+ unpark();
63897
+ } else {
63898
+ cancelBootRequest(id2);
63899
+ }
63900
+ }
63843
63901
  React__default.useEffect(() => {
63844
63902
  if (!windowed || !containerRef.current) {
63845
63903
  return;
@@ -63847,13 +63905,8 @@ function DoenetViewer({
63847
63905
  const observer = new IntersectionObserver(
63848
63906
  (entries) => {
63849
63907
  for (const entry of entries) {
63850
- visibleRef.current = entry.isIntersecting;
63851
- setViewerVisibility(id2, entry.isIntersecting);
63852
- if (entry.isIntersecting && parkedRef.current) {
63853
- unpark();
63854
- } else if (!entry.isIntersecting) {
63855
- cancelBootRequest(id2);
63856
- }
63908
+ intersectingRef.current = entry.isIntersecting;
63909
+ updateEffectiveVisibility();
63857
63910
  }
63858
63911
  },
63859
63912
  {
@@ -63869,9 +63922,9 @@ function DoenetViewer({
63869
63922
  if (!windowed) {
63870
63923
  return;
63871
63924
  }
63872
- if (!hasPersistencePath()) {
63925
+ if (!hasPersistencePath() && shouldWarnWindowedWithoutPersistence()) {
63873
63926
  console.warn(
63874
- "DoenetViewer mountPolicy: windowed mounting is enabled but neither flags.allowSaveState nor flags.allowLocalState is set, so parking would lose student work. This viewer will always stay live."
63927
+ "DoenetViewer mountPolicy: windowed mounting is enabled but neither flags.allowSaveState nor flags.allowLocalState is set, so parking would lose student work. Such viewers will always stay live. (Warned once per page.)"
63875
63928
  );
63876
63929
  }
63877
63930
  const unregister = registerWindowedViewer({
@@ -63891,6 +63944,13 @@ function DoenetViewer({
63891
63944
  unregister();
63892
63945
  };
63893
63946
  }, [windowed]);
63947
+ React__default.useEffect(() => {
63948
+ if (!windowed) {
63949
+ return;
63950
+ }
63951
+ keepLiveRef.current = keepLive;
63952
+ updateEffectiveVisibility();
63953
+ }, [windowed, keepLive]);
63894
63954
  if (inErrorState) {
63895
63955
  if (foundAutoVersion) {
63896
63956
  setIgnoreDetectedVersion(true);