@longsightgroup/qti3-player 0.4.0 → 0.5.4

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 (291) hide show
  1. package/README.md +146 -13
  2. package/dist/content/content-dom.d.ts +11 -0
  3. package/dist/content/content-dom.d.ts.map +1 -0
  4. package/dist/content/content-dom.js +262 -0
  5. package/dist/content/content-dom.js.map +1 -0
  6. package/dist/content/content-renderer.d.ts +17 -0
  7. package/dist/content/content-renderer.d.ts.map +1 -0
  8. package/dist/content/content-renderer.js +82 -0
  9. package/dist/content/content-renderer.js.map +1 -0
  10. package/dist/controls/remove-button.d.ts +3 -0
  11. package/dist/controls/remove-button.d.ts.map +1 -0
  12. package/dist/controls/remove-button.js +12 -0
  13. package/dist/controls/remove-button.js.map +1 -0
  14. package/dist/index.d.ts +13 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +6 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/interactions/choice-interaction.d.ts +4 -0
  19. package/dist/interactions/choice-interaction.d.ts.map +1 -0
  20. package/dist/interactions/choice-interaction.js +81 -0
  21. package/dist/interactions/choice-interaction.js.map +1 -0
  22. package/dist/interactions/drawing-interaction.d.ts +6 -0
  23. package/dist/interactions/drawing-interaction.d.ts.map +1 -0
  24. package/dist/interactions/drawing-interaction.js +401 -0
  25. package/dist/interactions/drawing-interaction.js.map +1 -0
  26. package/dist/interactions/end-attempt-interaction.d.ts +4 -0
  27. package/dist/interactions/end-attempt-interaction.d.ts.map +1 -0
  28. package/dist/interactions/end-attempt-interaction.js +13 -0
  29. package/dist/interactions/end-attempt-interaction.js.map +1 -0
  30. package/dist/interactions/gap-match-interaction.d.ts +4 -0
  31. package/dist/interactions/gap-match-interaction.d.ts.map +1 -0
  32. package/dist/interactions/gap-match-interaction.js +277 -0
  33. package/dist/interactions/gap-match-interaction.js.map +1 -0
  34. package/dist/interactions/graphic-associate-interaction.d.ts +4 -0
  35. package/dist/interactions/graphic-associate-interaction.d.ts.map +1 -0
  36. package/dist/interactions/graphic-associate-interaction.js +297 -0
  37. package/dist/interactions/graphic-associate-interaction.js.map +1 -0
  38. package/dist/interactions/graphic-context.d.ts +3 -0
  39. package/dist/interactions/graphic-context.d.ts.map +1 -0
  40. package/dist/interactions/graphic-context.js +35 -0
  41. package/dist/interactions/graphic-context.js.map +1 -0
  42. package/dist/interactions/hotspot-interaction.d.ts +4 -0
  43. package/dist/interactions/hotspot-interaction.d.ts.map +1 -0
  44. package/dist/interactions/hotspot-interaction.js +68 -0
  45. package/dist/interactions/hotspot-interaction.js.map +1 -0
  46. package/dist/interactions/hottext-interaction.d.ts +3 -0
  47. package/dist/interactions/hottext-interaction.d.ts.map +1 -0
  48. package/dist/interactions/hottext-interaction.js +66 -0
  49. package/dist/interactions/hottext-interaction.js.map +1 -0
  50. package/dist/interactions/inline-choice-interaction.d.ts +4 -0
  51. package/dist/interactions/inline-choice-interaction.d.ts.map +1 -0
  52. package/dist/interactions/inline-choice-interaction.js +31 -0
  53. package/dist/interactions/inline-choice-interaction.js.map +1 -0
  54. package/dist/interactions/inline-controls.d.ts +6 -0
  55. package/dist/interactions/inline-controls.d.ts.map +1 -0
  56. package/dist/interactions/inline-controls.js +15 -0
  57. package/dist/interactions/inline-controls.js.map +1 -0
  58. package/dist/interactions/interaction-diagnostics.d.ts +7 -0
  59. package/dist/interactions/interaction-diagnostics.d.ts.map +1 -0
  60. package/dist/interactions/interaction-diagnostics.js +137 -0
  61. package/dist/interactions/interaction-diagnostics.js.map +1 -0
  62. package/dist/interactions/interaction-dispatch.d.ts +2 -0
  63. package/dist/interactions/interaction-dispatch.d.ts.map +1 -0
  64. package/dist/interactions/interaction-dispatch.js +2 -0
  65. package/dist/interactions/interaction-dispatch.js.map +1 -0
  66. package/dist/interactions/interaction-label.d.ts +4 -0
  67. package/dist/interactions/interaction-label.d.ts.map +1 -0
  68. package/dist/interactions/interaction-label.js +8 -0
  69. package/dist/interactions/interaction-label.js.map +1 -0
  70. package/dist/interactions/interaction-registry.d.ts +24 -0
  71. package/dist/interactions/interaction-registry.d.ts.map +1 -0
  72. package/dist/interactions/interaction-registry.js +138 -0
  73. package/dist/interactions/interaction-registry.js.map +1 -0
  74. package/dist/interactions/match-interaction.d.ts +4 -0
  75. package/dist/interactions/match-interaction.d.ts.map +1 -0
  76. package/dist/interactions/match-interaction.js +188 -0
  77. package/dist/interactions/match-interaction.js.map +1 -0
  78. package/dist/interactions/object-asset.d.ts +8 -0
  79. package/dist/interactions/object-asset.d.ts.map +1 -0
  80. package/dist/interactions/object-asset.js +182 -0
  81. package/dist/interactions/object-asset.js.map +1 -0
  82. package/dist/interactions/pair-interaction.d.ts +4 -0
  83. package/dist/interactions/pair-interaction.d.ts.map +1 -0
  84. package/dist/interactions/pair-interaction.js +125 -0
  85. package/dist/interactions/pair-interaction.js.map +1 -0
  86. package/dist/interactions/point-value.d.ts +16 -0
  87. package/dist/interactions/point-value.d.ts.map +1 -0
  88. package/dist/interactions/point-value.js +37 -0
  89. package/dist/interactions/point-value.js.map +1 -0
  90. package/dist/interactions/portable-custom-interaction.d.ts +16 -0
  91. package/dist/interactions/portable-custom-interaction.d.ts.map +1 -0
  92. package/dist/interactions/portable-custom-interaction.js +97 -0
  93. package/dist/interactions/portable-custom-interaction.js.map +1 -0
  94. package/dist/interactions/position-object-interaction.d.ts +4 -0
  95. package/dist/interactions/position-object-interaction.d.ts.map +1 -0
  96. package/dist/interactions/position-object-interaction.js +177 -0
  97. package/dist/interactions/position-object-interaction.js.map +1 -0
  98. package/dist/interactions/routing.d.ts +5 -0
  99. package/dist/interactions/routing.d.ts.map +1 -0
  100. package/dist/interactions/routing.js +24 -0
  101. package/dist/interactions/routing.js.map +1 -0
  102. package/dist/interactions/select-point-interaction.d.ts +4 -0
  103. package/dist/interactions/select-point-interaction.d.ts.map +1 -0
  104. package/dist/interactions/select-point-interaction.js +166 -0
  105. package/dist/interactions/select-point-interaction.js.map +1 -0
  106. package/dist/interactions/shared.d.ts +12 -0
  107. package/dist/interactions/shared.d.ts.map +1 -0
  108. package/dist/interactions/shared.js +65 -0
  109. package/dist/interactions/shared.js.map +1 -0
  110. package/dist/interactions/text-interaction.d.ts +6 -0
  111. package/dist/interactions/text-interaction.d.ts.map +1 -0
  112. package/dist/interactions/text-interaction.js +110 -0
  113. package/dist/interactions/text-interaction.js.map +1 -0
  114. package/dist/interactions/unsupported-interaction.d.ts +4 -0
  115. package/dist/interactions/unsupported-interaction.d.ts.map +1 -0
  116. package/dist/interactions/unsupported-interaction.js +23 -0
  117. package/dist/interactions/unsupported-interaction.js.map +1 -0
  118. package/dist/interactions/upload-interaction.d.ts +4 -0
  119. package/dist/interactions/upload-interaction.d.ts.map +1 -0
  120. package/dist/interactions/upload-interaction.js +10 -0
  121. package/dist/interactions/upload-interaction.js.map +1 -0
  122. package/dist/player/attempt-availability.d.ts +6 -0
  123. package/dist/player/attempt-availability.d.ts.map +1 -0
  124. package/dist/player/attempt-availability.js +27 -0
  125. package/dist/player/attempt-availability.js.map +1 -0
  126. package/dist/player/content-state.d.ts +19 -0
  127. package/dist/player/content-state.d.ts.map +1 -0
  128. package/dist/player/content-state.js +39 -0
  129. package/dist/player/content-state.js.map +1 -0
  130. package/dist/player/dynamic-body.d.ts +7 -0
  131. package/dist/player/dynamic-body.d.ts.map +1 -0
  132. package/dist/player/dynamic-body.js +28 -0
  133. package/dist/player/dynamic-body.js.map +1 -0
  134. package/dist/player/feedback-panel.d.ts +3 -0
  135. package/dist/player/feedback-panel.d.ts.map +1 -0
  136. package/dist/player/feedback-panel.js +14 -0
  137. package/dist/player/feedback-panel.js.map +1 -0
  138. package/dist/player/fetch-xml.d.ts +2 -0
  139. package/dist/player/fetch-xml.d.ts.map +1 -0
  140. package/dist/player/fetch-xml.js +10 -0
  141. package/dist/player/fetch-xml.js.map +1 -0
  142. package/dist/player/interaction-render.d.ts +14 -0
  143. package/dist/player/interaction-render.d.ts.map +1 -0
  144. package/dist/player/interaction-render.js +51 -0
  145. package/dist/player/interaction-render.js.map +1 -0
  146. package/dist/player/render-shell.d.ts +8 -0
  147. package/dist/player/render-shell.d.ts.map +1 -0
  148. package/dist/player/render-shell.js +37 -0
  149. package/dist/player/render-shell.js.map +1 -0
  150. package/dist/player/resolve-assets.d.ts +3 -0
  151. package/dist/player/resolve-assets.d.ts.map +1 -0
  152. package/dist/player/resolve-assets.js +12 -0
  153. package/dist/player/resolve-assets.js.map +1 -0
  154. package/dist/player/validation-messages.d.ts +17 -0
  155. package/dist/player/validation-messages.d.ts.map +1 -0
  156. package/dist/player/validation-messages.js +33 -0
  157. package/dist/player/validation-messages.js.map +1 -0
  158. package/dist/player-adapter.d.ts +62 -0
  159. package/dist/player-adapter.d.ts.map +1 -0
  160. package/dist/player-adapter.js +119 -0
  161. package/dist/player-adapter.js.map +1 -0
  162. package/dist/player-dev.d.ts +4 -0
  163. package/dist/player-dev.d.ts.map +1 -0
  164. package/dist/player-dev.js +14 -0
  165. package/dist/player-dev.js.map +1 -0
  166. package/dist/player-element.d.ts +14 -1
  167. package/dist/player-element.d.ts.map +1 -1
  168. package/dist/player-element.js +57 -5
  169. package/dist/player-element.js.map +1 -1
  170. package/dist/player-locale.d.ts +8 -3
  171. package/dist/player-locale.d.ts.map +1 -1
  172. package/dist/player-locale.js +16 -175
  173. package/dist/player-locale.js.map +1 -1
  174. package/dist/player-message-catalog-default.d.ts +4 -0
  175. package/dist/player-message-catalog-default.d.ts.map +1 -0
  176. package/dist/player-message-catalog-default.js +118 -0
  177. package/dist/player-message-catalog-default.js.map +1 -0
  178. package/dist/player-message-catalog-validate.d.ts +31 -0
  179. package/dist/player-message-catalog-validate.d.ts.map +1 -0
  180. package/dist/player-message-catalog-validate.js +327 -0
  181. package/dist/player-message-catalog-validate.js.map +1 -0
  182. package/dist/player-message-catalog.d.ts +18 -0
  183. package/dist/player-message-catalog.d.ts.map +1 -0
  184. package/dist/player-message-catalog.js +40 -0
  185. package/dist/player-message-catalog.js.map +1 -0
  186. package/dist/player-message-keys.d.ts +6 -0
  187. package/dist/player-message-keys.d.ts.map +1 -0
  188. package/dist/player-message-keys.js +7 -0
  189. package/dist/player-message-keys.js.map +1 -0
  190. package/dist/player-message-manifest.d.ts +272 -0
  191. package/dist/player-message-manifest.d.ts.map +1 -0
  192. package/dist/player-message-manifest.js +83 -0
  193. package/dist/player-message-manifest.js.map +1 -0
  194. package/dist/player-message-overrides.d.ts +3 -0
  195. package/dist/player-message-overrides.d.ts.map +1 -0
  196. package/dist/player-message-overrides.js +28 -0
  197. package/dist/player-message-overrides.js.map +1 -0
  198. package/dist/player-message-resolver.d.ts +31 -0
  199. package/dist/player-message-resolver.d.ts.map +1 -0
  200. package/dist/player-message-resolver.js +110 -0
  201. package/dist/player-message-resolver.js.map +1 -0
  202. package/dist/player-messages.d.ts +0 -38
  203. package/dist/player-messages.d.ts.map +1 -1
  204. package/dist/player-types.d.ts +12 -2
  205. package/dist/player-types.d.ts.map +1 -1
  206. package/dist/reorder/a11y.d.ts +7 -0
  207. package/dist/reorder/a11y.d.ts.map +1 -0
  208. package/dist/reorder/a11y.js +34 -0
  209. package/dist/reorder/a11y.js.map +1 -0
  210. package/dist/reorder/graphic-order-interaction.d.ts +4 -0
  211. package/dist/reorder/graphic-order-interaction.d.ts.map +1 -0
  212. package/dist/reorder/graphic-order-interaction.js +205 -0
  213. package/dist/reorder/graphic-order-interaction.js.map +1 -0
  214. package/dist/reorder/list-controls.d.ts +22 -0
  215. package/dist/reorder/list-controls.d.ts.map +1 -0
  216. package/dist/reorder/list-controls.js +75 -0
  217. package/dist/reorder/list-controls.js.map +1 -0
  218. package/dist/reorder/order-interaction.d.ts +4 -0
  219. package/dist/reorder/order-interaction.d.ts.map +1 -0
  220. package/dist/reorder/order-interaction.js +54 -0
  221. package/dist/reorder/order-interaction.js.map +1 -0
  222. package/dist/styles/base-styles.d.ts +2 -0
  223. package/dist/styles/base-styles.d.ts.map +1 -0
  224. package/dist/styles/base-styles.js +133 -0
  225. package/dist/styles/base-styles.js.map +1 -0
  226. package/dist/styles/choice-hottext-styles.d.ts +2 -0
  227. package/dist/styles/choice-hottext-styles.d.ts.map +1 -0
  228. package/dist/styles/choice-hottext-styles.js +76 -0
  229. package/dist/styles/choice-hottext-styles.js.map +1 -0
  230. package/dist/styles/control-styles.d.ts +2 -0
  231. package/dist/styles/control-styles.d.ts.map +1 -0
  232. package/dist/styles/control-styles.js +114 -0
  233. package/dist/styles/control-styles.js.map +1 -0
  234. package/dist/styles/drawing-styles.d.ts +2 -0
  235. package/dist/styles/drawing-styles.d.ts.map +1 -0
  236. package/dist/styles/drawing-styles.js +30 -0
  237. package/dist/styles/drawing-styles.js.map +1 -0
  238. package/dist/styles/gap-match-styles.d.ts +2 -0
  239. package/dist/styles/gap-match-styles.d.ts.map +1 -0
  240. package/dist/styles/gap-match-styles.js +33 -0
  241. package/dist/styles/gap-match-styles.js.map +1 -0
  242. package/dist/styles/graphic-styles.d.ts +2 -0
  243. package/dist/styles/graphic-styles.d.ts.map +1 -0
  244. package/dist/styles/graphic-styles.js +289 -0
  245. package/dist/styles/graphic-styles.js.map +1 -0
  246. package/dist/styles/match-pair-styles.d.ts +2 -0
  247. package/dist/styles/match-pair-styles.d.ts.map +1 -0
  248. package/dist/styles/match-pair-styles.js +62 -0
  249. package/dist/styles/match-pair-styles.js.map +1 -0
  250. package/dist/styles/text-slider-styles.d.ts +2 -0
  251. package/dist/styles/text-slider-styles.d.ts.map +1 -0
  252. package/dist/styles/text-slider-styles.js +35 -0
  253. package/dist/styles/text-slider-styles.js.map +1 -0
  254. package/package.json +8 -8
  255. package/src/controls/remove-button.ts +8 -5
  256. package/src/index.ts +61 -5
  257. package/src/interactions/choice-interaction.ts +6 -2
  258. package/src/interactions/drawing-interaction.ts +14 -9
  259. package/src/interactions/end-attempt-interaction.ts +3 -3
  260. package/src/interactions/gap-match-interaction.ts +32 -13
  261. package/src/interactions/graphic-associate-interaction.ts +15 -10
  262. package/src/interactions/hotspot-interaction.ts +10 -6
  263. package/src/interactions/inline-choice-interaction.ts +4 -4
  264. package/src/interactions/interaction-registry.ts +12 -12
  265. package/src/interactions/match-interaction.ts +9 -6
  266. package/src/interactions/pair-interaction.ts +22 -14
  267. package/src/interactions/position-object-interaction.ts +22 -13
  268. package/src/interactions/select-point-interaction.ts +25 -13
  269. package/src/interactions/shared.ts +21 -4
  270. package/src/interactions/text-interaction.ts +14 -4
  271. package/src/interactions/upload-interaction.ts +6 -3
  272. package/src/player/interaction-render.ts +4 -4
  273. package/src/player-adapter.ts +253 -0
  274. package/src/player-dev.ts +14 -0
  275. package/src/player-element.ts +78 -8
  276. package/src/player-locale.ts +28 -199
  277. package/src/player-message-catalog-default.ts +119 -0
  278. package/src/player-message-catalog-validate.ts +425 -0
  279. package/src/player-message-catalog.ts +72 -0
  280. package/src/player-message-keys.ts +12 -0
  281. package/src/player-message-manifest.ts +103 -0
  282. package/src/player-message-overrides.ts +38 -0
  283. package/src/player-message-resolver.ts +205 -0
  284. package/src/player-messages.ts +0 -30
  285. package/src/player-types.ts +15 -4
  286. package/src/reorder/a11y.ts +22 -7
  287. package/src/reorder/graphic-order-interaction.ts +23 -16
  288. package/src/reorder/list-controls.ts +8 -6
  289. package/src/reorder/order-interaction.ts +7 -5
  290. package/src/styles/base-styles.ts +20 -5
  291. package/src/styles/graphic-styles.ts +0 -6
