@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
@@ -1,7 +1,7 @@
1
1
  import type { QtiChoice, QtiInteraction, QtiValue } from "@longsightgroup/qti3-core";
2
2
  import { removeButton } from "../controls/remove-button.js";
3
3
  import { missingChoicesMessage, responseGroup, valueToStrings } from "../interaction-support.js";
4
- import type { QtiPlayerMessages } from "../player-messages.js";
4
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
5
5
  import { parseUnlimitedMaximum } from "../response-limits.js";
6
6
  import { choiceText, sourceChoices, targetChoices, tokenButton, tokenRegion } from "./shared.js";
7
7
 
@@ -9,7 +9,7 @@ export function renderMatchResponse(
9
9
  interaction: QtiInteraction,
10
10
  update: (value: QtiValue) => void,
11
11
  currentValue: QtiValue,
12
- messages: QtiPlayerMessages,
12
+ messages: PlayerMessageResolver,
13
13
  ): HTMLElement {
14
14
  const group = responseGroup();
15
15
 
@@ -26,13 +26,13 @@ export function renderMatchResponse(
26
26
 
27
27
  const selector = document.createElement("div");
28
28
  selector.className = "qti3-match-selector";
29
- const sourceRegion = tokenRegion("Match sources");
29
+ const sourceRegion = tokenRegion(messages.message("matchSourcesBank"));
30
30
  sourceRegion.classList.add("qti3-match-source-bank");
31
- const targetRegion = tokenRegion("Match targets");
31
+ const targetRegion = tokenRegion(messages.message("matchTargetsBank"));
32
32
  targetRegion.classList.add("qti3-match-target-bank");
33
33
  const pairList = document.createElement("ul");
34
34
  pairList.className = "qti3-pair-list";
35
- pairList.setAttribute("aria-label", "Match selected pairs");
35
+ pairList.setAttribute("aria-label", messages.message("matchSelectedPairsList"));
36
36
 
37
37
  const commit = () => {
38
38
  if (interaction.responseCardinality === "single") update(selectedPairs[0] ?? null);
@@ -68,7 +68,10 @@ export function renderMatchResponse(
68
68
  pairList.replaceChildren(
69
69
  ...selectedPairs.map((pair) => {
70
70
  const [source, target] = pair.split(" ");
71
- const label = `${choiceText(sources, source)} to ${choiceText(targets, target)}`;
71
+ const label = messages.message("associationPairLabel", {
72
+ source: choiceText(sources, source),
73
+ target: choiceText(targets, target),
74
+ });
72
75
  const item = document.createElement("li");
73
76
  item.className = "qti3-pair-chip";
74
77
  const text = document.createElement("span");
@@ -1,12 +1,7 @@
1
1
  import type { QtiChoice, QtiInteraction, QtiValue } from "@longsightgroup/qti3-core";
2
2
  import { removeButton } from "../controls/remove-button.js";
3
- import {
4
- missingChoicesMessage,
5
- readableType,
6
- responseGroup,
7
- valueToStrings,
8
- } from "../interaction-support.js";
9
- import type { QtiPlayerMessages } from "../player-messages.js";
3
+ import { missingChoicesMessage, responseGroup, valueToStrings } from "../interaction-support.js";
4
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
10
5
  import {
11
6
  choiceText,
12
7
  pairRegionLabels,
@@ -20,7 +15,7 @@ export function renderPairResponse(
20
15
  interaction: QtiInteraction,
21
16
  update: (value: QtiValue) => void,
22
17
  currentValue: QtiValue,
23
- messages: QtiPlayerMessages,
18
+ messages: PlayerMessageResolver,
24
19
  ): HTMLElement {
25
20
  const group = responseGroup();
26
21
 
@@ -33,15 +28,24 @@ export function renderPairResponse(
33
28
  const selectedPairs: string[] = valueToStrings(currentValue);
34
29
  let selectedSource: QtiChoice | undefined;
35
30
  let selectedTarget: QtiChoice | undefined;
36
- const labels = pairRegionLabels(interaction);
31
+ const labels = pairRegionLabels(interaction, messages);
37
32
 
38
- const sourceRegion = tokenRegion(`${readableType(interaction.type)} sources`, labels.source);
39
- const targetRegion = tokenRegion(`${readableType(interaction.type)} targets`, labels.target);
33
+ const sourceRegion = tokenRegion(
34
+ messages.message("interactionSourcesBank", { type: interaction.type }),
35
+ labels.source,
36
+ );
37
+ const targetRegion = tokenRegion(
38
+ messages.message("interactionTargetsBank", { type: interaction.type }),
39
+ labels.target,
40
+ );
40
41
  const selector = document.createElement("div");
41
42
  selector.className = "qti3-pair-selector";
42
43
  const pairList = document.createElement("ul");
43
44
  pairList.className = "qti3-pair-list";
44
- pairList.setAttribute("aria-label", `${readableType(interaction.type)} selected pairs`);
45
+ pairList.setAttribute(
46
+ "aria-label",
47
+ messages.message("interactionSelectedPairsList", { type: interaction.type }),
48
+ );
45
49
  let draggedSource: string | undefined;
46
50
 
47
51
  const commit = () => {
@@ -84,11 +88,15 @@ export function renderPairResponse(
84
88
  pairList.replaceChildren(
85
89
  ...selectedPairs.map((pair) => {
86
90
  const [source, target] = pair.split(" ");
91
+ const label = messages.message("associationPairLabel", {
92
+ source: choiceText(sources, source),
93
+ target: choiceText(targets, target),
94
+ });
87
95
  const item = document.createElement("li");
88
96
  item.className = "qti3-pair-chip";
89
97
  const text = document.createElement("span");
90
- text.textContent = `${choiceText(sources, source)} to ${choiceText(targets, target)}`;
91
- const remove = removeButton(text.textContent, messages);
98
+ text.textContent = label;
99
+ const remove = removeButton(label, messages);
92
100
  remove.addEventListener("click", () => {
93
101
  const index = selectedPairs.indexOf(pair);
94
102
  if (index >= 0) selectedPairs.splice(index, 1);
@@ -6,11 +6,10 @@ import {
6
6
  appendGraphicObjectImage,
7
7
  objectIsImage,
8
8
  percent,
9
- readableType,
10
9
  responseGroup,
11
10
  } from "../interaction-support.js";
12
11
  import { movementButton } from "../movement.js";
13
- import type { QtiPlayerMessages } from "../player-messages.js";
12
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
14
13
  import {
15
14
  objectAssetHeight,
16
15
  objectAssetWidth,
@@ -22,11 +21,14 @@ export function renderPositionObjectResponse(
22
21
  interaction: QtiInteraction,
23
22
  update: (value: QtiValue) => void,
24
23
  currentValue: QtiValue,
25
- messages: QtiPlayerMessages,
24
+ messages: PlayerMessageResolver,
26
25
  ): HTMLElement {
27
26
  const group = responseGroup();
28
27
  group.role = "group";
29
- group.setAttribute("aria-label", `${readableType(interaction.type)} object placement response`);
28
+ group.setAttribute(
29
+ "aria-label",
30
+ messages.message("interactionPlacementResponse", { type: interaction.type }),
31
+ );
30
32
 
31
33
  const stageObject = interaction.positionObjectStage ?? interaction.object;
32
34
  const movableObject = interaction.positionObjectStage ? interaction.object : undefined;
@@ -43,7 +45,10 @@ export function renderPositionObjectResponse(
43
45
  stage.style.setProperty("--qti3-position-object-marker-block-size", `${movableHeight}px`);
44
46
  stage.tabIndex = 0;
45
47
  stage.role = "group";
46
- stage.setAttribute("aria-label", `${readableType(interaction.type)} placement stage`);
48
+ stage.setAttribute(
49
+ "aria-label",
50
+ messages.message("interactionPlacementStage", { type: interaction.type }),
51
+ );
47
52
 
48
53
  if (stageObject?.data && objectIsImage(stageObject)) {
49
54
  appendGraphicObjectImage(stage, stageObject, stageObject.text || "");
@@ -52,7 +57,7 @@ export function renderPositionObjectResponse(
52
57
  const marker = document.createElement("button");
53
58
  marker.type = "button";
54
59
  marker.className = "qti3-position-object-marker";
55
- marker.setAttribute("aria-label", messages.movableObject());
60
+ marker.setAttribute("aria-label", messages.message("movableObject"));
56
61
  applyPositionObjectMarkerSize(marker, movableWidth, movableHeight);
57
62
  marker.draggable = false;
58
63
 
@@ -62,12 +67,13 @@ export function renderPositionObjectResponse(
62
67
  image.alt = "";
63
68
  marker.append(image);
64
69
  } else {
65
- marker.textContent = messages.placeObject();
70
+ marker.textContent = messages.message("placeObject");
66
71
  }
67
72
  stage.append(marker);
68
73
 
69
74
  const coordinate = document.createElement("output");
70
- coordinate.className = "qti3-coordinate-output";
75
+ coordinate.className = "qti3-coordinate-output qti-visually-hidden";
76
+ coordinate.setAttribute("aria-live", "polite");
71
77
  const clamp = () => {
72
78
  point.x = Math.max(0, Math.min(width, point.x));
73
79
  point.y = Math.max(0, Math.min(height, point.y));
@@ -90,10 +96,10 @@ export function renderPositionObjectResponse(
90
96
  `calc(100% + ${Math.round(movableHeight / 2 + 8)}px)`,
91
97
  );
92
98
  coordinate.value = "";
93
- coordinate.textContent = "Object not placed";
99
+ coordinate.textContent = messages.message("objectNotPlaced");
94
100
  stage.setAttribute(
95
101
  "aria-label",
96
- `${readableType(interaction.type)} placement stage, object not placed`,
102
+ messages.message("interactionPlacementStageEmpty", { type: interaction.type }),
97
103
  );
98
104
  return;
99
105
  }
@@ -107,10 +113,11 @@ export function renderPositionObjectResponse(
107
113
  `${percent(point.y, height)}%`,
108
114
  );
109
115
  coordinate.value = pointToString(point);
110
- coordinate.textContent = `Object positioned at ${pointToString(point)}`;
116
+ const coordinates = pointToString(point);
117
+ coordinate.textContent = messages.message("objectPositionedAt", { coordinates });
111
118
  stage.setAttribute(
112
119
  "aria-label",
113
- `${readableType(interaction.type)} placement stage, object at ${pointToString(point)}`,
120
+ messages.message("interactionPlacementStageAt", { type: interaction.type, coordinates }),
114
121
  );
115
122
  };
116
123
  const pointFromPointer = (event: MouseEvent | PointerEvent) => {
@@ -200,7 +207,9 @@ export function renderPositionObjectResponse(
200
207
  ["down", 0, 1],
201
208
  ] as const) {
202
209
  controls.append(
203
- movementButton(direction, messages.moveObject({ direction }), () => moveBy(dx, dy)),
210
+ movementButton(direction, messages.message("moveObject", { direction }), () =>
211
+ moveBy(dx, dy),
212
+ ),
204
213
  );
205
214
  }
206
215
 
@@ -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
  }
@@ -36,7 +36,18 @@ export function isFeedbackVisible(
36
36
  return node.showHide === "show" ? hasIdentifier : !hasIdentifier;
37
37
  }
38
38
 
39
- export function isTemplateContentVisible(element: HTMLElement, value: QtiValue): boolean {
39
+ interface TemplateContentVisibilityElement {
40
+ dataset: {
41
+ templateIdentifier?: string;
42
+ templateValueIdentifier?: string;
43
+ showHide?: string;
44
+ };
45
+ }
46
+
47
+ export function isTemplateContentVisible(
48
+ element: TemplateContentVisibilityElement,
49
+ value: QtiValue,
50
+ ): boolean {
40
51
  const templateIdentifier = element.dataset.templateIdentifier;
41
52
  const identifier = element.dataset.templateValueIdentifier;
42
53
  if (!templateIdentifier || !identifier) return true;
@@ -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
  }