@doenet/doenetml-iframe 0.7.20-dev.323 → 0.7.20-dev.324
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 +184 -0
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,190 @@ latest editor buffer:
|
|
|
69
69
|
> surface a completion signal or error to the caller — failures from the
|
|
70
70
|
> underlying ComLink RPC are logged to the console rather than thrown.
|
|
71
71
|
|
|
72
|
+
## DoenetViewer
|
|
73
|
+
|
|
74
|
+
### Host message protocol (SPLICE)
|
|
75
|
+
|
|
76
|
+
The viewer exchanges JSON messages with the host page via `postMessage`.
|
|
77
|
+
Viewer → host messages arrive on **your window** (the iframe posts to
|
|
78
|
+
`window.parent`, which is your page). Host → viewer requests are posted on
|
|
79
|
+
**your own window** too — the wrapper forwards these subjects into the
|
|
80
|
+
iframe: `SPLICE.getState.response`, `SPLICE.requestSolutionView.response`,
|
|
81
|
+
`SPLICE.submitAllAnswers`, and `SPLICE.flushState`. On a page with several
|
|
82
|
+
viewers, every viewer receives a forwarded request; correlate responses by
|
|
83
|
+
`activity_id`/`doc_id`/`message_id`.
|
|
84
|
+
|
|
85
|
+
The messages are documented in the subsections below:
|
|
86
|
+
|
|
87
|
+
| Subject | Direction | Purpose |
|
|
88
|
+
| ---------------------------------------------------------- | ------------- | ---------------------------------------------- |
|
|
89
|
+
| `SPLICE.reportScoreAndState` | viewer → host | periodic score/state saves |
|
|
90
|
+
| `SPLICE.getState` / `.response` | viewer ⇄ host | load saved state at boot |
|
|
91
|
+
| `SPLICE.flushState` / `.response` | host ⇄ viewer | on-demand state flush (lossless unmount) |
|
|
92
|
+
| `SPLICE.submitAllAnswers` / `.response` | host ⇄ viewer | submit every answer in the document |
|
|
93
|
+
| `SPLICE.requestSolutionView` / `.response` | viewer ⇄ host | permission gate for viewing solutions |
|
|
94
|
+
| `SPLICE.sendEvent` | viewer → host | analytics/event stream |
|
|
95
|
+
|
|
96
|
+
### Saving and restoring state (lossless unmount)
|
|
97
|
+
|
|
98
|
+
The viewer reports the student's serialized document state to the host as
|
|
99
|
+
they work: it posts `SPLICE.reportScoreAndState` messages to the host
|
|
100
|
+
window (the wrapper's iframe posts to `window.parent`, which is your page):
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
window.addEventListener("message", (e) => {
|
|
104
|
+
if (e.data?.subject === "SPLICE.reportScoreAndState") {
|
|
105
|
+
// e.data.state — serialized document state (opaque; store as-is)
|
|
106
|
+
// e.data.score, e.data.activity_id, e.data.doc_id
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
To restore, remount the viewer with the saved state:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
<DoenetViewer
|
|
115
|
+
doenetML={doenetML}
|
|
116
|
+
flags={{ allowLoadState: true }}
|
|
117
|
+
initialState={savedState}
|
|
118
|
+
/>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**The gap — and `SPLICE.flushState`.** Reports are throttled (one per 60
|
|
122
|
+
seconds per viewer), so at any moment the student may have committed work
|
|
123
|
+
that no report has delivered yet. A host that unmounts a viewer based on
|
|
124
|
+
save events alone silently loses that work. Before unmounting, request a
|
|
125
|
+
flush — post on your own window; the wrapper forwards it into the iframe:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
window.postMessage(
|
|
129
|
+
{ subject: "SPLICE.flushState", message_id: "my-id-123" },
|
|
130
|
+
"*",
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The flush settles in-flight updates and pushes any pending state out through
|
|
135
|
+
the **normal `SPLICE.reportScoreAndState` message** (which reaches your page
|
|
136
|
+
as shown above) — so a host that already persists those reports saves the
|
|
137
|
+
just-flushed state with no extra code. (No report is emitted when nothing is
|
|
138
|
+
pending, or when state saving is disabled — there is then nothing to lose.)
|
|
139
|
+
The viewer then replies with a stateless acknowledgement (again on your
|
|
140
|
+
window):
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
{
|
|
144
|
+
subject: "SPLICE.flushState.response",
|
|
145
|
+
message_id: "my-id-123", // echoed from the request
|
|
146
|
+
activity_id, doc_id, // to correlate on multi-viewer pages
|
|
147
|
+
success: true,
|
|
148
|
+
hadState: true, // false ⇒ nothing beyond initialization
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The acknowledgement is the completion signal: once it arrives, every saved
|
|
153
|
+
`reportScoreAndState` is current, so unmounting loses nothing — remounting
|
|
154
|
+
later with `initialState: <the last saved state>` (and
|
|
155
|
+
`flags: { allowLoadState: true }`) restores the document exactly, including
|
|
156
|
+
work an earlier report never delivered. `hadState: false` means the viewer
|
|
157
|
+
held no state beyond what it was initialized with (e.g. its core has not
|
|
158
|
+
booted yet) — equally safe to unmount.
|
|
159
|
+
|
|
160
|
+
> **Note:** Wrap the round-trip in a retry/timeout — the viewer's listener
|
|
161
|
+
> registers on mount, so a request posted moments after mounting can land
|
|
162
|
+
> before anyone is listening, and flushing is idempotent so re-posting is
|
|
163
|
+
> safe. Every viewer on the page receives a broadcast request and responds
|
|
164
|
+
> (correlate by `activity_id`/`doc_id`/`message_id`).
|
|
165
|
+
|
|
166
|
+
### Loading saved state at boot (`SPLICE.getState`)
|
|
167
|
+
|
|
168
|
+
With `flags: { allowLoadState: true }` and no `initialState` prop, the
|
|
169
|
+
viewer asks the host for saved state when it boots:
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
{
|
|
173
|
+
subject: "SPLICE.getState",
|
|
174
|
+
message_id,
|
|
175
|
+
cid, // content id of the DoenetML source
|
|
176
|
+
domain_id: "Doenet",
|
|
177
|
+
activity_id, doc_id, attempt_number, user_id,
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The viewer does not block on a reply — it boots fresh immediately and
|
|
182
|
+
**reboots seeded with the state** if a response arrives. If you have saved
|
|
183
|
+
state for this document (an object previously received from
|
|
184
|
+
`reportScoreAndState`, whose `cid` matches the request),
|
|
185
|
+
respond:
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
{ subject: "SPLICE.getState.response", message_id, state }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If there is no saved state, no response is needed. To surface a load
|
|
192
|
+
failure to the student instead, respond with
|
|
193
|
+
`{ subject: "SPLICE.getState.response", error: { code, message } }`
|
|
194
|
+
(and no `message_id`).
|
|
195
|
+
|
|
196
|
+
Passing `initialState` yourself (or `initialState: null` for "start
|
|
197
|
+
fresh") skips this request entirely.
|
|
198
|
+
|
|
199
|
+
### Submitting all answers (`SPLICE.submitAllAnswers`)
|
|
200
|
+
|
|
201
|
+
Post `{ subject: "SPLICE.submitAllAnswers" }` and the viewer submits every
|
|
202
|
+
answer in the document, then responds with
|
|
203
|
+
`{ subject: "SPLICE.submitAllAnswers.response", success }`.
|
|
204
|
+
|
|
205
|
+
> **Note:** this pair carries no correlation id — on a page with several
|
|
206
|
+
> viewers, every viewer submits and responds, and the responses cannot be
|
|
207
|
+
> told apart. Use it with a single viewer per page (its original use case)
|
|
208
|
+
> or treat it as fire-and-forget.
|
|
209
|
+
|
|
210
|
+
### Solution-view permission (`SPLICE.requestSolutionView`)
|
|
211
|
+
|
|
212
|
+
With `flags: { solutionDisplayMode: "buttonRequirePermission" }`, a student
|
|
213
|
+
opening a solution triggers a permission request to the host:
|
|
214
|
+
|
|
215
|
+
```js
|
|
216
|
+
{
|
|
217
|
+
subject: "SPLICE.requestSolutionView",
|
|
218
|
+
message_id,
|
|
219
|
+
activity_id, doc_id, attempt_number, user_id,
|
|
220
|
+
component_idx, // the solution component being opened
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Decide and respond — note the response echoes the id as **`messageId`**
|
|
225
|
+
(camelCase), unlike the snake_case request field:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
{ subject: "SPLICE.requestSolutionView.response", messageId, allowView: true }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The solution is revealed only when `allowView` is `true`.
|
|
232
|
+
|
|
233
|
+
### Event stream (`SPLICE.sendEvent`)
|
|
234
|
+
|
|
235
|
+
With `flags: { allowSaveEvents: true }`, the viewer emits an analytics
|
|
236
|
+
event for student interactions (answers submitted, solutions viewed,
|
|
237
|
+
content experienced, …). Fire-and-forget; no response is expected:
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
{
|
|
241
|
+
subject: "SPLICE.sendEvent",
|
|
242
|
+
message_id,
|
|
243
|
+
name, // mirrors data.verb
|
|
244
|
+
data: {
|
|
245
|
+
activityId, cid, docId, attemptNumber, variantIndex,
|
|
246
|
+
verb, // e.g. "answered", "experienced"
|
|
247
|
+
object, // JSON string: the component acted on
|
|
248
|
+
result, // JSON string: the outcome
|
|
249
|
+
context, // JSON string: additional context
|
|
250
|
+
timestamp, // "YYYY-MM-DD HH:MM:SS"
|
|
251
|
+
version,
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
72
256
|
## Development
|
|
73
257
|
|
|
74
258
|
Source code in `src/iframe-viewer-index.ts` and `src/iframe-editor-index.ts`
|
package/index.js
CHANGED
|
@@ -63172,7 +63172,7 @@ function ExternalVirtualKeyboard({
|
|
|
63172
63172
|
}
|
|
63173
63173
|
);
|
|
63174
63174
|
}
|
|
63175
|
-
const version = "0.7.20-dev.
|
|
63175
|
+
const version = "0.7.20-dev.324";
|
|
63176
63176
|
const latestDoenetmlVersion = version;
|
|
63177
63177
|
function subscribeToPinnedTheme() {
|
|
63178
63178
|
return () => {
|
|
@@ -63241,7 +63241,7 @@ function DoenetViewer({
|
|
|
63241
63241
|
if (event.origin !== window.location.origin) {
|
|
63242
63242
|
return;
|
|
63243
63243
|
}
|
|
63244
|
-
if (event.data.subject === "SPLICE.getState.response" || event.data.subject === "SPLICE.requestSolutionView.response" || event.data.subject == "SPLICE.submitAllAnswers") {
|
|
63244
|
+
if (event.data.subject === "SPLICE.getState.response" || event.data.subject === "SPLICE.requestSolutionView.response" || event.data.subject == "SPLICE.submitAllAnswers" || event.data.subject === "SPLICE.flushState") {
|
|
63245
63245
|
ref.current?.contentWindow?.postMessage(event.data);
|
|
63246
63246
|
return;
|
|
63247
63247
|
}
|