@longsightgroup/qti3-player 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +153 -13
  2. package/dist/index.d.ts +13 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/player-adapter.d.ts +62 -0
  7. package/dist/player-adapter.d.ts.map +1 -0
  8. package/dist/player-adapter.js +119 -0
  9. package/dist/player-adapter.js.map +1 -0
  10. package/dist/player-dev.d.ts +4 -0
  11. package/dist/player-dev.d.ts.map +1 -0
  12. package/dist/player-dev.js +14 -0
  13. package/dist/player-dev.js.map +1 -0
  14. package/dist/player-element.d.ts +14 -1
  15. package/dist/player-element.d.ts.map +1 -1
  16. package/dist/player-element.js +57 -5
  17. package/dist/player-element.js.map +1 -1
  18. package/dist/player-locale.d.ts +8 -3
  19. package/dist/player-locale.d.ts.map +1 -1
  20. package/dist/player-locale.js +16 -175
  21. package/dist/player-locale.js.map +1 -1
  22. package/dist/player-message-catalog-default.d.ts +4 -0
  23. package/dist/player-message-catalog-default.d.ts.map +1 -0
  24. package/dist/player-message-catalog-default.js +118 -0
  25. package/dist/player-message-catalog-default.js.map +1 -0
  26. package/dist/player-message-catalog-validate.d.ts +31 -0
  27. package/dist/player-message-catalog-validate.d.ts.map +1 -0
  28. package/dist/player-message-catalog-validate.js +327 -0
  29. package/dist/player-message-catalog-validate.js.map +1 -0
  30. package/dist/player-message-catalog.d.ts +18 -0
  31. package/dist/player-message-catalog.d.ts.map +1 -0
  32. package/dist/player-message-catalog.js +40 -0
  33. package/dist/player-message-catalog.js.map +1 -0
  34. package/dist/player-message-keys.d.ts +6 -0
  35. package/dist/player-message-keys.d.ts.map +1 -0
  36. package/dist/player-message-keys.js +7 -0
  37. package/dist/player-message-keys.js.map +1 -0
  38. package/dist/player-message-manifest.d.ts +272 -0
  39. package/dist/player-message-manifest.d.ts.map +1 -0
  40. package/dist/player-message-manifest.js +83 -0
  41. package/dist/player-message-manifest.js.map +1 -0
  42. package/dist/player-message-overrides.d.ts +3 -0
  43. package/dist/player-message-overrides.d.ts.map +1 -0
  44. package/dist/player-message-overrides.js +28 -0
  45. package/dist/player-message-overrides.js.map +1 -0
  46. package/dist/player-message-resolver.d.ts +31 -0
  47. package/dist/player-message-resolver.d.ts.map +1 -0
  48. package/dist/player-message-resolver.js +110 -0
  49. package/dist/player-message-resolver.js.map +1 -0
  50. package/dist/player-messages.d.ts +0 -38
  51. package/dist/player-messages.d.ts.map +1 -1
  52. package/dist/player-types.d.ts +12 -2
  53. package/dist/player-types.d.ts.map +1 -1
  54. package/package.json +3 -3
  55. package/src/controls/remove-button.ts +8 -5
  56. package/src/index.ts +61 -5
  57. package/src/interactions/choice-interaction.ts +6 -2
  58. package/src/interactions/drawing-interaction.ts +14 -9
  59. package/src/interactions/end-attempt-interaction.ts +3 -3
  60. package/src/interactions/gap-match-interaction.ts +32 -13
  61. package/src/interactions/graphic-associate-interaction.ts +15 -10
  62. package/src/interactions/hotspot-interaction.ts +10 -6
  63. package/src/interactions/inline-choice-interaction.ts +4 -4
  64. package/src/interactions/interaction-registry.ts +12 -12
  65. package/src/interactions/match-interaction.ts +9 -6
  66. package/src/interactions/pair-interaction.ts +22 -14
  67. package/src/interactions/position-object-interaction.ts +22 -13
  68. package/src/interactions/select-point-interaction.ts +25 -13
  69. package/src/interactions/shared.ts +21 -4
  70. package/src/interactions/text-interaction.ts +14 -4
  71. package/src/interactions/upload-interaction.ts +6 -3
  72. package/src/player/content-state.ts +12 -1
  73. package/src/player/interaction-render.ts +4 -4
  74. package/src/player-adapter.ts +253 -0
  75. package/src/player-dev.ts +14 -0
  76. package/src/player-element.ts +78 -8
  77. package/src/player-locale.ts +28 -199
  78. package/src/player-message-catalog-default.ts +119 -0
  79. package/src/player-message-catalog-validate.ts +425 -0
  80. package/src/player-message-catalog.ts +72 -0
  81. package/src/player-message-keys.ts +12 -0
  82. package/src/player-message-manifest.ts +103 -0
  83. package/src/player-message-overrides.ts +38 -0
  84. package/src/player-message-resolver.ts +205 -0
  85. package/src/player-messages.ts +0 -30
  86. package/src/player-types.ts +15 -4
  87. package/src/reorder/a11y.ts +22 -7
  88. package/src/reorder/graphic-order-interaction.ts +23 -16
  89. package/src/reorder/list-controls.ts +8 -6
  90. package/src/reorder/order-interaction.ts +7 -5
  91. package/src/styles/base-styles.ts +20 -5
  92. package/src/styles/graphic-styles.ts +0 -6
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Single source of truth for player chrome message ids and resolver behavior.
3
+ * Default copy lives in `defaultPlayerMessageCatalog.strings`; hosts override via JSON catalogs.
4
+ */
5
+ export type PlayerMessageResolverKind =
6
+ | "plain"
7
+ | "template"
8
+ | "plural"
9
+ | "typeLabel"
10
+ | "typeTemplate"
11
+ | "directionTemplate"
12
+ | "extendedTextCounter";
13
+
14
+ export interface PlayerMessageManifestEntry {
15
+ readonly key: string;
16
+ readonly resolver: PlayerMessageResolverKind;
17
+ readonly params?: readonly string[];
18
+ }
19
+
20
+ export const PLAYER_MESSAGE_MANIFEST = [
21
+ { key: "remove", resolver: "plain" },
22
+ { key: "removePair", resolver: "template", params: ["label"] },
23
+ { key: "clearDrawing", resolver: "plain" },
24
+ { key: "clearPoints", resolver: "plain" },
25
+ { key: "endAttempt", resolver: "plain" },
26
+ { key: "uploadResponse", resolver: "plain" },
27
+ { key: "movableObject", resolver: "plain" },
28
+ { key: "placeObject", resolver: "plain" },
29
+ { key: "inlineChoicePrompt", resolver: "plain" },
30
+ { key: "extendedTextCounter", resolver: "extendedTextCounter", params: ["characters", "words"] },
31
+ { key: "hotspotSelectionSummary", resolver: "plural", params: ["selection", "count"] },
32
+ { key: "noPointSelected", resolver: "plain" },
33
+ { key: "noRegionSelected", resolver: "plain" },
34
+ { key: "noAssociationsMade", resolver: "plain" },
35
+ { key: "associationsMade", resolver: "plural", params: ["count"] },
36
+ { key: "associationPairLabel", resolver: "template", params: ["source", "target"] },
37
+ { key: "hotspotSelectedChooseAnother", resolver: "template", params: ["label"] },
38
+ { key: "moveChoice", resolver: "directionTemplate", params: ["label", "direction"] },
39
+ { key: "movePoint", resolver: "directionTemplate", params: ["direction"] },
40
+ { key: "moveObject", resolver: "directionTemplate", params: ["direction"] },
41
+ { key: "interactionType", resolver: "typeLabel", params: ["type"] },
42
+ { key: "matchSourcesBank", resolver: "plain" },
43
+ { key: "matchTargetsBank", resolver: "plain" },
44
+ { key: "matchSelectedPairsList", resolver: "plain" },
45
+ { key: "interactionSourcesBank", resolver: "typeTemplate", params: ["type"] },
46
+ { key: "interactionTargetsBank", resolver: "typeTemplate", params: ["type"] },
47
+ { key: "interactionSelectedPairsList", resolver: "typeTemplate", params: ["type"] },
48
+ { key: "associateFirstConceptRegion", resolver: "plain" },
49
+ { key: "associatePairWithRegion", resolver: "plain" },
50
+ { key: "matchPromptRegion", resolver: "plain" },
51
+ { key: "matchMatchRegion", resolver: "plain" },
52
+ { key: "genericSourceRegion", resolver: "plain" },
53
+ { key: "genericTargetRegion", resolver: "plain" },
54
+ { key: "interactionHotspots", resolver: "typeTemplate", params: ["type"] },
55
+ { key: "interactionImageAlt", resolver: "typeTemplate", params: ["type"] },
56
+ { key: "interactionCurrentOrderList", resolver: "typeTemplate", params: ["type"] },
57
+ { key: "interactionSelectedOrderList", resolver: "typeTemplate", params: ["type"] },
58
+ { key: "interactionChoicesBank", resolver: "typeTemplate", params: ["type"] },
59
+ { key: "interactionGapTargets", resolver: "typeTemplate", params: ["type"] },
60
+ { key: "interactionTargetImage", resolver: "typeTemplate", params: ["type"] },
61
+ { key: "interactionOptionsList", resolver: "typeTemplate", params: ["type"] },
62
+ { key: "interactionCoordinateResponse", resolver: "typeTemplate", params: ["type"] },
63
+ { key: "interactionCoordinateArea", resolver: "typeTemplate", params: ["type"] },
64
+ {
65
+ key: "interactionCoordinateAreaSelected",
66
+ resolver: "typeTemplate",
67
+ params: ["type", "coordinates"],
68
+ },
69
+ { key: "interactionPlacementResponse", resolver: "typeTemplate", params: ["type"] },
70
+ { key: "interactionPlacementStage", resolver: "typeTemplate", params: ["type"] },
71
+ { key: "interactionPlacementStageEmpty", resolver: "typeTemplate", params: ["type"] },
72
+ { key: "interactionPlacementStageAt", resolver: "typeTemplate", params: ["type", "coordinates"] },
73
+ { key: "interactionDrawingResponse", resolver: "typeTemplate", params: ["type"] },
74
+ { key: "drawingSurface", resolver: "plain" },
75
+ { key: "drawingSurfaceEmpty", resolver: "plain" },
76
+ { key: "drawingSurfaceStrokeCount", resolver: "plural", params: ["count"] },
77
+ { key: "drawingStatusEmpty", resolver: "plain" },
78
+ { key: "drawingStatusStrokeCount", resolver: "plural", params: ["count"] },
79
+ { key: "gapLabel", resolver: "template", params: ["index"] },
80
+ { key: "graphicGapTargetLabel", resolver: "template", params: ["index"] },
81
+ { key: "gapEmptyState", resolver: "template", params: ["label"] },
82
+ { key: "gapAssignedState", resolver: "template", params: ["label", "assigned"] },
83
+ { key: "gapLabelsPlacedCount", resolver: "plural", params: ["count"] },
84
+ { key: "gapNoLabelsPlaced", resolver: "plain" },
85
+ { key: "orderedItemAtPosition", resolver: "template", params: ["label", "position", "total"] },
86
+ { key: "orderedItemMovedOneStep", resolver: "directionTemplate", params: ["label", "direction"] },
87
+ {
88
+ key: "orderedItemMovedToPosition",
89
+ resolver: "template",
90
+ params: ["label", "position", "total"],
91
+ },
92
+ { key: "graphicOrderRegionsSelected", resolver: "plural", params: ["count"] },
93
+ { key: "graphicOrderNoRegionsSelected", resolver: "plain" },
94
+ { key: "objectNotPlaced", resolver: "plain" },
95
+ { key: "objectPositionedAt", resolver: "template", params: ["coordinates"] },
96
+ { key: "selectedPointAt", resolver: "template", params: ["coordinates"] },
97
+ { key: "selectedPointsSummary", resolver: "plural", params: ["count", "coordinates"] },
98
+ { key: "extendedTextResponseLabel", resolver: "plain" },
99
+ { key: "textResponseLabel", resolver: "plain" },
100
+ { key: "sliderResponseLabel", resolver: "plain" },
101
+ ] as const satisfies readonly PlayerMessageManifestEntry[];
102
+
103
+ export type PlayerMessageKey = (typeof PLAYER_MESSAGE_MANIFEST)[number]["key"];
@@ -0,0 +1,38 @@
1
+ import type { PlayerMessageKey } from "./player-message-manifest.js";
2
+ import type {
3
+ PlayerMessageOverride,
4
+ PlayerMessageParams,
5
+ PlayerMessageResolver,
6
+ QtiPlayerMessageOverrides,
7
+ } from "./player-message-resolver.js";
8
+
9
+ class OverridePlayerMessageResolver implements PlayerMessageResolver {
10
+ constructor(
11
+ private readonly base: PlayerMessageResolver,
12
+ private readonly overrides: QtiPlayerMessageOverrides,
13
+ ) {}
14
+
15
+ message<K extends PlayerMessageKey>(key: K, params?: PlayerMessageParams<K>): string {
16
+ const override = this.overrides[key] as PlayerMessageOverride<K> | undefined;
17
+ if (override) {
18
+ if (params === undefined) {
19
+ return (override as () => string)();
20
+ }
21
+ return (override as (p: PlayerMessageParams<K>) => string)(params);
22
+ }
23
+ if (params === undefined) {
24
+ return (this.base.message as (messageKey: K) => string)(key);
25
+ }
26
+ return (this.base.message as (messageKey: K, p: PlayerMessageParams<K>) => string)(key, params);
27
+ }
28
+ }
29
+
30
+ export function applyPlayerMessageOverrides(
31
+ base: PlayerMessageResolver,
32
+ overrides: QtiPlayerMessageOverrides,
33
+ ): PlayerMessageResolver {
34
+ if (Object.keys(overrides).length === 0) {
35
+ return base;
36
+ }
37
+ return new OverridePlayerMessageResolver(base, overrides);
38
+ }
@@ -0,0 +1,205 @@
1
+ import { defaultPlayerMessageCatalog } from "./player-message-catalog-default.js";
2
+ import {
3
+ formatPlayerMessage,
4
+ mergePlayerMessageCatalogs,
5
+ type PlayerMessageCatalog,
6
+ } from "./player-message-catalog.js";
7
+ import { warnPlayerMessageOnce } from "./player-dev.js";
8
+ import {
9
+ PLAYER_MESSAGE_MANIFEST,
10
+ type PlayerMessageKey,
11
+ type PlayerMessageManifestEntry,
12
+ type PlayerMessageResolverKind,
13
+ } from "./player-message-manifest.js";
14
+ import type { QtiPlayerMovementDirection } from "./player-messages.js";
15
+
16
+ type ManifestEntry = (typeof PLAYER_MESSAGE_MANIFEST)[number];
17
+
18
+ const MANIFEST_BY_KEY = new Map(PLAYER_MESSAGE_MANIFEST.map((entry) => [entry.key, entry]));
19
+
20
+ type NumericParamName = "count" | "characters" | "words" | "index" | "position" | "total";
21
+
22
+ type ParamValue<Name extends string> = Name extends NumericParamName
23
+ ? number
24
+ : Name extends "direction"
25
+ ? QtiPlayerMovementDirection
26
+ : string;
27
+
28
+ type ParamsFromManifestEntry<E extends PlayerMessageManifestEntry> =
29
+ E["params"] extends readonly string[]
30
+ ? { [K in E["params"][number]]: ParamValue<K> }
31
+ : Record<string, never>;
32
+
33
+ export type { PlayerMessageKey } from "./player-message-manifest.js";
34
+
35
+ export type PlayerMessageParams<K extends PlayerMessageKey> = ParamsFromManifestEntry<
36
+ Extract<ManifestEntry, { key: K }>
37
+ >;
38
+
39
+ type MessageParamsArg<K extends PlayerMessageKey> =
40
+ PlayerMessageParams<K> extends Record<string, never> ? never : PlayerMessageParams<K>;
41
+
42
+ /** Typed override handler for a single manifest message id. */
43
+ export type PlayerMessageOverride<K extends PlayerMessageKey> =
44
+ MessageParamsArg<K> extends never ? () => string : (params: PlayerMessageParams<K>) => string;
45
+
46
+ /** Per-message function overrides keyed by manifest id (params match {@link PlayerMessageParams}). */
47
+ export type QtiPlayerMessageOverrides = {
48
+ [K in PlayerMessageKey]?: PlayerMessageOverride<K>;
49
+ };
50
+
51
+ /** Key-driven player chrome messages (canonical runtime API). */
52
+ export interface PlayerMessageResolver {
53
+ message<K extends PlayerMessageKey>(
54
+ key: K,
55
+ ...args: MessageParamsArg<K> extends never ? [] : [MessageParamsArg<K>]
56
+ ): string;
57
+ }
58
+
59
+ function readableTypeFallback(type: string): string {
60
+ return type
61
+ .replace(/[A-Z]/g, (letter) => ` ${letter.toLowerCase()}`)
62
+ .replace(/^./, (letter) => letter.toUpperCase());
63
+ }
64
+
65
+ function mergedCatalog(catalog: PlayerMessageCatalog): PlayerMessageCatalog {
66
+ return mergePlayerMessageCatalogs(defaultPlayerMessageCatalog, catalog);
67
+ }
68
+
69
+ function resolveCatalogTemplate(
70
+ strings: Record<string, string>,
71
+ key: string,
72
+ count?: number,
73
+ ): string | undefined {
74
+ if (count !== undefined) {
75
+ const pluralKey = count === 1 ? `${key}.one` : `${key}.other`;
76
+ const pluralTemplate = strings[pluralKey] ?? defaultPlayerMessageCatalog.strings[pluralKey];
77
+ if (pluralTemplate !== undefined) {
78
+ return pluralTemplate;
79
+ }
80
+ }
81
+ return strings[key] ?? defaultPlayerMessageCatalog.strings[key];
82
+ }
83
+
84
+ function catalogString(
85
+ strings: Record<string, string>,
86
+ key: string,
87
+ values: Record<string, string | number> = {},
88
+ count?: number,
89
+ ): string {
90
+ const template = resolveCatalogTemplate(strings, key, count);
91
+ if (template === undefined) {
92
+ warnPlayerMessageOnce(
93
+ `missing-message:${key}`,
94
+ `Missing player message catalog key "${key}"; showing the key as UI text.`,
95
+ );
96
+ return key;
97
+ }
98
+ return formatPlayerMessage(template, values);
99
+ }
100
+
101
+ function resolveUnit(
102
+ strings: Record<string, string>,
103
+ prefix: "character" | "word",
104
+ count: number,
105
+ ): string {
106
+ const one = strings[`${prefix}Unit.one`];
107
+ const other = strings[`${prefix}Unit.other`];
108
+ if (one && other) {
109
+ return count === 1 ? one : other;
110
+ }
111
+ return prefix;
112
+ }
113
+
114
+ type ResolverContext = {
115
+ strings: Record<string, string>;
116
+ typeName: (type: string) => string;
117
+ directionLabel: (direction: QtiPlayerMovementDirection) => string;
118
+ };
119
+
120
+ function resolveManifestEntry(
121
+ entry: PlayerMessageManifestEntry,
122
+ context: ResolverContext,
123
+ params: Record<string, string | number> = {},
124
+ ): string {
125
+ const { strings, typeName, directionLabel } = context;
126
+ const { key, resolver } = entry;
127
+
128
+ switch (resolver as PlayerMessageResolverKind) {
129
+ case "plain":
130
+ return catalogString(strings, key);
131
+ case "typeLabel":
132
+ return typeName(String(params.type ?? ""));
133
+ case "template":
134
+ return catalogString(strings, key, params);
135
+ case "plural": {
136
+ const count = Number(params.count);
137
+ return catalogString(strings, key, params, count);
138
+ }
139
+ case "typeTemplate":
140
+ return catalogString(strings, key, {
141
+ ...params,
142
+ typeName: typeName(String(params.type ?? "")),
143
+ });
144
+ case "directionTemplate":
145
+ return catalogString(strings, key, {
146
+ ...params,
147
+ direction: directionLabel(params.direction as QtiPlayerMovementDirection),
148
+ });
149
+ case "extendedTextCounter": {
150
+ const characters = Number(params.characters);
151
+ const words = Number(params.words);
152
+ const template =
153
+ resolveCatalogTemplate(strings, key) ??
154
+ defaultPlayerMessageCatalog.strings.extendedTextCounter ??
155
+ key;
156
+ return formatPlayerMessage(template, {
157
+ characters,
158
+ words,
159
+ characterUnit: resolveUnit(strings, "character", characters),
160
+ wordUnit: resolveUnit(strings, "word", words),
161
+ });
162
+ }
163
+ default:
164
+ return key;
165
+ }
166
+ }
167
+
168
+ class CatalogPlayerMessageResolver implements PlayerMessageResolver {
169
+ private readonly context: ResolverContext;
170
+
171
+ constructor(catalog: PlayerMessageCatalog) {
172
+ const merged = mergedCatalog(catalog);
173
+ const types = merged.interactionTypes ?? {};
174
+ const directions = merged.directions ?? defaultPlayerMessageCatalog.directions ?? {};
175
+ this.context = {
176
+ strings: merged.strings,
177
+ typeName: (type) => types[type] ?? readableTypeFallback(type),
178
+ directionLabel: (direction) => directions[direction] ?? direction,
179
+ };
180
+ }
181
+
182
+ message<K extends PlayerMessageKey>(
183
+ key: K,
184
+ ...args: MessageParamsArg<K> extends never ? [] : [MessageParamsArg<K>]
185
+ ): string {
186
+ const entry = MANIFEST_BY_KEY.get(key);
187
+ if (!entry) {
188
+ return key;
189
+ }
190
+ const params = ((args as readonly unknown[])[0] ?? {}) as Record<string, string | number>;
191
+ return resolveManifestEntry(entry, this.context, params);
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Resolves player chrome from a host catalog using {@link PLAYER_MESSAGE_MANIFEST}.
197
+ * Missing string keys fall back to English defaults.
198
+ */
199
+ export function createPlayerMessageResolver(catalog: PlayerMessageCatalog): PlayerMessageResolver {
200
+ return new CatalogPlayerMessageResolver(catalog);
201
+ }
202
+
203
+ export const defaultPlayerMessageResolver: PlayerMessageResolver = createPlayerMessageResolver(
204
+ defaultPlayerMessageCatalog,
205
+ );
@@ -1,31 +1 @@
1
- export interface QtiPlayerRemoveMessageParams {
2
- label: string;
3
- }
4
-
5
- export interface QtiPlayerAssociationPairLabelParams {
6
- source: string;
7
- target: string;
8
- }
9
-
10
1
  export type QtiPlayerMovementDirection = "up" | "down" | "left" | "right";
11
-
12
- export interface QtiPlayerMessages {
13
- remove: () => string;
14
- removePair: (params: QtiPlayerRemoveMessageParams) => string;
15
- clearDrawing: () => string;
16
- clearPoints: () => string;
17
- endAttempt: () => string;
18
- uploadResponse: () => string;
19
- movableObject: () => string;
20
- placeObject: () => string;
21
- inlineChoicePrompt: () => string;
22
- noPointSelected: () => string;
23
- noRegionSelected: () => string;
24
- noAssociationsMade: () => string;
25
- associationsMade: (params: { count: number }) => string;
26
- associationPairLabel: (params: QtiPlayerAssociationPairLabelParams) => string;
27
- hotspotSelectedChooseAnother: (params: { label: string }) => string;
28
- moveChoice: (params: { label: string; direction: QtiPlayerMovementDirection }) => string;
29
- movePoint: (params: { direction: QtiPlayerMovementDirection }) => string;
30
- moveObject: (params: { direction: QtiPlayerMovementDirection }) => string;
31
- }
@@ -9,8 +9,6 @@ import type {
9
9
  QtiScoreResult,
10
10
  QtiValue,
11
11
  } from "@longsightgroup/qti3-core";
12
- import type { QtiPlayerMessages } from "./player-messages.js";
13
-
14
12
  export interface QtiPlayerSessionControl {
15
13
  validateResponses?: boolean | undefined;
16
14
  showFeedback?: boolean | undefined;
@@ -31,8 +29,6 @@ export interface QtiPlayerLoadOptions {
31
29
  resolveAsset?: QtiPlayerResolveAsset | undefined;
32
30
  }
33
31
 
34
- export type QtiPlayerMessageOverrides = Partial<QtiPlayerMessages>;
35
-
36
32
  export interface QtiReadyEventDetail {
37
33
  item: QtiAssessmentItem;
38
34
  }
@@ -77,6 +73,18 @@ export interface QtiEndAttemptEventDetail {
77
73
  state: QtiAttemptStateV1;
78
74
  }
79
75
 
76
+ export interface QtiDiagnosticsEventDetail {
77
+ diagnostics: QtiDiagnostic[];
78
+ }
79
+
80
+ export interface QtiResetEventDetail {
81
+ state?: QtiAttemptStateV1 | undefined;
82
+ }
83
+
84
+ export interface QtiRestoreEventDetail {
85
+ state?: QtiAttemptStateV1 | undefined;
86
+ }
87
+
80
88
  export interface QtiAssessmentItemPlayerEventDetailMap {
81
89
  "qti-ready": QtiReadyEventDetail;
82
90
  "qti-statechange": QtiStateChangeEventDetail;
@@ -86,6 +94,9 @@ export interface QtiAssessmentItemPlayerEventDetailMap {
86
94
  "qti-validation": QtiValidationEventDetail;
87
95
  "qti-suspend": QtiSuspendEventDetail;
88
96
  "qti-endattempt": QtiEndAttemptEventDetail;
97
+ "qti-diagnostics": QtiDiagnosticsEventDetail;
98
+ "qti-reset": QtiResetEventDetail;
99
+ "qti-restore": QtiRestoreEventDetail;
89
100
  }
90
101
 
91
102
  export type QtiAssessmentItemPlayerEventName = keyof QtiAssessmentItemPlayerEventDetailMap;
@@ -1,4 +1,5 @@
1
1
  import { choiceSelector } from "../interaction-support.js";
2
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
2
3
 
3
4
  export function createSelectionSummary(): HTMLParagraphElement {
4
5
  const summary = document.createElement("p");
@@ -7,32 +8,46 @@ export function createSelectionSummary(): HTMLParagraphElement {
7
8
  return summary;
8
9
  }
9
10
 
10
- export function orderedItemAccessibleName(label: string, index: number, total: number): string {
11
- return `${label}, position ${index + 1} of ${total}`;
11
+ export function orderedItemAccessibleName(
12
+ messages: PlayerMessageResolver,
13
+ label: string,
14
+ index: number,
15
+ total: number,
16
+ ): string {
17
+ return messages.message("orderedItemAtPosition", { label, position: index + 1, total });
12
18
  }
13
19
 
14
20
  export function announceOrderedItemMove(
15
21
  summary: HTMLElement,
22
+ messages: PlayerMessageResolver,
16
23
  label: string,
17
24
  to: number,
18
25
  total: number,
19
26
  from?: number,
20
27
  ): void {
21
28
  if (from !== undefined && Math.abs(to - from) === 1) {
22
- summary.textContent = `${label} moved ${to < from ? "up" : "down"}.`;
29
+ summary.textContent = messages.message("orderedItemMovedOneStep", {
30
+ label,
31
+ direction: to < from ? "up" : "down",
32
+ });
23
33
  return;
24
34
  }
25
- summary.textContent = `${label} moved to position ${to + 1} of ${total}.`;
35
+ summary.textContent = messages.message("orderedItemMovedToPosition", {
36
+ label,
37
+ position: to + 1,
38
+ total,
39
+ });
26
40
  }
27
41
 
28
42
  export function announceOrderedSelectionCount(
29
43
  summary: HTMLElement,
44
+ messages: PlayerMessageResolver,
30
45
  count: number,
31
- singular: string,
32
- plural: string,
33
46
  ): void {
34
47
  summary.textContent =
35
- count > 0 ? `${count} ${count === 1 ? singular : plural}.` : `No ${plural}.`;
48
+ count > 0
49
+ ? messages.message("graphicOrderRegionsSelected", { count })
50
+ : messages.message("graphicOrderNoRegionsSelected");
36
51
  }
37
52
 
38
53
  export function focusReorderControl(container: ParentNode, identifier: string): void {
@@ -12,12 +12,11 @@ import {
12
12
  objectHeight,
13
13
  objectWidth,
14
14
  placeHotspotButton,
15
- readableType,
16
15
  responseGroup,
17
16
  valueToStrings,
18
17
  } from "../interaction-support.js";
19
18
  import { movementButton } from "../movement.js";
20
- import type { QtiPlayerMessages } from "../player-messages.js";
19
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
21
20
  import {
22
21
  announceOrderedItemMove,
23
22
  announceOrderedSelectionCount,
@@ -30,7 +29,7 @@ export function renderGraphicOrderResponse(
30
29
  interaction: QtiInteraction,
31
30
  update: (value: QtiValue) => void,
32
31
  currentValue: QtiValue,
33
- messages: QtiPlayerMessages,
32
+ messages: PlayerMessageResolver,
34
33
  ): HTMLElement {
35
34
  const group = responseGroup();
36
35
  const width = objectWidth(interaction);
@@ -47,14 +46,17 @@ export function renderGraphicOrderResponse(
47
46
  const surface = document.createElement("div");
48
47
  applyGraphicSurfaceLayout(surface, width, height, "qti3-graphic-order-surface");
49
48
  surface.role = "group";
50
- surface.setAttribute("aria-label", `${readableType(interaction.type)} hotspots`);
49
+ surface.setAttribute(
50
+ "aria-label",
51
+ messages.message("interactionHotspots", { type: interaction.type }),
52
+ );
51
53
 
52
54
  const object = interaction.object;
53
55
  if (object) {
54
56
  appendGraphicObjectImage(
55
57
  surface,
56
58
  object,
57
- object.text || `${readableType(interaction.type)} image`,
59
+ object.text || messages.message("interactionImageAlt", { type: interaction.type }),
58
60
  );
59
61
  }
60
62
 
@@ -82,7 +84,10 @@ export function renderGraphicOrderResponse(
82
84
  const summary = createSelectionSummary();
83
85
  const list = document.createElement("ol");
84
86
  list.className = "qti3-graphic-order-list";
85
- list.setAttribute("aria-label", `${readableType(interaction.type)} selected order`);
87
+ list.setAttribute(
88
+ "aria-label",
89
+ messages.message("interactionSelectedOrderList", { type: interaction.type }),
90
+ );
86
91
 
87
92
  const orderedChoices = () =>
88
93
  orderedIdentifiers
@@ -90,12 +95,7 @@ export function renderGraphicOrderResponse(
90
95
  .filter((choice): choice is QtiChoice => Boolean(choice));
91
96
  const commit = () => update([...orderedIdentifiers]);
92
97
  const updateSelectionCountSummary = () => {
93
- announceOrderedSelectionCount(
94
- summary,
95
- orderedIdentifiers.length,
96
- "region ordered",
97
- "regions ordered",
98
- );
98
+ announceOrderedSelectionCount(summary, messages, orderedIdentifiers.length);
99
99
  };
100
100
  const focusHotspot = (identifier: string) => {
101
101
  surface
@@ -135,7 +135,14 @@ export function renderGraphicOrderResponse(
135
135
  if (!entry) return;
136
136
  orderedIdentifiers.splice(targetIndex, 0, entry);
137
137
  renderState();
138
- announceOrderedItemMove(summary, choiceLabel, targetIndex, orderedIdentifiers.length, index);
138
+ announceOrderedItemMove(
139
+ summary,
140
+ messages,
141
+ choiceLabel,
142
+ targetIndex,
143
+ orderedIdentifiers.length,
144
+ index,
145
+ );
139
146
  commit();
140
147
  focusReorderControl(list, identifier);
141
148
  };
@@ -182,7 +189,7 @@ export function renderGraphicOrderResponse(
182
189
  label.textContent = `${index + 1}. ${choiceLabel}`;
183
190
  label.setAttribute(
184
191
  "aria-label",
185
- orderedItemAccessibleName(choiceLabel, index, currentChoices.length),
192
+ orderedItemAccessibleName(messages, choiceLabel, index, currentChoices.length),
186
193
  );
187
194
  label.addEventListener("click", () => focusHotspot(choice.identifier));
188
195
  label.addEventListener("keydown", (event) => {
@@ -200,14 +207,14 @@ export function renderGraphicOrderResponse(
200
207
 
201
208
  const up = movementButton(
202
209
  "up",
203
- messages.moveChoice({ label: choiceLabel, direction: "up" }),
210
+ messages.message("moveChoice", { label: choiceLabel, direction: "up" }),
204
211
  () => moveHotspot(choice.identifier, -1),
205
212
  );
206
213
  up.disabled = index === 0;
207
214
 
208
215
  const down = movementButton(
209
216
  "down",
210
- messages.moveChoice({ label: choiceLabel, direction: "down" }),
217
+ messages.message("moveChoice", { label: choiceLabel, direction: "down" }),
211
218
  () => moveHotspot(choice.identifier, 1),
212
219
  );
213
220
  down.disabled = index === currentChoices.length - 1;
@@ -1,5 +1,5 @@
1
1
  import { movementButton } from "../movement.js";
2
- import type { QtiPlayerMessages } from "../player-messages.js";
2
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
3
3
  import { orderedItemAccessibleName } from "./a11y.js";
4
4
 
5
5
  export interface ReorderHandleOptions {
@@ -9,7 +9,7 @@ export interface ReorderHandleOptions {
9
9
  total: number;
10
10
  handleClassName: string;
11
11
  visibleText: string;
12
- messages: QtiPlayerMessages;
12
+ messages: PlayerMessageResolver;
13
13
  onMoveBy: (delta: number) => void;
14
14
  }
15
15
 
@@ -25,7 +25,7 @@ export function createReorderHandleControls(options: ReorderHandleOptions): {
25
25
  handle.type = "button";
26
26
  handle.className = handleClassName;
27
27
  handle.dataset.choiceIdentifier = identifier;
28
- handle.setAttribute("aria-label", orderedItemAccessibleName(label, index, total));
28
+ handle.setAttribute("aria-label", orderedItemAccessibleName(messages, label, index, total));
29
29
  handle.textContent = visibleText;
30
30
  handle.addEventListener("keydown", (event) => {
31
31
  if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
@@ -37,13 +37,15 @@ export function createReorderHandleControls(options: ReorderHandleOptions): {
37
37
  }
38
38
  });
39
39
 
40
- const up = movementButton("up", messages.moveChoice({ label, direction: "up" }), () =>
40
+ const up = movementButton("up", messages.message("moveChoice", { label, direction: "up" }), () =>
41
41
  onMoveBy(-1),
42
42
  );
43
43
  up.disabled = index === 0;
44
44
 
45
- const down = movementButton("down", messages.moveChoice({ label, direction: "down" }), () =>
46
- onMoveBy(1),
45
+ const down = movementButton(
46
+ "down",
47
+ messages.message("moveChoice", { label, direction: "down" }),
48
+ () => onMoveBy(1),
47
49
  );
48
50
  down.disabled = index === total - 1;
49
51
 
@@ -1,10 +1,9 @@
1
1
  import type { QtiInteraction, QtiValue } from "@longsightgroup/qti3-core";
2
- import type { QtiPlayerMessages } from "../player-messages.js";
2
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
3
3
  import {
4
4
  interactionChoices,
5
5
  missingChoicesMessage,
6
6
  orderChoicesFromValue,
7
- readableType,
8
7
  responseGroup,
9
8
  } from "../interaction-support.js";
10
9
  import { announceOrderedItemMove, createSelectionSummary, focusReorderControl } from "./a11y.js";
@@ -18,7 +17,7 @@ export function renderOrderedResponse(
18
17
  interaction: QtiInteraction,
19
18
  update: (value: QtiValue) => void,
20
19
  currentValue: QtiValue,
21
- messages: QtiPlayerMessages,
20
+ messages: PlayerMessageResolver,
22
21
  ): HTMLElement {
23
22
  const group = responseGroup();
24
23
  const choices = interactionChoices(interaction).filter((choice) => choice.role !== "gap");
@@ -29,7 +28,10 @@ export function renderOrderedResponse(
29
28
  const ordered = orderChoicesFromValue(choices, currentValue);
30
29
  const list = document.createElement("ol");
31
30
  list.className = "qti3-reorder-list";
32
- list.setAttribute("aria-label", `${readableType(interaction.type)} current order`);
31
+ list.setAttribute(
32
+ "aria-label",
33
+ messages.message("interactionCurrentOrderList", { type: interaction.type }),
34
+ );
33
35
  const summary = createSelectionSummary();
34
36
  const dragState: OrderDragState = {};
35
37
 
@@ -42,7 +44,7 @@ export function renderOrderedResponse(
42
44
  if (!choice) return;
43
45
  ordered.splice(to, 0, choice);
44
46
  renderList();
45
- announceOrderedItemMove(summary, choice.text, to, ordered.length, from);
47
+ announceOrderedItemMove(summary, messages, choice.text, to, ordered.length, from);
46
48
  commit();
47
49
  focusReorderControl(list, choice.identifier);
48
50
  };