@longsightgroup/qti3-player 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/icons.d.ts +8 -0
  2. package/dist/icons.d.ts.map +1 -0
  3. package/dist/icons.js +45 -0
  4. package/dist/icons.js.map +1 -0
  5. package/dist/index.d.ts +3 -134
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -4712
  8. package/dist/index.js.map +1 -1
  9. package/dist/interaction-support.d.ts +34 -0
  10. package/dist/interaction-support.d.ts.map +1 -0
  11. package/dist/interaction-support.js +189 -0
  12. package/dist/interaction-support.js.map +1 -0
  13. package/dist/movement.d.ts +3 -0
  14. package/dist/movement.d.ts.map +1 -0
  15. package/dist/movement.js +21 -0
  16. package/dist/movement.js.map +1 -0
  17. package/dist/player-element.d.ts +60 -0
  18. package/dist/player-element.d.ts.map +1 -0
  19. package/dist/player-element.js +367 -0
  20. package/dist/player-element.js.map +1 -0
  21. package/dist/player-locale.d.ts +6 -0
  22. package/dist/player-locale.d.ts.map +1 -0
  23. package/dist/player-locale.js +205 -0
  24. package/dist/player-locale.js.map +1 -0
  25. package/dist/player-messages.d.ts +40 -0
  26. package/dist/player-messages.d.ts.map +1 -0
  27. package/dist/player-messages.js +2 -0
  28. package/dist/player-messages.js.map +1 -0
  29. package/dist/player-styles.d.ts +3 -0
  30. package/dist/player-styles.d.ts.map +1 -0
  31. package/dist/player-styles.js +24 -0
  32. package/dist/player-styles.js.map +1 -0
  33. package/dist/player-types.d.ts +71 -0
  34. package/dist/player-types.d.ts.map +1 -0
  35. package/dist/player-types.js +2 -0
  36. package/dist/player-types.js.map +1 -0
  37. package/dist/player-validation-dom.d.ts +3 -0
  38. package/dist/player-validation-dom.d.ts.map +1 -0
  39. package/dist/player-validation-dom.js +28 -0
  40. package/dist/player-validation-dom.js.map +1 -0
  41. package/dist/player-validation.d.ts +13 -0
  42. package/dist/player-validation.d.ts.map +1 -0
  43. package/dist/player-validation.js +123 -0
  44. package/dist/player-validation.js.map +1 -0
  45. package/dist/portable-custom-support.d.ts +11 -0
  46. package/dist/portable-custom-support.d.ts.map +1 -0
  47. package/dist/portable-custom-support.js +70 -0
  48. package/dist/portable-custom-support.js.map +1 -0
  49. package/dist/response-limits.d.ts +9 -0
  50. package/dist/response-limits.d.ts.map +1 -0
  51. package/dist/response-limits.js +44 -0
  52. package/dist/response-limits.js.map +1 -0
  53. package/package.json +4 -4
  54. package/src/content/content-dom.ts +274 -0
  55. package/src/content/content-renderer.ts +114 -0
  56. package/src/controls/remove-button.ts +13 -0
  57. package/src/icons.ts +47 -0
  58. package/src/index.ts +26 -5307
  59. package/src/interaction-support.ts +263 -0
  60. package/src/interactions/choice-interaction.ts +92 -0
  61. package/src/interactions/drawing-interaction.ts +447 -0
  62. package/src/interactions/end-attempt-interaction.ts +19 -0
  63. package/src/interactions/gap-match-interaction.ts +337 -0
  64. package/src/interactions/graphic-associate-interaction.ts +324 -0
  65. package/src/interactions/graphic-context.ts +33 -0
  66. package/src/interactions/hotspot-interaction.ts +87 -0
  67. package/src/interactions/hottext-interaction.ts +81 -0
  68. package/src/interactions/inline-choice-interaction.ts +45 -0
  69. package/src/interactions/inline-controls.ts +21 -0
  70. package/src/interactions/interaction-diagnostics.ts +159 -0
  71. package/src/interactions/interaction-dispatch.ts +9 -0
  72. package/src/interactions/interaction-label.ts +10 -0
  73. package/src/interactions/interaction-registry.ts +209 -0
  74. package/src/interactions/match-interaction.ts +199 -0
  75. package/src/interactions/object-asset.ts +212 -0
  76. package/src/interactions/pair-interaction.ts +147 -0
  77. package/src/interactions/point-value.ts +41 -0
  78. package/src/interactions/portable-custom-interaction.ts +139 -0
  79. package/src/interactions/position-object-interaction.ts +210 -0
  80. package/src/interactions/routing.ts +27 -0
  81. package/src/interactions/select-point-interaction.ts +185 -0
  82. package/src/interactions/shared.ts +56 -0
  83. package/src/interactions/text-interaction.ts +127 -0
  84. package/src/interactions/unsupported-interaction.ts +25 -0
  85. package/src/interactions/upload-interaction.ts +16 -0
  86. package/src/movement.ts +29 -0
  87. package/src/player/attempt-availability.ts +36 -0
  88. package/src/player/content-state.ts +63 -0
  89. package/src/player/dynamic-body.ts +40 -0
  90. package/src/player/feedback-panel.ts +23 -0
  91. package/src/player/fetch-xml.ts +8 -0
  92. package/src/player/interaction-render.ts +89 -0
  93. package/src/player/render-shell.ts +44 -0
  94. package/src/player/resolve-assets.ts +12 -0
  95. package/src/player/validation-messages.ts +42 -0
  96. package/src/player-element.ts +493 -0
  97. package/src/player-locale.ts +232 -0
  98. package/src/player-messages.ts +31 -0
  99. package/src/player-styles.ts +25 -0
  100. package/src/player-types.ts +99 -0
  101. package/src/player-validation-dom.ts +31 -0
  102. package/src/player-validation.ts +158 -0
  103. package/src/portable-custom-support.ts +74 -0
  104. package/src/reorder/a11y.ts +40 -0
  105. package/src/reorder/graphic-order-interaction.ts +260 -0
  106. package/src/reorder/list-controls.ts +114 -0
  107. package/src/reorder/order-interaction.ts +75 -0
  108. package/src/response-limits.ts +47 -0
  109. package/src/styles/base-styles.ts +117 -0
  110. package/src/styles/choice-hottext-styles.ts +75 -0
  111. package/src/styles/control-styles.ts +113 -0
  112. package/src/styles/drawing-styles.ts +29 -0
  113. package/src/styles/gap-match-styles.ts +32 -0
  114. package/src/styles/graphic-styles.ts +294 -0
  115. package/src/styles/match-pair-styles.ts +61 -0
  116. package/src/styles/text-slider-styles.ts +34 -0
