@citolab/tspci 2.8.4 → 2.9.7
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/CHANGELOG.md +67 -0
- package/package.json +4 -3
- package/preset/javascript/src/index.js +1 -0
- package/preset/preact/src/index.tsx +16 -0
- package/preset/typescript/src/index.ts +1 -0
- package/public/index.html +798 -323
- package/src/cli.js +57 -32
package/public/index.html
CHANGED
|
@@ -1,378 +1,853 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<title>Hello RequireJS</title>
|
|
5
|
+
<script
|
|
6
|
+
src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"
|
|
7
|
+
integrity="sha512-c3Nl8+7g4LMSTdrm621y7kf9v3SDPnhxLNhcjFJbKECVnmZHTdo+IRO05sNLTH/D3vA6u1X32ehoLC7WFVdheg=="
|
|
8
|
+
crossorigin="anonymous"
|
|
9
|
+
referrerpolicy="no-referrer"
|
|
10
|
+
></script>
|
|
11
|
+
<!-- <link rel="stylesheet" class="bootswatcher" href="//bootswatch.com/5/lumen/bootstrap.min.css" /> -->
|
|
12
|
+
|
|
13
|
+
<link
|
|
14
|
+
rel="stylesheet"
|
|
15
|
+
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
<style>
|
|
19
|
+
html {
|
|
20
|
+
font-size: 100%;
|
|
21
|
+
}
|
|
3
22
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/* min-width: 800px; */
|
|
21
|
-
|
|
22
|
-
max-width: 1024px;
|
|
23
|
-
height: 600px;
|
|
24
|
-
min-height: 600px;
|
|
25
|
-
/* max-height: 600px; */
|
|
26
|
-
border: 1px solid #eeeeee;
|
|
27
|
-
overflow: hidden;
|
|
28
|
-
margin: 0 auto;
|
|
29
|
-
background-color: white;
|
|
30
|
-
border-radius: 10px;
|
|
31
|
-
margin-top: 1rem;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
#devtools {
|
|
35
|
-
max-width: 1024px;
|
|
36
|
-
margin: 0 auto;
|
|
37
|
-
}
|
|
38
|
-
</style>
|
|
39
|
-
</head>
|
|
40
|
-
|
|
41
|
-
<body class="bg-light">
|
|
42
|
-
<div class="container">
|
|
43
|
-
<div class="alert alert-danger position-fixed mx-auto left-2 top-2" style="display: none" role="alert"
|
|
44
|
-
id="noresponse">
|
|
45
|
-
user has not yet interacted yet<br />getResponse should return undefined.
|
|
46
|
-
</div>
|
|
23
|
+
#pciContainer {
|
|
24
|
+
width: 100%;
|
|
25
|
+
/* min-width: 800px; */
|
|
26
|
+
|
|
27
|
+
max-width: 1024px;
|
|
28
|
+
height: 600px;
|
|
29
|
+
min-height: 600px;
|
|
30
|
+
/* max-height: 600px; */
|
|
31
|
+
border: 1px solid #dbe3ec;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
margin: 1.25rem auto 0;
|
|
34
|
+
background-color: #ffffff;
|
|
35
|
+
border-radius: 16px;
|
|
36
|
+
padding: 1rem;
|
|
37
|
+
box-shadow: 0 10px 26px rgba(15, 23, 42, 0.08);
|
|
38
|
+
}
|
|
47
39
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
40
|
+
qti-portable-custom-interaction {
|
|
41
|
+
display: block;
|
|
42
|
+
width: 100%;
|
|
43
|
+
height: 100%;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#devtools {
|
|
47
|
+
max-width: 1024px;
|
|
48
|
+
margin: 0 auto;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.container {
|
|
52
|
+
max-width: 1360px;
|
|
53
|
+
margin-left: auto;
|
|
54
|
+
margin-right: auto;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.app-shell {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: flex-start;
|
|
60
|
+
gap: 1rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#main {
|
|
64
|
+
flex: 1 1 auto;
|
|
65
|
+
min-width: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#right-pane {
|
|
69
|
+
flex: 0 0 320px;
|
|
70
|
+
width: 320px;
|
|
71
|
+
padding-top: 1.25rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.dev-layout {
|
|
75
|
+
margin-top: 1rem;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.dev-main {
|
|
79
|
+
min-width: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dev-sidepane {
|
|
83
|
+
position: sticky;
|
|
84
|
+
top: 1rem;
|
|
85
|
+
width: 100%;
|
|
86
|
+
max-height: calc(100vh - 2rem);
|
|
87
|
+
overflow: auto;
|
|
88
|
+
background: #fff;
|
|
89
|
+
border: 1px solid #dee2e6;
|
|
90
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
|
91
|
+
padding: 1rem;
|
|
92
|
+
border-radius: 0.5rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@media (max-width: 1100px) {
|
|
96
|
+
.app-shell {
|
|
97
|
+
display: block;
|
|
96
98
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
await store.replay([...actions], { until: value, animate: false });
|
|
108
|
-
isReplaying.current = false;
|
|
109
|
-
}
|
|
99
|
+
|
|
100
|
+
#right-pane {
|
|
101
|
+
width: 100%;
|
|
102
|
+
max-width: 100%;
|
|
103
|
+
padding-top: 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.dev-sidepane {
|
|
107
|
+
position: static;
|
|
108
|
+
max-height: none;
|
|
110
109
|
}
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
.json-block {
|
|
113
|
+
min-height: 180px;
|
|
114
|
+
max-height: 280px;
|
|
115
|
+
overflow: auto;
|
|
116
|
+
background: #111827;
|
|
117
|
+
color: #e5e7eb;
|
|
118
|
+
border-radius: 0.5rem;
|
|
119
|
+
padding: 0.75rem;
|
|
120
|
+
font-size: 12px;
|
|
121
|
+
line-height: 1.4;
|
|
122
|
+
margin: 0;
|
|
123
|
+
border: 1px solid #374151;
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
localStorage.removeItem(item.typeIdentifier + "_props");
|
|
124
|
-
localStorage.removeItem(item.typeIdentifier + "_response");
|
|
126
|
+
.json-key {
|
|
127
|
+
color: #93c5fd;
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
location.reload();
|
|
130
|
+
.json-string {
|
|
131
|
+
color: #86efac;
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
item.resetResponse();
|
|
134
|
+
.json-number {
|
|
135
|
+
color: #fca5a5;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
138
|
+
.json-boolean {
|
|
139
|
+
color: #fcd34d;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.json-null {
|
|
143
|
+
color: #c4b5fd;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.icon-btn {
|
|
147
|
+
border: 1px solid #ced4da;
|
|
148
|
+
background: #fff;
|
|
149
|
+
border-radius: 999px;
|
|
150
|
+
width: 26px;
|
|
151
|
+
height: 26px;
|
|
152
|
+
display: inline-flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
justify-content: center;
|
|
155
|
+
font-size: 13px;
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.spin {
|
|
160
|
+
animation: spin 1s linear infinite;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@keyframes spin {
|
|
164
|
+
from {
|
|
165
|
+
transform: rotate(0deg);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
to {
|
|
169
|
+
transform: rotate(360deg);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.tiny-note {
|
|
174
|
+
font-size: 12px;
|
|
175
|
+
color: #6c757d;
|
|
176
|
+
margin-top: 0.35rem;
|
|
177
|
+
}
|
|
178
|
+
</style>
|
|
179
|
+
</head>
|
|
180
|
+
|
|
181
|
+
<body class="bg-light">
|
|
182
|
+
<div class="container app-shell">
|
|
183
|
+
<div id="main">
|
|
184
|
+
<div
|
|
185
|
+
class="alert alert-danger position-fixed mx-auto left-2 top-2"
|
|
186
|
+
style="display: none"
|
|
187
|
+
role="alert"
|
|
188
|
+
id="noresponse"
|
|
189
|
+
>
|
|
190
|
+
user has not yet interacted yet<br />getResponse should return
|
|
191
|
+
undefined.
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div id="pciContainer" class="shadow-sm">
|
|
195
|
+
<div id="loading" class="text-muted mx-auto p-4">Loading...</div>
|
|
196
|
+
</div>
|
|
197
|
+
<div id="devtools"></div>
|
|
198
|
+
</div>
|
|
199
|
+
<div id="right-pane"></div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<script type="module">
|
|
203
|
+
import {
|
|
204
|
+
h,
|
|
205
|
+
render,
|
|
206
|
+
} from "https://unpkg.com/preact@latest?module";
|
|
207
|
+
import {
|
|
208
|
+
useState,
|
|
209
|
+
useEffect,
|
|
210
|
+
useRef,
|
|
211
|
+
} from "https://unpkg.com/preact@latest/hooks/dist/hooks.module.js?module";
|
|
212
|
+
import htm from "https://unpkg.com/htm?module";
|
|
213
|
+
// Initialize htm with Preact
|
|
214
|
+
const html = htm.bind(h);
|
|
215
|
+
|
|
216
|
+
const CONFIG_TO_DATA_PROPERTY_KEYS = [
|
|
217
|
+
"title",
|
|
218
|
+
"prompt",
|
|
219
|
+
"width",
|
|
220
|
+
"height",
|
|
221
|
+
];
|
|
222
|
+
const CONFIG_CHANGE_DEBOUNCE_MS = 1000;
|
|
223
|
+
const INTERACTION_REFRESH_DELAY_MS = 120;
|
|
224
|
+
|
|
225
|
+
function mergeConfigPropertiesIntoDataProperties(
|
|
226
|
+
properties,
|
|
227
|
+
dataProperties = {},
|
|
228
|
+
) {
|
|
229
|
+
const merged = { ...(dataProperties || {}) };
|
|
230
|
+
if (!properties) {
|
|
231
|
+
return merged;
|
|
232
|
+
}
|
|
233
|
+
CONFIG_TO_DATA_PROPERTY_KEYS.forEach((key) => {
|
|
234
|
+
const value = properties[key];
|
|
235
|
+
if (value !== null && typeof value !== "undefined") {
|
|
236
|
+
merged[key] = String(value);
|
|
146
237
|
}
|
|
147
|
-
|
|
148
|
-
|
|
238
|
+
});
|
|
239
|
+
return merged;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getQtiPortableCustomInteractionElement() {
|
|
243
|
+
return document.querySelector(
|
|
244
|
+
"#pciContainer qti-portable-custom-interaction",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getQtiAssessmentItemElement() {
|
|
249
|
+
return document.querySelector("#pciContainer qti-assessment-item");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function applyDataPropertiesToQtiHost(dataProperties = {}) {
|
|
253
|
+
const host = getQtiPortableCustomInteractionElement();
|
|
254
|
+
if (!host) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
Object.keys(dataProperties || {}).forEach((key) => {
|
|
258
|
+
const normalizedKey = String(key || "")
|
|
259
|
+
.trim()
|
|
260
|
+
.replace(/^data-/, "");
|
|
261
|
+
if (!normalizedKey) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const value = dataProperties[key];
|
|
265
|
+
if (value === null || typeof value === "undefined") {
|
|
266
|
+
return;
|
|
149
267
|
}
|
|
150
|
-
|
|
151
|
-
|
|
268
|
+
host.setAttribute(`data-${normalizedKey}`, String(value));
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function syncConfigPropertiesToHostDataProperties(
|
|
273
|
+
config,
|
|
274
|
+
dataProperties,
|
|
275
|
+
) {
|
|
276
|
+
if (!config || !config.properties) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
currentPciHostDataProperties = mergeConfigPropertiesIntoDataProperties(
|
|
280
|
+
config.properties,
|
|
281
|
+
dataProperties,
|
|
282
|
+
);
|
|
283
|
+
applyDataPropertiesToQtiHost(currentPciHostDataProperties);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let currentPciFactory = null;
|
|
287
|
+
let currentPciConfig = null;
|
|
288
|
+
let currentPciHostDataProperties = {};
|
|
289
|
+
let currentPciInteraction = null;
|
|
290
|
+
let currentAssessmentItem = null;
|
|
291
|
+
let configUpdateTimer = null;
|
|
292
|
+
|
|
293
|
+
function ensureOptionalTaoMethods(target) {
|
|
294
|
+
if (!target || typeof target !== "object") {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (typeof target.off !== "function") {
|
|
298
|
+
target.off = () => {};
|
|
299
|
+
}
|
|
300
|
+
if (typeof target.on !== "function") {
|
|
301
|
+
target.on = () => {};
|
|
302
|
+
}
|
|
303
|
+
if (typeof target.trigger !== "function") {
|
|
304
|
+
target.trigger = () => {};
|
|
305
|
+
}
|
|
306
|
+
if (typeof target.resetResponse !== "function") {
|
|
307
|
+
target.resetResponse = () => {};
|
|
152
308
|
}
|
|
153
309
|
}
|
|
154
310
|
|
|
155
|
-
function
|
|
156
|
-
const
|
|
157
|
-
|
|
311
|
+
function setCurrentRuntime(factory, interaction, config, dataProperties) {
|
|
312
|
+
const runtimeItem =
|
|
313
|
+
interaction && typeof interaction === "object"
|
|
314
|
+
? interaction
|
|
315
|
+
: factory;
|
|
316
|
+
ensureOptionalTaoMethods(runtimeItem);
|
|
317
|
+
currentPciFactory = factory;
|
|
318
|
+
currentPciInteraction =
|
|
319
|
+
interaction && typeof interaction === "object" ? interaction : null;
|
|
320
|
+
currentPciConfig = config;
|
|
321
|
+
currentPciHostDataProperties = dataProperties || {};
|
|
322
|
+
renderDevtools();
|
|
323
|
+
renderConfigPanel();
|
|
158
324
|
}
|
|
159
325
|
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
326
|
+
function refreshCurrentPci(options = {}) {
|
|
327
|
+
if (
|
|
328
|
+
!currentPciFactory ||
|
|
329
|
+
typeof currentPciFactory.getInstance !== "function"
|
|
330
|
+
) {
|
|
164
331
|
return;
|
|
165
332
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
333
|
+
const config = currentPciConfig || { properties: {} };
|
|
334
|
+
const state = Object.prototype.hasOwnProperty.call(
|
|
335
|
+
options,
|
|
336
|
+
"stateOverride",
|
|
337
|
+
)
|
|
338
|
+
? options.stateOverride
|
|
339
|
+
: currentAssessmentItem?.state;
|
|
340
|
+
if (typeof currentPciInteraction?.oncompleted === "function") {
|
|
341
|
+
currentPciInteraction.oncompleted();
|
|
342
|
+
}
|
|
343
|
+
if (typeof currentPciInteraction?.destroy === "function") {
|
|
344
|
+
currentPciInteraction.destroy();
|
|
170
345
|
}
|
|
346
|
+
const dataProperties = mergeConfigPropertiesIntoDataProperties(
|
|
347
|
+
config.properties,
|
|
348
|
+
currentPciHostDataProperties || {},
|
|
349
|
+
);
|
|
350
|
+
const dom = createQtiPciHost(currentPciFactory, dataProperties);
|
|
351
|
+
const originalOnReady = config.onready;
|
|
352
|
+
const configWithOnReady = {
|
|
353
|
+
...config,
|
|
354
|
+
onready: (...args) => {
|
|
355
|
+
if (typeof originalOnReady === "function") {
|
|
356
|
+
originalOnReady(...args);
|
|
357
|
+
}
|
|
358
|
+
if (typeof options.onready === "function") {
|
|
359
|
+
options.onready(...args);
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
const interaction = currentPciFactory.getInstance(
|
|
364
|
+
dom,
|
|
365
|
+
configWithOnReady,
|
|
366
|
+
state,
|
|
367
|
+
);
|
|
368
|
+
setCurrentRuntime(
|
|
369
|
+
currentPciFactory,
|
|
370
|
+
interaction,
|
|
371
|
+
config,
|
|
372
|
+
dataProperties,
|
|
373
|
+
);
|
|
171
374
|
}
|
|
172
375
|
|
|
173
|
-
function
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
376
|
+
function renderDevtools() {
|
|
377
|
+
const devtools = document.getElementById("devtools");
|
|
378
|
+
if (!devtools) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
render(html`<${App} />`, devtools);
|
|
177
382
|
}
|
|
178
383
|
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
isReplaying.current = true;
|
|
183
|
-
replayedIndex.current = 0;
|
|
184
|
-
await store.replay([...actions]);
|
|
185
|
-
isReplaying.current = false;
|
|
384
|
+
function applyConfigChangesDebounced() {
|
|
385
|
+
if (configUpdateTimer) {
|
|
386
|
+
clearTimeout(configUpdateTimer);
|
|
186
387
|
}
|
|
388
|
+
configUpdateTimer = setTimeout(() => {
|
|
389
|
+
const current = currentPciConfig;
|
|
390
|
+
if (!currentPciFactory || !current || !current.properties) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
syncConfigPropertiesToHostDataProperties(
|
|
394
|
+
current,
|
|
395
|
+
currentPciHostDataProperties,
|
|
396
|
+
);
|
|
397
|
+
localStorage.setItem(
|
|
398
|
+
currentPciFactory.typeIdentifier + "_props",
|
|
399
|
+
JSON.stringify(current.properties || {}),
|
|
400
|
+
);
|
|
401
|
+
refreshCurrentPci();
|
|
402
|
+
renderConfigPanel();
|
|
403
|
+
}, CONFIG_CHANGE_DEBOUNCE_MS);
|
|
187
404
|
}
|
|
188
405
|
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
|
|
406
|
+
function changeConfigProperty(key, value) {
|
|
407
|
+
const current = currentPciConfig;
|
|
408
|
+
if (!current || !current.properties) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
current.properties[key] = value;
|
|
412
|
+
renderConfigPanel();
|
|
413
|
+
applyConfigChangesDebounced();
|
|
192
414
|
}
|
|
193
415
|
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
416
|
+
function ConfigPanel() {
|
|
417
|
+
const configSource = currentPciConfig;
|
|
418
|
+
const keyArray = configSource?.properties
|
|
419
|
+
? Object.keys(configSource.properties).map((keyName) => ({
|
|
420
|
+
key: keyName,
|
|
421
|
+
value: configSource.properties[keyName],
|
|
422
|
+
}))
|
|
423
|
+
: [];
|
|
424
|
+
|
|
425
|
+
return html`
|
|
426
|
+
<aside class="dev-sidepane">
|
|
427
|
+
<h6 class="mb-3">Config</h6>
|
|
428
|
+
<form onSubmit="${(e) => e.preventDefault()}">
|
|
429
|
+
${keyArray.map(
|
|
430
|
+
({ key, value }) =>
|
|
431
|
+
html`<div class="mb-2">
|
|
432
|
+
<label class="form-label text-muted mb-1">${key}</label>
|
|
433
|
+
<input
|
|
434
|
+
type="text"
|
|
435
|
+
class="form-control"
|
|
436
|
+
value="${value}"
|
|
437
|
+
onInput="${(e) =>
|
|
438
|
+
changeConfigProperty(key, e.target.value)}"
|
|
439
|
+
/>
|
|
440
|
+
</div>`,
|
|
441
|
+
)}
|
|
442
|
+
</form>
|
|
443
|
+
</aside>
|
|
444
|
+
`;
|
|
197
445
|
}
|
|
198
446
|
|
|
199
|
-
function
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
447
|
+
function renderConfigPanel() {
|
|
448
|
+
const pane = document.getElementById("right-pane");
|
|
449
|
+
if (!pane) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
render(html`<${ConfigPanel} />`, pane);
|
|
204
453
|
}
|
|
205
454
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
></button>
|
|
217
|
-
</div>
|
|
218
|
-
<div class="offcanvas-body">
|
|
219
|
-
<ul class="card-body">
|
|
220
|
-
${keyArray.map(
|
|
221
|
-
({ key, value }) => html`<div>
|
|
222
|
-
<div>${key}</div>
|
|
223
|
-
<input
|
|
224
|
-
type="text"
|
|
225
|
-
class="form-control"
|
|
226
|
-
value="${value}"
|
|
227
|
-
onkeyup="${(e) => changeConfig(key, e.target.value)}"
|
|
228
|
-
/>
|
|
229
|
-
</div>`
|
|
230
|
-
)}
|
|
231
|
-
</ul>
|
|
232
|
-
<button class="btn btn-primary" onClick="${() => reConfig()}">save and reload</button>
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
455
|
+
function toPrettyJson(value) {
|
|
456
|
+
if (typeof value === "undefined") {
|
|
457
|
+
return "undefined";
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
return JSON.stringify(value, null, 2);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
return `[unserializable] ${String(error?.message || error)}`;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
235
465
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
</div>
|
|
466
|
+
function escapeHtml(value) {
|
|
467
|
+
return String(value)
|
|
468
|
+
.replace(/&/g, "&")
|
|
469
|
+
.replace(/</g, "<")
|
|
470
|
+
.replace(/>/g, ">");
|
|
471
|
+
}
|
|
243
472
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
getResponse
|
|
252
|
-
</button>
|
|
253
|
-
<button class="btn btn-outline-secondary" onClick="${() => alert(item.getState())}">getStates</button>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
473
|
+
function escapeHtmlAttribute(value) {
|
|
474
|
+
return String(value)
|
|
475
|
+
.replace(/&/g, "&")
|
|
476
|
+
.replace(/"/g, """)
|
|
477
|
+
.replace(/</g, "<")
|
|
478
|
+
.replace(/>/g, ">");
|
|
479
|
+
}
|
|
256
480
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
481
|
+
function highlightJson(jsonText) {
|
|
482
|
+
const escaped = escapeHtml(jsonText);
|
|
483
|
+
return escaped.replace(
|
|
484
|
+
/("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
|
485
|
+
(match) => {
|
|
486
|
+
if (match.startsWith('"') && match.endsWith(":")) {
|
|
487
|
+
return `<span class="json-key">${match}</span>`;
|
|
488
|
+
}
|
|
489
|
+
if (match.startsWith('"')) {
|
|
490
|
+
return `<span class="json-string">${match}</span>`;
|
|
491
|
+
}
|
|
492
|
+
if (match === "true" || match === "false") {
|
|
493
|
+
return `<span class="json-boolean">${match}</span>`;
|
|
494
|
+
}
|
|
495
|
+
if (match === "null") {
|
|
496
|
+
return `<span class="json-null">${match}</span>`;
|
|
497
|
+
}
|
|
498
|
+
return `<span class="json-number">${match}</span>`;
|
|
499
|
+
},
|
|
500
|
+
);
|
|
501
|
+
}
|
|
263
502
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
503
|
+
function App() {
|
|
504
|
+
const [responseSnapshot, setResponseSnapshot] = useState("undefined");
|
|
505
|
+
const [stateSnapshot, setStateSnapshot] = useState("undefined");
|
|
506
|
+
const [isRestoringState, setIsRestoringState] = useState(false);
|
|
507
|
+
const interactionRefreshTimer = useRef(null);
|
|
508
|
+
const getCurrentAssessmentItem = () =>
|
|
509
|
+
currentAssessmentItem || getQtiAssessmentItemElement();
|
|
510
|
+
|
|
511
|
+
function refreshLiveData() {
|
|
512
|
+
const assessmentItem = getCurrentAssessmentItem();
|
|
513
|
+
let responseValue = undefined;
|
|
514
|
+
let stateValue = undefined;
|
|
515
|
+
|
|
516
|
+
if (assessmentItem) {
|
|
517
|
+
try {
|
|
518
|
+
stateValue = assessmentItem.state;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
stateValue = `[assessment state threw] ${String(error?.message || error)}`;
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const variables = assessmentItem.variables;
|
|
524
|
+
const responseVariable = Array.isArray(variables)
|
|
525
|
+
? variables.find((v) => v?.identifier === "RESPONSE")
|
|
526
|
+
: undefined;
|
|
527
|
+
responseValue = responseVariable?.value;
|
|
528
|
+
} catch (error) {
|
|
529
|
+
responseValue = `[assessment variables threw] ${String(error?.message || error)}`;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
setResponseSnapshot(toPrettyJson(responseValue));
|
|
534
|
+
setStateSnapshot(toPrettyJson(stateValue));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
useEffect(() => {
|
|
538
|
+
refreshLiveData();
|
|
539
|
+
}, []);
|
|
540
|
+
|
|
541
|
+
useEffect(() => {
|
|
542
|
+
const container = document.getElementById("pciContainer");
|
|
543
|
+
if (!container) return;
|
|
544
|
+
const onAssessmentItemConnected = (event) => {
|
|
545
|
+
const detail = event?.detail;
|
|
546
|
+
const candidate =
|
|
547
|
+
detail?.assessmentItem ||
|
|
548
|
+
detail?.["qti-assessment-item"] ||
|
|
549
|
+
(detail?.tagName === "QTI-ASSESSMENT-ITEM" ? detail : null);
|
|
550
|
+
if (candidate && candidate.tagName === "QTI-ASSESSMENT-ITEM") {
|
|
551
|
+
currentAssessmentItem = candidate;
|
|
552
|
+
refreshLiveData();
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
const onInteractionChanged = (event) => {
|
|
556
|
+
if (interactionRefreshTimer.current) {
|
|
557
|
+
clearTimeout(interactionRefreshTimer.current);
|
|
558
|
+
}
|
|
559
|
+
interactionRefreshTimer.current = setTimeout(() => {
|
|
560
|
+
refreshLiveData();
|
|
561
|
+
interactionRefreshTimer.current = null;
|
|
562
|
+
}, INTERACTION_REFRESH_DELAY_MS);
|
|
563
|
+
};
|
|
564
|
+
container.addEventListener(
|
|
565
|
+
"qti-assessment-item-connected",
|
|
566
|
+
onAssessmentItemConnected,
|
|
567
|
+
);
|
|
568
|
+
container.addEventListener(
|
|
569
|
+
"qti-interaction-changed",
|
|
570
|
+
onInteractionChanged,
|
|
571
|
+
);
|
|
572
|
+
return () => {
|
|
573
|
+
if (interactionRefreshTimer.current) {
|
|
574
|
+
clearTimeout(interactionRefreshTimer.current);
|
|
575
|
+
interactionRefreshTimer.current = null;
|
|
576
|
+
}
|
|
577
|
+
container.removeEventListener(
|
|
578
|
+
"qti-assessment-item-connected",
|
|
579
|
+
onAssessmentItemConnected,
|
|
580
|
+
);
|
|
581
|
+
container.removeEventListener(
|
|
582
|
+
"qti-interaction-changed",
|
|
583
|
+
onInteractionChanged,
|
|
584
|
+
);
|
|
585
|
+
};
|
|
586
|
+
}, []);
|
|
587
|
+
|
|
588
|
+
useEffect(() => {
|
|
589
|
+
const onGlobalInteractionChanged = (event) => {
|
|
590
|
+
if (interactionRefreshTimer.current) {
|
|
591
|
+
clearTimeout(interactionRefreshTimer.current);
|
|
592
|
+
}
|
|
593
|
+
interactionRefreshTimer.current = setTimeout(() => {
|
|
594
|
+
refreshLiveData();
|
|
595
|
+
interactionRefreshTimer.current = null;
|
|
596
|
+
}, INTERACTION_REFRESH_DELAY_MS);
|
|
597
|
+
};
|
|
598
|
+
document.addEventListener(
|
|
599
|
+
"qti-interaction-changed",
|
|
600
|
+
onGlobalInteractionChanged,
|
|
601
|
+
true,
|
|
602
|
+
);
|
|
603
|
+
return () => {
|
|
604
|
+
document.removeEventListener(
|
|
605
|
+
"qti-interaction-changed",
|
|
606
|
+
onGlobalInteractionChanged,
|
|
607
|
+
true,
|
|
608
|
+
);
|
|
609
|
+
};
|
|
610
|
+
}, []);
|
|
611
|
+
|
|
612
|
+
useEffect(() => {
|
|
613
|
+
return () => {
|
|
614
|
+
if (configUpdateTimer) {
|
|
615
|
+
clearTimeout(configUpdateTimer);
|
|
616
|
+
configUpdateTimer = null;
|
|
617
|
+
}
|
|
618
|
+
if (interactionRefreshTimer.current) {
|
|
619
|
+
clearTimeout(interactionRefreshTimer.current);
|
|
620
|
+
interactionRefreshTimer.current = null;
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
}, []);
|
|
624
|
+
|
|
625
|
+
function emptyAndRestoreState() {
|
|
626
|
+
if (isRestoringState) return;
|
|
627
|
+
const storedState = currentAssessmentItem?.state;
|
|
628
|
+
const storageKey = currentPciFactory
|
|
629
|
+
? `${currentPciFactory.typeIdentifier}_state_restore`
|
|
630
|
+
: "pci_state_restore";
|
|
631
|
+
try {
|
|
632
|
+
sessionStorage.setItem(storageKey, JSON.stringify(storedState));
|
|
633
|
+
} catch (error) {
|
|
634
|
+
// Ignore storage failures; fallback to in-memory state.
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let reinitState = storedState;
|
|
638
|
+
try {
|
|
639
|
+
const fromStorage = sessionStorage.getItem(storageKey);
|
|
640
|
+
if (fromStorage !== null) {
|
|
641
|
+
reinitState = JSON.parse(fromStorage);
|
|
642
|
+
}
|
|
643
|
+
} catch (error) {
|
|
644
|
+
reinitState = storedState;
|
|
645
|
+
}
|
|
281
646
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
647
|
+
setIsRestoringState(true);
|
|
648
|
+
refreshCurrentPci({
|
|
649
|
+
stateOverride: reinitState,
|
|
650
|
+
onready: () => {
|
|
651
|
+
setIsRestoringState(false);
|
|
652
|
+
refreshLiveData();
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return html`
|
|
658
|
+
<div class="dev-layout">
|
|
659
|
+
<div class="dev-main">
|
|
660
|
+
<div class="row g-3 mt-1">
|
|
661
|
+
<div class="col-md-6">
|
|
662
|
+
<h6 class="mb-1">Response</h6>
|
|
663
|
+
<pre class="json-block"><code dangerouslySetInnerHTML=${{
|
|
664
|
+
__html: highlightJson(responseSnapshot),
|
|
665
|
+
}}></code></pre>
|
|
666
|
+
</div>
|
|
667
|
+
<div class="col-md-6">
|
|
668
|
+
<div
|
|
669
|
+
class="d-flex justify-content-between align-items-center"
|
|
289
670
|
>
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
671
|
+
<h6 class="mb-1">State</h6>
|
|
672
|
+
<button
|
|
673
|
+
class="icon-btn ${isRestoringState ? "spin" : ""}"
|
|
674
|
+
title="empty the state and restore"
|
|
675
|
+
onClick="${() => emptyAndRestoreState()}"
|
|
676
|
+
>
|
|
677
|
+
↻
|
|
678
|
+
</button>
|
|
679
|
+
</div>
|
|
680
|
+
<pre class="json-block"><code dangerouslySetInnerHTML=${{
|
|
681
|
+
__html: highlightJson(stateSnapshot),
|
|
682
|
+
}}></code></pre>
|
|
293
683
|
</div>
|
|
294
684
|
</div>
|
|
295
685
|
</div>
|
|
296
|
-
${store && html`<div class="mt-3 d-flex flex-column gap-2">
|
|
297
|
-
<div class="d-flex gap-2 align-items-center">
|
|
298
|
-
<button class="btn btn-outline-secondary" onClick="${() => playState()}">
|
|
299
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play" viewBox="0 0 16 16">
|
|
300
|
-
<path d="M3.905 14.21a1 1 0 0 1-1.555-.832V2.622a1 1 0 0 1 1.555-.832l9.066 5.293a1 1 0 0 1 0 1.664l-9.066 5.293zm-.57-1.664L11.4 8 3.335 2.454v10.092z"/>
|
|
301
|
-
</svg>
|
|
302
|
-
</button>
|
|
303
|
-
<div class="w-100 d-flex"><input
|
|
304
|
-
class="form-control-range w-100"
|
|
305
|
-
type="range"
|
|
306
|
-
min="0"
|
|
307
|
-
max="${maxRange}"
|
|
308
|
-
value=${currentRangeValue}
|
|
309
|
-
onInput=${(e) => scrubRange(e.target.value)}
|
|
310
|
-
step="1"
|
|
311
|
-
/><div>${maxRange}</div></div>
|
|
312
|
-
</div>
|
|
313
|
-
</div>` }
|
|
314
|
-
|
|
315
686
|
</div>
|
|
316
687
|
`;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
688
|
+
}
|
|
689
|
+
function register(item) {
|
|
690
|
+
let state = undefined;
|
|
691
|
+
const localStorageState = localStorage.getItem(
|
|
692
|
+
item.typeIdentifier + "_state",
|
|
693
|
+
);
|
|
694
|
+
if (localStorageState) {
|
|
695
|
+
state = JSON.parse(localStorageState);
|
|
696
|
+
}
|
|
697
|
+
let props = {};
|
|
698
|
+
const localStorageProps = localStorage.getItem(
|
|
699
|
+
item.typeIdentifier + "_props",
|
|
700
|
+
);
|
|
701
|
+
if (localStorageProps) {
|
|
702
|
+
props = JSON.parse(localStorageProps);
|
|
703
|
+
}
|
|
704
|
+
const dataProperties = mergeConfigPropertiesIntoDataProperties(props);
|
|
705
|
+
item.qtiHostDataProperties = dataProperties;
|
|
706
|
+
const dom = createQtiPciHost(item, dataProperties);
|
|
707
|
+
const config = {
|
|
708
|
+
onready: () => {
|
|
709
|
+
const loading = document.getElementById("loading");
|
|
710
|
+
if (loading) {
|
|
711
|
+
loading.innerHTML = "";
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
properties: props,
|
|
715
|
+
};
|
|
337
716
|
|
|
338
|
-
|
|
717
|
+
const interaction = item.getInstance(dom, config, state);
|
|
718
|
+
setCurrentRuntime(item, interaction, config, dataProperties);
|
|
719
|
+
syncConfigPropertiesToHostDataProperties(config, dataProperties);
|
|
720
|
+
localStorage.removeItem(item.typeIdentifier + "_state");
|
|
721
|
+
}
|
|
339
722
|
|
|
340
|
-
|
|
341
|
-
|
|
723
|
+
async function loadQtiComponents() {
|
|
724
|
+
if (customElements.get("qti-portable-custom-interaction")) {
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
const moduleCandidates = [
|
|
728
|
+
"https://cdn.jsdelivr.net/npm/@citolab/qti-components/+esm",
|
|
729
|
+
"https://cdn.jsdelivr.net/npm/@citolab/qti-components/dist/index.js",
|
|
730
|
+
"https://unpkg.com/@citolab/qti-components/dist/index.js?module",
|
|
731
|
+
];
|
|
732
|
+
|
|
733
|
+
for (const modulePath of moduleCandidates) {
|
|
734
|
+
try {
|
|
735
|
+
await import(modulePath);
|
|
736
|
+
if (customElements.get("qti-portable-custom-interaction")) {
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
} catch (error) {
|
|
740
|
+
// Try the next candidate.
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
console.warn(
|
|
745
|
+
"Could not load qti-components; using default PCI container rendering.",
|
|
746
|
+
);
|
|
747
|
+
return false;
|
|
342
748
|
}
|
|
343
|
-
localStorage.removeItem(item.typeIdentifier + "_state");
|
|
344
|
-
}
|
|
345
749
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
750
|
+
function createQtiPciHost(item, dataProperties = {}) {
|
|
751
|
+
const container = document.getElementById("pciContainer");
|
|
752
|
+
container.innerHTML = "";
|
|
753
|
+
currentAssessmentItem = null;
|
|
754
|
+
|
|
755
|
+
if (!customElements.get("qti-portable-custom-interaction")) {
|
|
756
|
+
return container;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const typeIdentifier = item.typeIdentifier || "pci";
|
|
760
|
+
const escapedTypeIdentifier = escapeHtmlAttribute(typeIdentifier);
|
|
761
|
+
const dataAttributes = Object.keys(dataProperties || {})
|
|
762
|
+
.map((key) => {
|
|
763
|
+
const normalizedKey = String(key || "")
|
|
764
|
+
.trim()
|
|
765
|
+
.replace(/^data-/, "");
|
|
766
|
+
if (!normalizedKey) {
|
|
767
|
+
return "";
|
|
768
|
+
}
|
|
769
|
+
const value = dataProperties[key];
|
|
770
|
+
if (value === null || typeof value === "undefined") {
|
|
771
|
+
return "";
|
|
772
|
+
}
|
|
773
|
+
return ` data-${normalizedKey}="${escapeHtmlAttribute(value)}"`;
|
|
774
|
+
})
|
|
775
|
+
.join("");
|
|
776
|
+
|
|
777
|
+
const qtiItem = document.createElement("qti-item");
|
|
778
|
+
qtiItem.innerHTML = `
|
|
779
|
+
<qti-assessment-item identifier="PREVIEW-ITEM" title="${escapedTypeIdentifier}" adaptive="false" time-dependent="false">
|
|
780
|
+
<qti-response-declaration identifier="RESPONSE" cardinality="single" base-type="string"></qti-response-declaration>
|
|
781
|
+
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float"></qti-outcome-declaration>
|
|
782
|
+
<qti-item-body>
|
|
783
|
+
<qti-portable-custom-interaction response-identifier="RESPONSE" module="pci" custom-interaction-type-identifier="${escapedTypeIdentifier}"${dataAttributes}>
|
|
784
|
+
<qti-interaction-modules>
|
|
785
|
+
<qti-interaction-module id="pci" primary-path="./index"></qti-interaction-module>
|
|
786
|
+
</qti-interaction-modules>
|
|
787
|
+
<qti-interaction-markup>
|
|
788
|
+
<div data-qti-pci-runtime-host style="height:100%"></div>
|
|
789
|
+
</qti-interaction-markup>
|
|
790
|
+
</qti-portable-custom-interaction>
|
|
791
|
+
</qti-item-body>
|
|
792
|
+
</qti-assessment-item>
|
|
793
|
+
`;
|
|
794
|
+
container.appendChild(qtiItem);
|
|
795
|
+
|
|
796
|
+
return (
|
|
797
|
+
qtiItem.querySelector(
|
|
798
|
+
"[data-qti-pci-runtime-host]",
|
|
799
|
+
) || qtiItem.querySelector("qti-portable-custom-interaction")
|
|
800
|
+
);
|
|
801
|
+
}
|
|
349
802
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
render(html`<${App} item="${itemInstance}" />`, document.getElementById("devtools"));
|
|
803
|
+
define("qtiCustomInteractionContext", (qtiCustomInteractionContext) => {
|
|
804
|
+
return { register: (ctxA) => register(ctxA) };
|
|
353
805
|
});
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
</
|
|
377
|
-
|
|
378
|
-
|
|
806
|
+
|
|
807
|
+
function loadItem() {
|
|
808
|
+
requirejs(["index.js"], (itemInstance) => {
|
|
809
|
+
currentAssessmentItem = null;
|
|
810
|
+
if (!currentPciFactory) {
|
|
811
|
+
currentPciFactory = itemInstance;
|
|
812
|
+
}
|
|
813
|
+
renderDevtools();
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
window.onload = async () => {
|
|
817
|
+
// Some runtimes assume a global `off()` exists (TAO compatibility).
|
|
818
|
+
// In the live IMS preview this should be optional, so provide a safe fallback.
|
|
819
|
+
if (typeof globalThis.off !== "function") {
|
|
820
|
+
globalThis.off = () => {};
|
|
821
|
+
}
|
|
822
|
+
await loadQtiComponents();
|
|
823
|
+
loadItem();
|
|
824
|
+
};
|
|
825
|
+
</script>
|
|
826
|
+
|
|
827
|
+
<!--
|
|
828
|
+
<h3 style="margin-top: 12rem">checklist</h3>
|
|
829
|
+
<ul style="margin-bottom: 10rem">
|
|
830
|
+
<li>Is the css scoped to the item?</li>
|
|
831
|
+
<li>Is the css/item somewhat responsive on smaller screens</li>
|
|
832
|
+
<li>Can you reset the item to its initial state</li>
|
|
833
|
+
<li>Can it restore its state ( reload )</li>
|
|
834
|
+
<li>Can you change the config</li>
|
|
835
|
+
<li>Colorblind</li>
|
|
836
|
+
<li>Touch/Mouse friendly</li>
|
|
837
|
+
<li>Works in older browser like IE11</li>
|
|
838
|
+
<li>Destroys itself correctly, removing event listeners</li>
|
|
839
|
+
<li>No objects or variables on global scope</li>
|
|
840
|
+
<li>Triggers correctly on changed config properties</li>
|
|
841
|
+
<li>Implements on off() methods of tao?</li>
|
|
842
|
+
<li>Response type and response correct</li>
|
|
843
|
+
<li>
|
|
844
|
+
getResponse() should give undefined when user has not done anything
|
|
845
|
+
</li>
|
|
846
|
+
<li>
|
|
847
|
+
For TAO to enter response is the entered comparable by the response
|
|
848
|
+
given
|
|
849
|
+
</li>
|
|
850
|
+
</ul>
|
|
851
|
+
-->
|
|
852
|
+
</body>
|
|
853
|
+
</html>
|