@@ -5,11 +5,10 @@ import {
5
5
  appendGraphicObjectImage,
6
6
  objectHeight,
7
7
  objectWidth,
8
- readableType,
9
8
  responseGroup,
10
9
  } from "../interaction-support.js";
11
10
  import { movementButton } from "../movement.js";
12
- import type { QtiPlayerMessages } from "../player-messages.js";
11
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
13
12
  import { maximumAllowedResponses } from "../response-limits.js";
14
13
  import { parsePointValues, pointToString } from "./point-value.js";
15
14
 
@@ -17,11 +16,14 @@ export function renderSelectPointResponse(
17
16
  interaction: QtiInteraction,
18
17
  update: (value: QtiValue) => void,
19
18
  currentValue: QtiValue,
20
- messages: QtiPlayerMessages,
19
+ messages: PlayerMessageResolver,
21
20
  ): HTMLElement {
22
21
  const group = responseGroup();
23
22
  group.role = "group";
24
- group.setAttribute("aria-label", `${readableType(interaction.type)} coordinate response`);
23
+ group.setAttribute(
24
+ "aria-label",
25
+ messages.message("interactionCoordinateResponse", { type: interaction.type }),
26
+ );
25
27
  const isMultiple = interaction.responseCardinality === "multiple";
26
28
  const maxPoints = isMultiple ? maximumAllowedResponses(interaction) : 1;
27
29
 
@@ -33,7 +35,10 @@ export function renderSelectPointResponse(
33
35
  objectHeight(interaction),
34
36
  "qti3-point-surface",
35
37
  );
36
- surface.setAttribute("aria-label", `${readableType(interaction.type)} coordinate area`);
38
+ surface.setAttribute(
39
+ "aria-label",
40
+ messages.message("interactionCoordinateArea", { type: interaction.type }),
41
+ );
37
42
 
38
43
  const object = interaction.object;
39
44
  if (object) {
@@ -45,7 +50,8 @@ export function renderSelectPointResponse(
45
50
  let points = parsePointValues(currentValue);
46
51
  let activeIndex = points.length > 0 ? points.length - 1 : -1;
47
52
  const coordinate = document.createElement("output");
48
- coordinate.className = "qti3-coordinate-output";
53
+ coordinate.className = "qti3-coordinate-output qti-visually-hidden";
54
+ coordinate.setAttribute("aria-live", "polite");
49
55
  const initialPoint = () => ({
50
56
  x: Math.round(width / 2),
51
57
  y: Math.round(height / 2),
@@ -62,8 +68,11 @@ export function renderSelectPointResponse(
62
68
  surface.querySelectorAll(".qti3-point-marker").forEach((marker) => marker.remove());
63
69
  if (points.length === 0) {
64
70
  coordinate.value = "";
65
- coordinate.textContent = messages.noPointSelected();
66
- surface.setAttribute("aria-label", `${readableType(interaction.type)} coordinate area`);
71
+ coordinate.textContent = messages.message("noPointSelected");
72
+ surface.setAttribute(
73
+ "aria-label",
74
+ messages.message("interactionCoordinateArea", { type: interaction.type }),
75
+ );
67
76
  return;
68
77
  }
69
78
  points.forEach((point, index) => {
@@ -83,11 +92,14 @@ export function renderSelectPointResponse(
83
92
  ? points.map(pointToString).join(" | ")
84
93
  : pointToString(points[0]);
85
94
  coordinate.textContent = isMultiple
86
- ? `${points.length} selected point${points.length === 1 ? "" : "s"}: ${text}`
87
- : `Selected point ${pointToString(points[0])}`;
95
+ ? messages.message("selectedPointsSummary", { count: points.length, coordinates: text })
96
+ : messages.message("selectedPointAt", { coordinates: pointToString(points[0]) });
88
97
  surface.setAttribute(
89
98
  "aria-label",
90
- `${readableType(interaction.type)} coordinate area, selected ${text}`,
99
+ messages.message("interactionCoordinateAreaSelected", {
100
+ type: interaction.type,
101
+ coordinates: text,
102
+ }),
91
103
  );
92
104
  };
93
105
  const clampPoint = (point: { x: number; y: number }) => {
@@ -158,7 +170,7 @@ export function renderSelectPointResponse(
158
170
  ["down", 0, 1],
159
171
  ] as const) {
160
172
  controls.append(
161
- movementButton(direction, messages.movePoint({ direction }), () => {
173
+ movementButton(direction, messages.message("movePoint", { direction }), () => {
162
174
  const point = mutableActivePoint();
163
175
  point.x += dx;
164
176
  point.y += dy;
@@ -171,7 +183,7 @@ export function renderSelectPointResponse(
171
183
  if (isMultiple) {
172
184
  const clear = document.createElement("button");
173
185
  clear.type = "button";
174
- clear.textContent = messages.clearPoints();
186
+ clear.textContent = messages.message("clearPoints");
175
187
  clear.addEventListener("click", () => {
176
188
  points = [];
177
189
  activeIndex = -1;
@@ -1,4 +1,5 @@
1
1
  import type { QtiChoice, QtiInteraction } from "@longsightgroup/qti3-core";
2
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
2
3
  import { interactionChoices } from "../interaction-support.js";
3
4
 
4
5
  export function tokenRegion(label: string, visibleLabel?: string): HTMLElement {
@@ -49,8 +50,24 @@ export function targetChoices(interaction: QtiInteraction): QtiChoice[] {
49
50
  return targets.length > 0 ? targets : choices;
50
51
  }
51
52
 
52
- export function pairRegionLabels(interaction: QtiInteraction): { source: string; target: string } {
53
- if (interaction.type === "associate") return { source: "First concept", target: "Pair with" };
54
- if (interaction.type === "match") return { source: "Prompt", target: "Match" };
55
- return { source: "Source", target: "Target" };
53
+ export function pairRegionLabels(
54
+ interaction: QtiInteraction,
55
+ messages: PlayerMessageResolver,
56
+ ): { source: string; target: string } {
57
+ if (interaction.type === "associate") {
58
+ return {
59
+ source: messages.message("associateFirstConceptRegion"),
60
+ target: messages.message("associatePairWithRegion"),
61
+ };
62
+ }
63
+ if (interaction.type === "match") {
64
+ return {
65
+ source: messages.message("matchPromptRegion"),
66
+ target: messages.message("matchMatchRegion"),
67
+ };
68
+ }
69
+ return {
70
+ source: messages.message("genericSourceRegion"),
71
+ target: messages.message("genericTargetRegion"),
72
+ };
56
73
  }
@@ -1,4 +1,5 @@
1
1
  import type { QtiInteraction, QtiValue } from "@longsightgroup/qti3-core";
2
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
2
3
 
3
4
  function scalarString(value: QtiValue): string {
4
5
  if (value === null || Array.isArray(value) || typeof value === "object") return "";
@@ -32,6 +33,7 @@ export function renderTextResponse(
32
33
  update: (value: QtiValue) => void,
33
34
  mode: "entry" | "extended",
34
35
  currentValue: QtiValue,
36
+ messages: PlayerMessageResolver,
35
37
  ): HTMLElement {
36
38
  const group = document.createElement("div");
37
39
  group.className = "qti3-text-response";
@@ -43,7 +45,10 @@ export function renderTextResponse(
43
45
  control.value = scalarString(currentValue);
44
46
  control.setAttribute(
45
47
  "aria-label",
46
- interaction.prompt ?? (mode === "extended" ? "Extended text response" : "Text response"),
48
+ interaction.prompt ??
49
+ (mode === "extended"
50
+ ? messages.message("extendedTextResponseLabel")
51
+ : messages.message("textResponseLabel")),
47
52
  );
48
53
  if (mode === "extended" && expectedLines > 0) {
49
54
  (control as HTMLTextAreaElement).rows = expectedLines;
@@ -60,7 +65,10 @@ export function renderTextResponse(
60
65
  const value = control.value;
61
66
  if (counter) {
62
67
  const words = value.trim().length > 0 ? value.trim().split(/\s+/).length : 0;
63
- counter.textContent = `${value.length} characters, ${words} words`;
68
+ counter.textContent = messages.message("extendedTextCounter", {
69
+ characters: value.length,
70
+ words,
71
+ });
64
72
  }
65
73
  if (emitResponse) update(value);
66
74
  };
@@ -76,6 +84,7 @@ export function renderInlineTextEntry(
76
84
  interaction: QtiInteraction,
77
85
  update: (value: QtiValue) => void,
78
86
  currentValue: QtiValue,
87
+ messages: PlayerMessageResolver,
79
88
  ): HTMLElement {
80
89
  const group = document.createElement("span");
81
90
  group.className = "qti3-inline-text-response";
@@ -84,7 +93,7 @@ export function renderInlineTextEntry(
84
93
  input.value = scalarString(currentValue);
85
94
  input.setAttribute(
86
95
  "aria-label",
87
- interaction.prompt ?? interaction.contextText ?? "Text response",
96
+ interaction.prompt ?? interaction.contextText ?? messages.message("textResponseLabel"),
88
97
  );
89
98
  const expectedLength = Number(interaction.attributes["expected-length"] ?? 0);
90
99
  applyExpectedTextEntryWidth(input, expectedLength);
@@ -102,6 +111,7 @@ export function renderSliderResponse(
102
111
  interaction: QtiInteraction,
103
112
  update: (value: QtiValue) => void,
104
113
  currentValue: QtiValue,
114
+ messages: PlayerMessageResolver,
105
115
  ): HTMLElement {
106
116
  const group = document.createElement("div");
107
117
  group.className = "qti3-slider-response";
@@ -111,7 +121,7 @@ export function renderSliderResponse(
111
121
  input.max = interaction.attributes["upper-bound"] ?? "100";
112
122
  input.step = interaction.attributes.step ?? "1";
113
123
  input.value = scalarString(currentValue) || interaction.attributes["lower-bound"] || "0";
114
- input.setAttribute("aria-label", interaction.prompt ?? "Slider response");
124
+ input.setAttribute("aria-label", interaction.prompt ?? messages.message("sliderResponseLabel"));
115
125
  const output = document.createElement("output");
116
126
  output.className = "qti3-slider-output";
117
127
  output.value = input.value;
@@ -1,16 +1,19 @@
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 { interactionLabel } from "./interaction-label.js";
4
4
 
5
5
  export function renderUploadResponse(
6
6
  interaction: QtiInteraction,
7
7
  update: (value: QtiValue) => void,
8
- messages: QtiPlayerMessages,
8
+ messages: PlayerMessageResolver,
9
9
  ): HTMLElement {
10
10
  const input = document.createElement("input");
11
11
  input.type = "file";
12
12
  input.className = "qti3-upload-input";
13
- input.setAttribute("aria-label", interactionLabel(interaction) || messages.uploadResponse());
13
+ input.setAttribute(
14
+ "aria-label",
15
+ interactionLabel(interaction) || messages.message("uploadResponse"),
16
+ );
14
17
  input.addEventListener("change", () => update(input.files?.[0]?.name ?? ""));
15
18
  return input;
16
19
  }
@@ -5,12 +5,12 @@ import { interactionLabel, qtiSharedClassNames } from "../interactions/interacti
5
5
  import { renderSelect } from "../interactions/inline-choice-interaction.js";
6
6
  import { renderInlineTextEntry } from "../interactions/text-interaction.js";
7
7
  import { renderUnsupportedEmbeddedInteraction } from "../interactions/unsupported-interaction.js";
8
- import type { QtiPlayerMessages } from "../player-messages.js";
8
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
9
9
  import { inlineValidationMessageElement, validationMessageElement } from "../player-validation.js";
10
10
 
11
11
  export interface BlockInteractionRenderOptions {
12
12
  interaction: QtiInteraction;
13
- messages: QtiPlayerMessages;
13
+ messages: PlayerMessageResolver;
14
14
  update: (value: QtiValue) => void;
15
15
  currentValue: QtiValue;
16
16
  isCompleted: () => boolean;
@@ -65,7 +65,7 @@ export function renderEmbeddedInteractionSection(
65
65
  interaction: QtiInteraction,
66
66
  update: (value: QtiValue) => void,
67
67
  currentValue: QtiValue,
68
- messages: QtiPlayerMessages,
68
+ messages: PlayerMessageResolver,
69
69
  ): HTMLElement {
70
70
  if (interaction.type !== "inlineChoice" && interaction.type !== "textEntry") {
71
71
  return renderUnsupportedEmbeddedInteraction(interaction);
@@ -83,7 +83,7 @@ export function renderEmbeddedInteractionSection(
83
83
  wrapper.append(
84
84
  interaction.type === "inlineChoice"
85
85
  ? renderSelect(interaction, update, currentValue, messages)
86
- : renderInlineTextEntry(interaction, update, currentValue),
86
+ : renderInlineTextEntry(interaction, update, currentValue, messages),
87
87
  );
88
88
  return wrapper;
89
89
  }
@@ -0,0 +1,253 @@
1
+ import type {
2
+ QtiAttemptStateV1,
3
+ QtiCatalogSupportResolution,
4
+ QtiCatalogSupportResolutionOptions,
5
+ QtiScoreResult,
6
+ QtiTextToSpeechTraversal,
7
+ } from "@longsightgroup/qti3-core";
8
+ import type { PlayerMessageCatalog } from "./player-message-catalog.js";
9
+ import type { QtiAssessmentItemPlayer } from "./player-element.js";
10
+ import type { QtiPlayerMessageOverrides } from "./player-message-resolver.js";
11
+ import type {
12
+ QtiAssessmentItemPlayerEventDetailMap,
13
+ QtiAssessmentItemPlayerEventName,
14
+ QtiPlayerFetchXml,
15
+ QtiPlayerLoadOptions,
16
+ QtiPlayerResolveAsset,
17
+ QtiScoreAttemptOptions,
18
+ } from "./player-types.js";
19
+
20
+ export type QtiAssessmentItemPlayerAdapterEventPropName =
21
+ | "onReady"
22
+ | "onResponseChange"
23
+ | "onStateChange"
24
+ | "onScore"
25
+ | "onValidation"
26
+ | "onSuspend"
27
+ | "onEndAttempt"
28
+ | "onPortableCustomMount"
29
+ | "onDiagnostics"
30
+ | "onReset"
31
+ | "onRestore";
32
+
33
+ export type QtiAssessmentItemPlayerAdapterPropName =
34
+ | "xml"
35
+ | "loadOptions"
36
+ | "languageOfInterface"
37
+ | "messageCatalog"
38
+ | "messages"
39
+ | "onLoadError"
40
+ | QtiAssessmentItemPlayerAdapterEventPropName;
41
+
42
+ export type QtiAssessmentItemPlayerAdapterEventCallback<
43
+ T extends QtiAssessmentItemPlayerEventName,
44
+ > = (detail: QtiAssessmentItemPlayerEventDetailMap[T]) => void;
45
+
46
+ export type QtiAssessmentItemPlayerAdapterEventHandlerProps = {
47
+ [K in (typeof qtiAssessmentItemPlayerAdapterEventEntries)[number] as K[1]]?: QtiAssessmentItemPlayerAdapterEventCallback<
48
+ K[0]
49
+ >;
50
+ };
51
+
52
+ export interface QtiAssessmentItemPlayerAdapterProps extends QtiAssessmentItemPlayerAdapterEventHandlerProps {
53
+ /** Already-prepared candidate-safe XML for delivery, or authoring/preview XML in preview tools. */
54
+ xml?: string | undefined;
55
+ loadOptions?: QtiPlayerLoadOptions | undefined;
56
+ languageOfInterface?: string | undefined;
57
+ messageCatalog?: PlayerMessageCatalog | undefined;
58
+ messages?: QtiPlayerMessageOverrides | undefined;
59
+ onLoadError?: ((error: Error) => void) | undefined;
60
+ }
61
+
62
+ export interface QtiAssessmentItemPlayerHandle {
63
+ readonly element: QtiAssessmentItemPlayer;
64
+ loadXml(xml: string, options?: QtiPlayerLoadOptions): Promise<void>;
65
+ loadUrl(url: string, options?: QtiPlayerLoadOptions): Promise<void>;
66
+ scoreAttempt(options?: QtiScoreAttemptOptions): QtiScoreResult | undefined;
67
+ restore(state: QtiAttemptStateV1): void;
68
+ suspend(): void;
69
+ endAttempt(options?: QtiScoreAttemptOptions): void;
70
+ reset(): void;
71
+ clearItem(): void;
72
+ serialize(): QtiAttemptStateV1 | undefined;
73
+ getTextToSpeechTraversal(): QtiTextToSpeechTraversal | undefined;
74
+ getCatalogSupportResolution(
75
+ options?: QtiCatalogSupportResolutionOptions,
76
+ ): QtiCatalogSupportResolution | undefined;
77
+ }
78
+
79
+ export const qtiAssessmentItemPlayerAdapterEventEntries = [
80
+ ["qti-ready", "onReady"],
81
+ ["qti-responsechange", "onResponseChange"],
82
+ ["qti-statechange", "onStateChange"],
83
+ ["qti-score", "onScore"],
84
+ ["qti-validation", "onValidation"],
85
+ ["qti-suspend", "onSuspend"],
86
+ ["qti-endattempt", "onEndAttempt"],
87
+ ["qti-portable-custom-mount", "onPortableCustomMount"],
88
+ ["qti-diagnostics", "onDiagnostics"],
89
+ ["qti-reset", "onReset"],
90
+ ["qti-restore", "onRestore"],
91
+ ] as const satisfies readonly (readonly [
92
+ QtiAssessmentItemPlayerEventName,
93
+ QtiAssessmentItemPlayerAdapterEventPropName,
94
+ ])[];
95
+
96
+ export const qtiAssessmentItemPlayerAdapterPropNames = [
97
+ "xml",
98
+ "loadOptions",
99
+ "languageOfInterface",
100
+ "messageCatalog",
101
+ "messages",
102
+ "onLoadError",
103
+ ...qtiAssessmentItemPlayerAdapterEventEntries.map((entry) => entry[1]),
104
+ ] as const satisfies readonly QtiAssessmentItemPlayerAdapterPropName[];
105
+
106
+ const qtiAssessmentItemPlayerAdapterPropNameSet = new Set<string>(
107
+ qtiAssessmentItemPlayerAdapterPropNames,
108
+ );
109
+
110
+ export function isQtiAssessmentItemPlayerAdapterPropName(name: string): boolean {
111
+ return qtiAssessmentItemPlayerAdapterPropNameSet.has(name);
112
+ }
113
+
114
+ export type QtiAssessmentItemPlayerLoadDependencies = readonly [
115
+ string | undefined,
116
+ QtiPlayerLoadOptions["status"] | undefined,
117
+ boolean | undefined,
118
+ boolean | undefined,
119
+ QtiPlayerFetchXml | undefined,
120
+ QtiPlayerResolveAsset | undefined,
121
+ ];
122
+
123
+ export function qtiAssessmentItemPlayerLoadStateKey(
124
+ state: QtiAttemptStateV1 | undefined,
125
+ ): string | undefined {
126
+ if (!state) return undefined;
127
+ // JSON key order follows insertion order; in-place mutation without reload is not detected.
128
+ return JSON.stringify(state);
129
+ }
130
+
131
+ export function qtiAssessmentItemPlayerLoadDependencies(
132
+ loadOptions: QtiPlayerLoadOptions | undefined,
133
+ ): QtiAssessmentItemPlayerLoadDependencies {
134
+ return [
135
+ qtiAssessmentItemPlayerLoadStateKey(loadOptions?.state),
136
+ loadOptions?.status,
137
+ loadOptions?.sessionControl?.validateResponses,
138
+ loadOptions?.sessionControl?.showFeedback,
139
+ loadOptions?.fetchXml,
140
+ loadOptions?.resolveAsset,
141
+ ];
142
+ }
143
+
144
+ export function bindQtiAssessmentItemPlayerAdapterEvents(
145
+ element: QtiAssessmentItemPlayer,
146
+ getProps: () => QtiAssessmentItemPlayerAdapterProps,
147
+ ): () => void {
148
+ const removers = qtiAssessmentItemPlayerAdapterEventEntries.map(([eventName, propName]) => {
149
+ const listener = (event: Event) => {
150
+ const callback = getProps()[propName] as
151
+ | QtiAssessmentItemPlayerAdapterEventCallback<typeof eventName>
152
+ | undefined;
153
+ callback?.(
154
+ (event as CustomEvent<QtiAssessmentItemPlayerEventDetailMap[typeof eventName]>).detail,
155
+ );
156
+ };
157
+ element.addEventListener(eventName, listener);
158
+ return () => element.removeEventListener(eventName, listener);
159
+ });
160
+ return () => {
161
+ for (const remove of removers) remove();
162
+ };
163
+ }
164
+
165
+ export function syncQtiAssessmentItemPlayerAdapterChrome(
166
+ element: QtiAssessmentItemPlayer,
167
+ props: Pick<
168
+ QtiAssessmentItemPlayerAdapterProps,
169
+ "languageOfInterface" | "messageCatalog" | "messages"
170
+ >,
171
+ ): void {
172
+ element.languageOfInterface = props.languageOfInterface;
173
+ element.messageCatalog = props.messageCatalog;
174
+ element.messages = props.messages;
175
+ }
176
+
177
+ /** @deprecated Use {@link syncQtiAssessmentItemPlayerAdapterChrome}. */
178
+ export const syncQtiAssessmentItemPlayerAdapterMessages = syncQtiAssessmentItemPlayerAdapterChrome;
179
+
180
+ export interface QtiAssessmentItemPlayerAdapterLoadSyncInput {
181
+ xml?: string | undefined;
182
+ loadOptions?: QtiPlayerLoadOptions | undefined;
183
+ onLoadError?: ((error: Error) => void) | undefined;
184
+ }
185
+
186
+ export function createQtiAssessmentItemPlayerAdapterLoadSync(): {
187
+ run(
188
+ element: QtiAssessmentItemPlayer,
189
+ input: QtiAssessmentItemPlayerAdapterLoadSyncInput,
190
+ ): () => void;
191
+ } {
192
+ let loadSequence = 0;
193
+
194
+ return {
195
+ run(element, input) {
196
+ if (input.xml === undefined) {
197
+ loadSequence += 1;
198
+ element.clearItem();
199
+ return () => {};
200
+ }
201
+
202
+ let active = true;
203
+ const sequence = (loadSequence += 1);
204
+ void element
205
+ .loadXml(input.xml, input.loadOptions)
206
+ .then(() => {
207
+ if (!active || sequence !== loadSequence) return;
208
+ })
209
+ .catch((error: unknown) => {
210
+ if (!active || sequence !== loadSequence) return;
211
+ input.onLoadError?.(normalizeQtiAssessmentItemPlayerLoadError(error));
212
+ });
213
+
214
+ return () => {
215
+ active = false;
216
+ };
217
+ },
218
+ };
219
+ }
220
+
221
+ export function createQtiAssessmentItemPlayerHandle(
222
+ getElement: () => QtiAssessmentItemPlayer | null,
223
+ ): QtiAssessmentItemPlayerHandle {
224
+ return {
225
+ get element() {
226
+ return requiredElement(getElement);
227
+ },
228
+ loadXml: (xml, options) => requiredElement(getElement).loadXml(xml, options),
229
+ loadUrl: (url, options) => requiredElement(getElement).loadUrl(url, options),
230
+ scoreAttempt: (options) => requiredElement(getElement).scoreAttempt(options),
231
+ restore: (state) => requiredElement(getElement).restore(state),
232
+ suspend: () => requiredElement(getElement).suspend(),
233
+ endAttempt: (options) => requiredElement(getElement).endAttempt(options),
234
+ reset: () => requiredElement(getElement).reset(),
235
+ clearItem: () => requiredElement(getElement).clearItem(),
236
+ serialize: () => requiredElement(getElement).serialize(),
237
+ getTextToSpeechTraversal: () => requiredElement(getElement).getTextToSpeechTraversal(),
238
+ getCatalogSupportResolution: (options) =>
239
+ requiredElement(getElement).getCatalogSupportResolution(options),
240
+ };
241
+ }
242
+
243
+ export function normalizeQtiAssessmentItemPlayerLoadError(error: unknown): Error {
244
+ return error instanceof Error ? error : new Error(String(error));
245
+ }
246
+
247
+ function requiredElement(
248
+ getElement: () => QtiAssessmentItemPlayer | null,
249
+ ): QtiAssessmentItemPlayer {
250
+ const element = getElement();
251
+ if (!element) throw new Error("QTI assessment item player element is not mounted.");
252
+ return element;
253
+ }
@@ -0,0 +1,14 @@
1
+ /** True when dev warnings are enabled (non-production Node, or browser without NODE_ENV). */
2
+ export function playerDevWarningsEnabled(): boolean {
3
+ const nodeEnv = (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process?.env
4
+ ?.NODE_ENV;
5
+ return nodeEnv !== "production";
6
+ }
7
+
8
+ const warnedKeys = new Set<string>();
9
+
10
+ export function warnPlayerMessageOnce(code: string, message: string): void {
11
+ if (!playerDevWarningsEnabled() || warnedKeys.has(code)) return;
12
+ warnedKeys.add(code);
13
+ console.warn(`[qti3-player] ${message}`);
14
+ }