@@ -0,0 +1,493 @@
1
+ import {
2
+ assertQtiAttemptStateV1,
3
+ createItemSession,
4
+ createCatalogSupportResolution,
5
+ createTextToSpeechTraversal,
6
+ parseQtiXml,
7
+ type QtiAttemptStateV1,
8
+ type QtiDiagnostic,
9
+ type QtiDocument,
10
+ type QtiInteraction,
11
+ type QtiItemSession,
12
+ type QtiPortableCustomStateValue,
13
+ type QtiScoreResult,
14
+ type QtiCatalogSupportResolution,
15
+ type QtiCatalogSupportResolutionOptions,
16
+ type QtiTextToSpeechTraversal,
17
+ type QtiValue,
18
+ } from "@longsightgroup/qti3-core";
19
+ import { renderContentNodes, type PlayerContentContext } from "./content/content-renderer.js";
20
+ import { contentNodeText } from "./content/content-dom.js";
21
+ import {
22
+ portableCustomValidityDiagnostic,
23
+ renderPortableCustomResponse,
24
+ } from "./interactions/portable-custom-interaction.js";
25
+ import {
26
+ collectEmbeddedInteractionDiagnostics,
27
+ collectInteractionRenderDiagnostics,
28
+ } from "./interactions/interaction-diagnostics.js";
29
+ import { defaultPlayerLocale, normalizedLocale, resolvePlayerMessages } from "./player-locale.js";
30
+ import { syncAttemptAvailability } from "./player/attempt-availability.js";
31
+ import {
32
+ currentTemplateValue,
33
+ currentVariableValue,
34
+ isFeedbackVisible,
35
+ isTemplateContentVisible,
36
+ mathTemplateValue,
37
+ } from "./player/content-state.js";
38
+ import { syncDynamicBodyState } from "./player/dynamic-body.js";
39
+ import { defaultFetchXml } from "./player/fetch-xml.js";
40
+ import { syncFeedbackPanel } from "./player/feedback-panel.js";
41
+ import {
42
+ renderBlockInteractionSection,
43
+ renderEmbeddedInteractionSection,
44
+ } from "./player/interaction-render.js";
45
+ import { renderPlayerShell } from "./player/render-shell.js";
46
+ import { resolveRenderedAssets } from "./player/resolve-assets.js";
47
+ import type {
48
+ QtiAssessmentItemPlayerEventDetailMap,
49
+ QtiAssessmentItemPlayerEventName,
50
+ QtiPlayerLoadOptions,
51
+ QtiPlayerMessageOverrides,
52
+ QtiPlayerResolveAsset,
53
+ QtiPlayerSessionControl,
54
+ QtiScoreAttemptOptions,
55
+ } from "./player-types.js";
56
+ import { cloneDiagnostics, errorView, validateItemResponses } from "./player-validation.js";
57
+ import { syncValidationMessages } from "./player-validation-dom.js";
58
+ import {
59
+ mergeVisibleValidationMessages,
60
+ responseValidationMessages,
61
+ } from "./player/validation-messages.js";
62
+
63
+ const HTMLElementBase: typeof HTMLElement =
64
+ globalThis.HTMLElement ??
65
+ (class {
66
+ replaceChildren(): void {}
67
+ dispatchEvent(): boolean {
68
+ return true;
69
+ }
70
+ } as unknown as typeof HTMLElement);
71
+
72
+ export class QtiAssessmentItemPlayer extends HTMLElementBase {
73
+ static get observedAttributes(): string[] {
74
+ return ["language-of-interface", "locale"];
75
+ }
76
+
77
+ private documentModel?: QtiDocument;
78
+ private session?: QtiItemSession;
79
+ private resolveAsset: QtiPlayerResolveAsset | undefined;
80
+ private validationMessages: QtiDiagnostic[] = [];
81
+ private authoringDiagnostics: QtiDiagnostic[] = [];
82
+ private languageOfInterfaceOverride: string | undefined;
83
+ private messageOverrides: QtiPlayerMessageOverrides = {};
84
+ private sessionControl: Required<QtiPlayerSessionControl> = {
85
+ validateResponses: true,
86
+ showFeedback: true,
87
+ };
88
+
89
+ get languageOfInterface(): string {
90
+ return (
91
+ this.languageOfInterfaceOverride ??
92
+ this.getAttribute?.("language-of-interface") ??
93
+ this.getAttribute?.("locale") ??
94
+ defaultPlayerLocale(this)
95
+ );
96
+ }
97
+
98
+ set languageOfInterface(value: string | undefined) {
99
+ this.languageOfInterfaceOverride = normalizedLocale(value);
100
+ this.rerenderIfLoaded();
101
+ }
102
+
103
+ get locale(): string {
104
+ return this.languageOfInterface;
105
+ }
106
+
107
+ set locale(value: string | undefined) {
108
+ this.languageOfInterface = value;
109
+ }
110
+
111
+ get messages(): QtiPlayerMessageOverrides {
112
+ return this.messageOverrides;
113
+ }
114
+
115
+ set messages(value: QtiPlayerMessageOverrides | undefined) {
116
+ this.messageOverrides = value ?? {};
117
+ this.rerenderIfLoaded();
118
+ }
119
+
120
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
121
+ if ((name !== "language-of-interface" && name !== "locale") || oldValue === newValue) {
122
+ return;
123
+ }
124
+ this.rerenderIfLoaded();
125
+ }
126
+
127
+ async loadXml(xml: string, options: QtiPlayerLoadOptions = {}): Promise<void> {
128
+ this.sessionControl = {
129
+ validateResponses: options.sessionControl?.validateResponses ?? true,
130
+ showFeedback: options.sessionControl?.showFeedback ?? true,
131
+ };
132
+ this.resolveAsset = options.resolveAsset;
133
+ const result = parseQtiXml(xml);
134
+ const playerDiagnostics = result.document
135
+ ? [
136
+ ...collectInteractionRenderDiagnostics(result.document.item.interactions),
137
+ ...collectEmbeddedInteractionDiagnostics(result.document.item),
138
+ ]
139
+ : [];
140
+ this.dispatchEvent(
141
+ new CustomEvent("qti-diagnostics", {
142
+ detail: { diagnostics: [...result.diagnostics, ...playerDiagnostics] },
143
+ }),
144
+ );
145
+ this.authoringDiagnostics = cloneDiagnostics(
146
+ playerDiagnostics.filter((diagnostic) => diagnostic.severity === "error"),
147
+ );
148
+ if (!result.document) {
149
+ this.replaceChildren(errorView("Unable to parse QTI item."));
150
+ return;
151
+ }
152
+
153
+ this.documentModel = result.document;
154
+ this.session = createItemSession(result.document, options.state);
155
+ this.validationMessages = cloneDiagnostics(
156
+ responseValidationMessages(options.state?.validationMessages ?? []),
157
+ );
158
+ if (options.status) this.session.setStatus(options.status);
159
+ this.render();
160
+ this.renderValidationMessages();
161
+ this.updateAttemptAvailability();
162
+ this.dispatchPlayerEvent("qti-ready", { item: result.document.item });
163
+ this.emitStateChange();
164
+ }
165
+
166
+ async loadUrl(url: string, options: QtiPlayerLoadOptions = {}): Promise<void> {
167
+ const fetchXml = options.fetchXml ?? defaultFetchXml;
168
+ await this.loadXml(await fetchXml(url), options);
169
+ }
170
+
171
+ scoreAttempt(options: QtiScoreAttemptOptions = {}): QtiScoreResult | undefined {
172
+ const session = this.session;
173
+ if (!session) return undefined;
174
+ const shouldValidateResponses =
175
+ options.validateResponses ?? this.sessionControl.validateResponses;
176
+ const responseValidation = shouldValidateResponses ? this.validateResponses() : [];
177
+ const validationMessages = [...this.authoringDiagnostics, ...responseValidation];
178
+ if (validationMessages.length > 0) {
179
+ this.validationMessages = cloneDiagnostics(responseValidation);
180
+ this.renderValidationMessages();
181
+ const state = this.serialize();
182
+ if (!state) return undefined;
183
+ this.dispatchPlayerEvent("qti-validation", {
184
+ validationMessages: cloneDiagnostics(validationMessages),
185
+ state,
186
+ });
187
+ this.emitStateChange(state);
188
+ return undefined;
189
+ }
190
+ this.validationMessages = [];
191
+ this.renderValidationMessages();
192
+ const result = session.score();
193
+ this.dispatchPlayerEvent("qti-score", result);
194
+ this.updateDynamicBodyState();
195
+ this.updateAttemptAvailability();
196
+ if (this.sessionControl.showFeedback) this.renderFeedback(result.outcomes);
197
+ this.emitStateChange(result.state);
198
+ return result;
199
+ }
200
+
201
+ reset(): void {
202
+ if (!this.documentModel) return;
203
+ this.session = createItemSession(this.documentModel);
204
+ this.validationMessages = [];
205
+ this.render();
206
+ this.updateAttemptAvailability();
207
+ this.dispatchEvent(new CustomEvent("qti-reset", { detail: { state: this.serialize() } }));
208
+ this.emitStateChange();
209
+ }
210
+
211
+ restore(state: QtiAttemptStateV1): void {
212
+ if (!this.documentModel) {
213
+ throw new Error("Cannot restore QTI state before loading an item.");
214
+ }
215
+ assertQtiAttemptStateV1(state);
216
+ if (state.itemIdentifier !== this.documentModel.item.identifier) {
217
+ throw new Error(
218
+ `Cannot restore state for ${state.itemIdentifier} into ${this.documentModel.item.identifier}.`,
219
+ );
220
+ }
221
+ this.session = createItemSession(this.documentModel, state);
222
+ this.validationMessages = cloneDiagnostics(
223
+ responseValidationMessages(state.validationMessages),
224
+ );
225
+ this.render();
226
+ this.renderValidationMessages();
227
+ this.updateAttemptAvailability();
228
+ this.dispatchEvent(new CustomEvent("qti-restore", { detail: { state: this.serialize() } }));
229
+ this.emitStateChange();
230
+ }
231
+
232
+ suspend(): void {
233
+ if (!this.session) return;
234
+ this.session.setStatus("suspended");
235
+ const state = this.serialize();
236
+ if (!state) return;
237
+ this.dispatchPlayerEvent("qti-suspend", { state });
238
+ this.emitStateChange(state);
239
+ }
240
+
241
+ endAttempt(options: QtiScoreAttemptOptions = {}): void {
242
+ const result = this.scoreAttempt(options);
243
+ if (!result) return;
244
+ if (
245
+ !this.documentModel?.item.adaptive ||
246
+ result.state.outcomes.completionStatus === "completed"
247
+ ) {
248
+ this.session?.setStatus("completed");
249
+ }
250
+ this.updateAttemptAvailability();
251
+ const state = this.serialize();
252
+ if (!state) return;
253
+ this.dispatchPlayerEvent("qti-endattempt", { state });
254
+ this.emitStateChange(state);
255
+ }
256
+
257
+ serialize(): QtiAttemptStateV1 | undefined {
258
+ const state = this.session?.serialize();
259
+ if (state) {
260
+ state.validationMessages = cloneDiagnostics(
261
+ mergeVisibleValidationMessages(this.authoringDiagnostics, this.validationMessages),
262
+ );
263
+ }
264
+ return state;
265
+ }
266
+
267
+ getTextToSpeechTraversal(): QtiTextToSpeechTraversal | undefined {
268
+ if (!this.documentModel) return undefined;
269
+ return createTextToSpeechTraversal(this.documentModel);
270
+ }
271
+
272
+ getCatalogSupportResolution(
273
+ options: QtiCatalogSupportResolutionOptions = {},
274
+ ): QtiCatalogSupportResolution | undefined {
275
+ if (!this.documentModel) return undefined;
276
+ return createCatalogSupportResolution(this.documentModel, options);
277
+ }
278
+
279
+ private emitStateChange(state = this.serialize()): void {
280
+ if (!state) return;
281
+ this.dispatchPlayerEvent("qti-statechange", { state });
282
+ }
283
+
284
+ private dispatchPlayerEvent<T extends QtiAssessmentItemPlayerEventName>(
285
+ type: T,
286
+ detail: QtiAssessmentItemPlayerEventDetailMap[T],
287
+ ): void {
288
+ this.dispatchEvent(new CustomEvent<QtiAssessmentItemPlayerEventDetailMap[T]>(type, { detail }));
289
+ }
290
+
291
+ private playerMessages() {
292
+ return resolvePlayerMessages(this.languageOfInterface, this.messageOverrides);
293
+ }
294
+
295
+ private rerenderIfLoaded(): void {
296
+ if (!this.documentModel) return;
297
+ this.render();
298
+ this.renderValidationMessages();
299
+ this.updateAttemptAvailability();
300
+ }
301
+
302
+ private render(): void {
303
+ const documentModel = this.documentModel;
304
+ if (!documentModel) return;
305
+
306
+ this.applyDefaultStyles();
307
+ const root = renderPlayerShell({
308
+ documentModel,
309
+ contentContext: this.contentContext(),
310
+ renderStandaloneInteraction: (interaction) => this.renderInteraction(interaction),
311
+ });
312
+ if (this.resolveAsset) resolveRenderedAssets(root, this.resolveAsset);
313
+ this.replaceChildren(root);
314
+ }
315
+
316
+ private renderInteraction(interaction: QtiInteraction): HTMLElement {
317
+ const responseIdentifier = interaction.responseIdentifier;
318
+ return renderBlockInteractionSection({
319
+ interaction,
320
+ messages: this.playerMessages(),
321
+ update: this.bindResponseUpdate(responseIdentifier),
322
+ currentValue: responseIdentifier ? this.currentResponseValue(responseIdentifier) : null,
323
+ isCompleted: () => this.attemptIsCompleted(),
324
+ endAttempt: () => this.endAttempt(),
325
+ renderPortableCustom: (portableInteraction, portableUpdate, portableValue) =>
326
+ this.renderPortableCustomResponse(portableInteraction, portableUpdate, portableValue),
327
+ });
328
+ }
329
+
330
+ private bindResponseUpdate(responseIdentifier: string | undefined): (value: QtiValue) => void {
331
+ return (value) => {
332
+ if (this.attemptIsCompleted()) return;
333
+ if (!responseIdentifier || !this.session) return;
334
+ this.session.respond(responseIdentifier, value);
335
+ this.clearValidationMessage(responseIdentifier);
336
+ this.dispatchPlayerEvent("qti-responsechange", { responseIdentifier, value });
337
+ this.emitStateChange();
338
+ };
339
+ }
340
+
341
+ private contentContext(): PlayerContentContext {
342
+ const sessionState = () => this.session?.serialize();
343
+ return {
344
+ interactionAt: (index) => this.documentModel?.item.interactions[index],
345
+ renderBlockInteraction: (interaction) => this.renderInteraction(interaction),
346
+ renderEmbeddedInteraction: (embeddedInteraction) => {
347
+ const responseIdentifier = embeddedInteraction.responseIdentifier;
348
+ return renderEmbeddedInteractionSection(
349
+ embeddedInteraction,
350
+ this.bindResponseUpdate(responseIdentifier),
351
+ responseIdentifier ? this.currentResponseValue(responseIdentifier) : null,
352
+ this.playerMessages(),
353
+ );
354
+ },
355
+ currentVariableValue: (identifier) => currentVariableValue(sessionState(), identifier),
356
+ mathTemplateValue: (node) => {
357
+ const identifier = contentNodeText(node).trim();
358
+ return mathTemplateValue(
359
+ node,
360
+ this.documentModel,
361
+ identifier ? currentTemplateValue(sessionState(), identifier) : null,
362
+ );
363
+ },
364
+ isFeedbackVisible: (node) =>
365
+ isFeedbackVisible(node, currentVariableValue(sessionState(), node.outcomeIdentifier)),
366
+ isTemplateContentVisible: (element) => {
367
+ const templateIdentifier = element.dataset.templateIdentifier;
368
+ return isTemplateContentVisible(
369
+ element,
370
+ templateIdentifier ? currentTemplateValue(sessionState(), templateIdentifier) : null,
371
+ );
372
+ },
373
+ };
374
+ }
375
+
376
+ private renderPortableCustomResponse(
377
+ interaction: QtiInteraction,
378
+ update: (value: QtiValue) => void,
379
+ currentValue: QtiValue,
380
+ ): HTMLElement {
381
+ const responseIdentifier = interaction.responseIdentifier;
382
+ return renderPortableCustomResponse({
383
+ interaction,
384
+ update,
385
+ currentValue,
386
+ currentState: responseIdentifier
387
+ ? this.currentInteractionState(responseIdentifier)
388
+ : undefined,
389
+ renderMarkup: (nodes) => renderContentNodes(nodes, this.contentContext()),
390
+ setInteractionState: (identifier, state) =>
391
+ this.session?.setInteractionState(identifier, state),
392
+ setValidity: (identifier, valid, message) =>
393
+ this.setPortableCustomValidity(identifier, valid, message),
394
+ emitStateChange: () => this.emitStateChange(),
395
+ onMount: (detail) => this.dispatchPlayerEvent("qti-portable-custom-mount", detail),
396
+ });
397
+ }
398
+
399
+ private updateDynamicBodyState(): void {
400
+ const sessionState = this.session?.serialize();
401
+ syncDynamicBodyState(this, {
402
+ variableValue: (identifier) => currentVariableValue(sessionState, identifier),
403
+ templateValue: (identifier) => currentTemplateValue(sessionState, identifier),
404
+ });
405
+ }
406
+
407
+ private updateAttemptAvailability(): void {
408
+ syncAttemptAvailability(this, {
409
+ completed: this.attemptIsCompleted(),
410
+ status: this.session?.serialize().status ?? "unloaded",
411
+ host: this,
412
+ });
413
+ }
414
+
415
+ private attemptIsCompleted(): boolean {
416
+ return this.session?.serialize().status === "completed";
417
+ }
418
+
419
+ private currentResponseValue(identifier: string): QtiValue {
420
+ return this.session?.serialize().responses[identifier] ?? null;
421
+ }
422
+
423
+ private currentInteractionState(identifier: string): QtiPortableCustomStateValue | undefined {
424
+ return this.session?.serialize().interactionStates?.[identifier];
425
+ }
426
+
427
+ private setPortableCustomValidity(
428
+ responseIdentifier: string,
429
+ valid: boolean,
430
+ message: string | undefined,
431
+ ): void {
432
+ const diagnostic = portableCustomValidityDiagnostic(responseIdentifier, valid, message);
433
+ if (!diagnostic) {
434
+ this.clearValidationMessage(responseIdentifier);
435
+ return;
436
+ }
437
+ this.validationMessages = [
438
+ ...this.validationMessages.filter((entry) => entry.path !== responseIdentifier),
439
+ diagnostic,
440
+ ];
441
+ this.renderValidationMessages();
442
+ }
443
+
444
+ private applyDefaultStyles(): void {
445
+ this.style.color = "CanvasText";
446
+ this.style.backgroundColor = "Canvas";
447
+ this.style.colorScheme = "light dark";
448
+ }
449
+
450
+ private validateResponses(): QtiDiagnostic[] {
451
+ const state = this.session?.serialize();
452
+ if (!state || !this.documentModel) return [];
453
+ return validateItemResponses(this.documentModel, state);
454
+ }
455
+
456
+ private visibleValidationMessages(): QtiDiagnostic[] {
457
+ return mergeVisibleValidationMessages(this.authoringDiagnostics, this.validationMessages);
458
+ }
459
+
460
+ private renderValidationMessages(): void {
461
+ syncValidationMessages(this, this.visibleValidationMessages());
462
+ }
463
+
464
+ private clearValidationMessage(responseIdentifier: string): void {
465
+ const before = this.validationMessages.length;
466
+ this.validationMessages = this.validationMessages.filter(
467
+ (message) => message.path !== responseIdentifier,
468
+ );
469
+ if (this.validationMessages.length !== before) this.renderValidationMessages();
470
+ }
471
+
472
+ private renderFeedback(outcomes: Record<string, QtiValue>): void {
473
+ const documentModel = this.documentModel;
474
+ if (!documentModel) return;
475
+ syncFeedbackPanel(
476
+ this.querySelector<HTMLElement>(".qti3-feedback"),
477
+ documentModel.item,
478
+ outcomes,
479
+ );
480
+ }
481
+ }
482
+
483
+ export function defineQtiAssessmentItemPlayer(): void {
484
+ if (globalThis.customElements && !customElements.get("qti-assessment-item-player")) {
485
+ customElements.define("qti-assessment-item-player", QtiAssessmentItemPlayer);
486
+ }
487
+ }
488
+
489
+ declare global {
490
+ interface HTMLElementTagNameMap {
491
+ "qti-assessment-item-player": QtiAssessmentItemPlayer;
492
+ }
493
+ }