@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,5 +1,4 @@
1
1
  import type { QtiAssessmentItem, QtiAttemptStateV1, QtiAttemptStatus, QtiDiagnostic, QtiInteraction, QtiPortableCustomDefinition, QtiPortableCustomStateValue, QtiScoreResult, QtiValue } from "@longsightgroup/qti3-core";
2
- import type { QtiPlayerMessages } from "./player-messages.js";
3
2
  export interface QtiPlayerSessionControl {
4
3
  validateResponses?: boolean | undefined;
5
4
  showFeedback?: boolean | undefined;
@@ -16,7 +15,6 @@ export interface QtiPlayerLoadOptions {
16
15
  fetchXml?: QtiPlayerFetchXml | undefined;
17
16
  resolveAsset?: QtiPlayerResolveAsset | undefined;
18
17
  }
19
- export type QtiPlayerMessageOverrides = Partial<QtiPlayerMessages>;
20
18
  export interface QtiReadyEventDetail {
21
19
  item: QtiAssessmentItem;
22
20
  }
@@ -53,6 +51,15 @@ export interface QtiSuspendEventDetail {
53
51
  export interface QtiEndAttemptEventDetail {
54
52
  state: QtiAttemptStateV1;
55
53
  }
54
+ export interface QtiDiagnosticsEventDetail {
55
+ diagnostics: QtiDiagnostic[];
56
+ }
57
+ export interface QtiResetEventDetail {
58
+ state?: QtiAttemptStateV1 | undefined;
59
+ }
60
+ export interface QtiRestoreEventDetail {
61
+ state?: QtiAttemptStateV1 | undefined;
62
+ }
56
63
  export interface QtiAssessmentItemPlayerEventDetailMap {
57
64
  "qti-ready": QtiReadyEventDetail;
58
65
  "qti-statechange": QtiStateChangeEventDetail;
@@ -62,6 +69,9 @@ export interface QtiAssessmentItemPlayerEventDetailMap {
62
69
  "qti-validation": QtiValidationEventDetail;
63
70
  "qti-suspend": QtiSuspendEventDetail;
64
71
  "qti-endattempt": QtiEndAttemptEventDetail;
72
+ "qti-diagnostics": QtiDiagnosticsEventDetail;
73
+ "qti-reset": QtiResetEventDetail;
74
+ "qti-restore": QtiRestoreEventDetail;
65
75
  }
66
76
  export type QtiAssessmentItemPlayerEventName = keyof QtiAssessmentItemPlayerEventDetailMap;
67
77
  export type QtiAssessmentItemPlayerEvent<T extends QtiAssessmentItemPlayerEventName = QtiAssessmentItemPlayerEventName> = CustomEvent<QtiAssessmentItemPlayerEventDetailMap[T]>;
@@ -1 +1 @@
1
- {"version":3,"file":"player-types.d.ts","sourceRoot":"","sources":["../src/player-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,2BAA2B,EAC3B,2BAA2B,EAC3B,cAAc,EACd,QAAQ,EACT,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,MAAM,WAAW,uBAAuB;IACtC,iBAAiB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACxC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACpC;AAED,MAAM,WAAW,sBAAsB;IACrC,iBAAiB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACzC;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AACjE,MAAM,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;AAE5D,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACtC,MAAM,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACtC,cAAc,CAAC,EAAE,uBAAuB,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACzC,YAAY,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC;CAClD;AAED,MAAM,MAAM,yBAAyB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAEnE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,iBAAiB,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,KAAK,EAAE,iBAAiB,CAAC;CAC1B;AAED,MAAM,WAAW,4BAA4B;IAC3C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,QAAQ,CAAC;CACjB;AAED,MAAM,WAAW,iCAAiC;IAChD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,cAAc,CAAC;IAC5B,UAAU,EAAE,2BAA2B,CAAC;IACxC,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,2BAA2B,GAAG,SAAS,CAAC;CACjD;AAED,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAEjD,MAAM,WAAW,wBAAwB;IACvC,qFAAqF;IACrF,kBAAkB,EAAE,aAAa,EAAE,CAAC;IACpC,KAAK,EAAE,iBAAiB,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,iBAAiB,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAC;CAC1B;AAED,MAAM,WAAW,qCAAqC;IACpD,WAAW,EAAE,mBAAmB,CAAC;IACjC,iBAAiB,EAAE,yBAAyB,CAAC;IAC7C,oBAAoB,EAAE,4BAA4B,CAAC;IACnD,2BAA2B,EAAE,iCAAiC,CAAC;IAC/D,WAAW,EAAE,mBAAmB,CAAC;IACjC,gBAAgB,EAAE,wBAAwB,CAAC;IAC3C,aAAa,EAAE,qBAAqB,CAAC;IACrC,gBAAgB,EAAE,wBAAwB,CAAC;CAC5C;AAED,MAAM,MAAM,gCAAgC,GAAG,MAAM,qCAAqC,CAAC;AAE3F,MAAM,MAAM,4BAA4B,CACtC,CAAC,SAAS,gCAAgC,GAAG,gCAAgC,IAC3E,WAAW,CAAC,qCAAqC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE1D,MAAM,MAAM,qCAAqC,GAAG;KACjD,CAAC,IAAI,gCAAgC,GAAG,WAAW,CAAC,qCAAqC,CAAC,CAAC,CAAC,CAAC;CAC/F,CAAC"}
1
+ {"version":3,"file":"player-types.d.ts","sourceRoot":"","sources":["../src/player-types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longsightgroup/qti3-player",
3
- "version": "0.3.0",
3
+ "version": "0.5.1",
4
4
  "description": "Style-neutral web component player for rendering and scoring QTI 3 assessment items.",
5
5
  "keywords": [
6
6
  "assessment",
@@ -45,8 +45,8 @@
45
45
  "registry": "https://registry.npmjs.org"
46
46
  },
47
47
  "dependencies": {
48
- "@longsightgroup/qti3-fixtures": "0.3.0",
49
- "@longsightgroup/qti3-core": "0.3.0"
48
+ "@longsightgroup/qti3-core": "0.5.1",
49
+ "@longsightgroup/qti3-fixtures": "0.5.1"
50
50
  },
51
51
  "scripts": {}
52
52
  }
@@ -1,13 +1,16 @@
1
1
  import { trashIcon } from "../icons.js";
2
- import type { QtiPlayerMessages } from "../player-messages.js";
2
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
3
3
 
4
- export function removeButton(label: string | null, messages: QtiPlayerMessages): HTMLButtonElement {
5
- const safeLabel = label?.trim() || messages.remove();
4
+ export function removeButton(
5
+ label: string | null,
6
+ messages: PlayerMessageResolver,
7
+ ): HTMLButtonElement {
8
+ const safeLabel = label?.trim() || messages.message("remove");
6
9
  const button = document.createElement("button");
7
10
  button.type = "button";
8
11
  button.className = "qti3-icon-button qti3-remove-button";
9
- button.title = messages.remove();
10
- button.setAttribute("aria-label", messages.removePair({ label: safeLabel }));
12
+ button.title = messages.message("remove");
13
+ button.setAttribute("aria-label", messages.message("removePair", { label: safeLabel }));
11
14
  button.append(trashIcon());
12
15
  return button;
13
16
  }
package/src/index.ts CHANGED
@@ -1,26 +1,82 @@
1
+ export type { QtiPlayerMovementDirection } from "./player-messages.js";
1
2
  export type {
2
- QtiPlayerAssociationPairLabelParams,
3
- QtiPlayerMessages,
4
- QtiPlayerRemoveMessageParams,
5
- } from "./player-messages.js";
3
+ QtiAttemptStateV1,
4
+ QtiCatalogSupportResolution,
5
+ QtiCatalogSupportResolutionOptions,
6
+ QtiScoreResult,
7
+ QtiTextToSpeechTraversal,
8
+ } from "@longsightgroup/qti3-core";
6
9
  export type {
7
10
  QtiAssessmentItemPlayerCustomEventMap,
8
11
  QtiAssessmentItemPlayerEvent,
9
12
  QtiAssessmentItemPlayerEventDetailMap,
10
13
  QtiAssessmentItemPlayerEventName,
14
+ QtiDiagnosticsEventDetail,
11
15
  QtiEndAttemptEventDetail,
12
16
  QtiPlayerFetchXml,
13
17
  QtiPlayerLoadOptions,
14
- QtiPlayerMessageOverrides,
15
18
  QtiPlayerResolveAsset,
16
19
  QtiPlayerSessionControl,
17
20
  QtiPortableCustomMountEventDetail,
18
21
  QtiReadyEventDetail,
19
22
  QtiResponseChangeEventDetail,
23
+ QtiResetEventDetail,
24
+ QtiRestoreEventDetail,
20
25
  QtiScoreAttemptOptions,
21
26
  QtiScoreEventDetail,
22
27
  QtiStateChangeEventDetail,
23
28
  QtiSuspendEventDetail,
24
29
  QtiValidationEventDetail,
25
30
  } from "./player-types.js";
31
+ export type {
32
+ QtiAssessmentItemPlayerAdapterEventCallback,
33
+ QtiAssessmentItemPlayerAdapterEventHandlerProps,
34
+ QtiAssessmentItemPlayerAdapterEventPropName,
35
+ QtiAssessmentItemPlayerAdapterLoadSyncInput,
36
+ QtiAssessmentItemPlayerAdapterProps,
37
+ QtiAssessmentItemPlayerAdapterPropName,
38
+ QtiAssessmentItemPlayerHandle,
39
+ QtiAssessmentItemPlayerLoadDependencies,
40
+ } from "./player-adapter.js";
41
+ export {
42
+ bindQtiAssessmentItemPlayerAdapterEvents,
43
+ createQtiAssessmentItemPlayerAdapterLoadSync,
44
+ createQtiAssessmentItemPlayerHandle,
45
+ isQtiAssessmentItemPlayerAdapterPropName,
46
+ normalizeQtiAssessmentItemPlayerLoadError,
47
+ qtiAssessmentItemPlayerAdapterEventEntries,
48
+ qtiAssessmentItemPlayerAdapterPropNames,
49
+ qtiAssessmentItemPlayerLoadDependencies,
50
+ qtiAssessmentItemPlayerLoadStateKey,
51
+ syncQtiAssessmentItemPlayerAdapterChrome,
52
+ syncQtiAssessmentItemPlayerAdapterMessages,
53
+ } from "./player-adapter.js";
54
+ export type { PlayerMessageCatalog } from "./player-message-catalog.js";
55
+ export type { PlayerMessageKey, PlayerMessageResolverKind } from "./player-message-manifest.js";
56
+ export { PLAYER_MESSAGE_MANIFEST } from "./player-message-manifest.js";
57
+ export {
58
+ createPlayerMessageResolver,
59
+ defaultPlayerMessageCatalog,
60
+ defaultPlayerMessageResolver,
61
+ extractMessagePlaceholders,
62
+ formatPlayerMessage,
63
+ mergePlayerMessageCatalogs,
64
+ type PlayerMessageOverride,
65
+ type PlayerMessageParams,
66
+ type PlayerMessageResolver,
67
+ type QtiPlayerMessageOverrides,
68
+ } from "./player-message-catalog.js";
69
+ export { PLAYER_MESSAGE_KEYS, PLAYER_MESSAGE_STRING_KEYS } from "./player-message-keys.js";
70
+ export type {
71
+ PlayerMessageCatalogDiagnostic,
72
+ PlayerMessageCatalogDiagnosticCode,
73
+ PlayerMessageCatalogValidationResult,
74
+ ValidatePlayerMessageCatalogOptions,
75
+ } from "./player-message-catalog-validate.js";
76
+ export {
77
+ allowedCatalogPlaceholders,
78
+ requiredCatalogPlaceholders,
79
+ validatePlayerMessageCatalog,
80
+ } from "./player-message-catalog-validate.js";
81
+ export { resolvePlayerMessages } from "./player-locale.js";
26
82
  export { QtiAssessmentItemPlayer, defineQtiAssessmentItemPlayer } from "./player-element.js";
@@ -2,10 +2,10 @@ import type { QtiInteraction, QtiValue } from "@longsightgroup/qti3-core";
2
2
  import {
3
3
  interactionChoices,
4
4
  missingChoicesMessage,
5
- readableType,
6
5
  responseGroup,
7
6
  valueToStrings,
8
7
  } from "../interaction-support.js";
8
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
9
9
 
10
10
  function choicePresentationLabel(interaction: QtiInteraction, index: number): string {
11
11
  const classNames = new Set((interaction.attributes.class ?? "").split(/\s+/).filter(Boolean));
@@ -28,6 +28,7 @@ export function renderChoice(
28
28
  interaction: QtiInteraction,
29
29
  update: (value: QtiValue) => void,
30
30
  currentValue: QtiValue,
31
+ messages: PlayerMessageResolver,
31
32
  ): HTMLElement {
32
33
  const group = responseGroup("qti3-choice-group");
33
34
 
@@ -37,7 +38,10 @@ export function renderChoice(
37
38
  const list = document.createElement("div");
38
39
  list.className = "qti3-choice-list";
39
40
  list.role = "group";
40
- list.setAttribute("aria-label", `${readableType(interaction.type)} options`);
41
+ list.setAttribute(
42
+ "aria-label",
43
+ messages.message("interactionOptionsList", { type: interaction.type }),
44
+ );
41
45
  const syncSelected = () => {
42
46
  for (const label of list.querySelectorAll<HTMLElement>(".qti3-choice-option")) {
43
47
  const identifier = label.dataset.choiceIdentifier ?? "";
@@ -1,6 +1,6 @@
1
1
  import type { QtiInteraction, QtiObjectAsset, QtiValue } from "@longsightgroup/qti3-core";
2
- import { applyResponsiveGraphicSize, objectIsImage, readableType } from "../interaction-support.js";
3
- import type { QtiPlayerMessages } from "../player-messages.js";
2
+ import { applyResponsiveGraphicSize, objectIsImage } from "../interaction-support.js";
3
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
4
4
 
5
5
  export const DRAWING_STROKE_COLOR = "#000";
6
6
  export const DRAWING_STROKE_WIDTH = 3;
@@ -9,16 +9,19 @@ export function renderDrawingResponse(
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 = document.createElement("div");
15
15
  group.role = "group";
16
- group.setAttribute("aria-label", `${readableType(interaction.type)} response`);
16
+ group.setAttribute(
17
+ "aria-label",
18
+ messages.message("interactionDrawingResponse", { type: interaction.type }),
19
+ );
17
20
 
18
21
  const surface = document.createElementNS("http://www.w3.org/2000/svg", "svg");
19
22
  surface.classList.add("qti3-drawing-surface");
20
23
  surface.setAttribute("role", "img");
21
- surface.setAttribute("aria-label", "Drawing response surface");
24
+ surface.setAttribute("aria-label", messages.message("drawingSurface"));
22
25
  surface.setAttribute("tabindex", "0");
23
26
  const width = drawingWidth(interaction);
24
27
  const height = drawingHeight(interaction);
@@ -68,12 +71,14 @@ export function renderDrawingResponse(
68
71
  const count = strokes.length;
69
72
  summary.value = serializeDrawingStrokes(strokes);
70
73
  summary.textContent =
71
- count === 0 ? "No drawing strokes." : `${count} drawing stroke${count === 1 ? "" : "s"}.`;
74
+ count === 0
75
+ ? messages.message("drawingStatusEmpty")
76
+ : messages.message("drawingStatusStrokeCount", { count });
72
77
  surface.setAttribute(
73
78
  "aria-label",
74
79
  count === 0
75
- ? "Drawing response surface, no strokes"
76
- : `Drawing response surface, ${count} stroke${count === 1 ? "" : "s"}`,
80
+ ? messages.message("drawingSurfaceEmpty")
81
+ : messages.message("drawingSurfaceStrokeCount", { count }),
77
82
  );
78
83
  };
79
84
  for (const points of restoredStrokes) {
@@ -127,7 +132,7 @@ export function renderDrawingResponse(
127
132
 
128
133
  const clear = document.createElement("button");
129
134
  clear.type = "button";
130
- clear.textContent = messages.clearDrawing();
135
+ clear.textContent = messages.message("clearDrawing");
131
136
  clear.addEventListener("click", () => {
132
137
  strokes.splice(0, strokes.length);
133
138
  activeStroke = undefined;
@@ -1,16 +1,16 @@
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
 
4
4
  export function renderEndAttemptResponse(
5
5
  interaction: QtiInteraction,
6
6
  update: (value: QtiValue) => void,
7
7
  endAttempt: () => void,
8
- messages: QtiPlayerMessages,
8
+ messages: PlayerMessageResolver,
9
9
  ): HTMLElement {
10
10
  const button = document.createElement("button");
11
11
  button.type = "button";
12
12
  button.className = "qti3-end-attempt-button";
13
- button.textContent = interaction.attributes.title ?? messages.endAttempt();
13
+ button.textContent = interaction.attributes.title ?? messages.message("endAttempt");
14
14
  button.addEventListener("click", () => {
15
15
  if (interaction.responseIdentifier) update(true);
16
16
  endAttempt();
@@ -6,10 +6,10 @@ import {
6
6
  objectHeight,
7
7
  objectWidth,
8
8
  placeHotspotButton,
9
- readableType,
10
9
  responseGroup,
11
10
  valueToStrings,
12
11
  } from "../interaction-support.js";
12
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
13
13
  import { parseUnlimitedMaximum } from "../response-limits.js";
14
14
  import { appendGraphicContext } from "./graphic-context.js";
15
15
  import { appendInlineControl, normalizeInlineSegmentText } from "./inline-controls.js";
@@ -33,13 +33,14 @@ export function renderGapMatchResponse(
33
33
  interaction: QtiInteraction,
34
34
  update: (value: QtiValue) => void,
35
35
  currentValue: QtiValue,
36
+ messages: PlayerMessageResolver,
36
37
  ): HTMLElement {
37
38
  if (
38
39
  interaction.type === "graphicGapMatch" &&
39
40
  interaction.object &&
40
41
  interaction.choices.some((choice) => choice.role === "hotspot")
41
42
  ) {
42
- return renderGraphicGapMatchResponse(interaction, update, currentValue);
43
+ return renderGraphicGapMatchResponse(interaction, update, currentValue, messages);
43
44
  }
44
45
 
45
46
  const group = responseGroup();
@@ -54,11 +55,16 @@ export function renderGapMatchResponse(
54
55
  let selectedSource: QtiChoice | undefined;
55
56
  let draggedSource: string | undefined;
56
57
 
57
- const sourceRegion = tokenRegion(`${readableType(interaction.type)} choices`);
58
+ const sourceRegion = tokenRegion(
59
+ messages.message("interactionChoicesBank", { type: interaction.type }),
60
+ );
58
61
  const gapRegion = document.createElement("div");
59
62
  gapRegion.className = "qti3-gap-region qti3-gap-passage";
60
63
  gapRegion.role = "group";
61
- gapRegion.setAttribute("aria-label", `${readableType(interaction.type)} targets`);
64
+ gapRegion.setAttribute(
65
+ "aria-label",
66
+ messages.message("interactionGapTargets", { type: interaction.type }),
67
+ );
62
68
  for (const pair of valueToStrings(currentValue)) {
63
69
  const [sourceIdentifier, gapIdentifier] = pair.split(/\s+/);
64
70
  const source = sources.find((choice) => choice.identifier === sourceIdentifier);
@@ -91,7 +97,7 @@ export function renderGapMatchResponse(
91
97
  };
92
98
  const gapControl = (gap: QtiChoice, index: number) => {
93
99
  const assigned = assignments.get(gap.identifier);
94
- const gapLabel = `Gap ${index + 1}`;
100
+ const gapLabel = messages.message("gapLabel", { index: index + 1 });
95
101
  const target = document.createElement("span");
96
102
  target.className = "qti3-gap-target";
97
103
  target.dataset.gapIdentifier = gap.identifier;
@@ -112,7 +118,9 @@ export function renderGapMatchResponse(
112
118
  button.textContent = assigned ? assigned.text : "";
113
119
  button.setAttribute(
114
120
  "aria-label",
115
- assigned ? `${gapLabel}, assigned ${assigned.text}` : `${gapLabel}, empty`,
121
+ assigned
122
+ ? messages.message("gapAssignedState", { label: gapLabel, assigned: assigned.text })
123
+ : messages.message("gapEmptyState", { label: gapLabel }),
116
124
  );
117
125
  button.addEventListener("click", () => assign(gap, selectedSource?.identifier));
118
126
  button.addEventListener("keydown", (event) => {
@@ -173,6 +181,7 @@ function renderGraphicGapMatchResponse(
173
181
  interaction: QtiInteraction,
174
182
  update: (value: QtiValue) => void,
175
183
  currentValue: QtiValue,
184
+ messages: PlayerMessageResolver,
176
185
  ): HTMLElement {
177
186
  const group = responseGroup();
178
187
  const width = objectWidth(interaction);
@@ -202,7 +211,10 @@ function renderGraphicGapMatchResponse(
202
211
  "qti3-graphic-gap-match-surface",
203
212
  );
204
213
  surface.role = "group";
205
- surface.setAttribute("aria-label", `${readableType(interaction.type)} target image`);
214
+ surface.setAttribute(
215
+ "aria-label",
216
+ messages.message("interactionTargetImage", { type: interaction.type }),
217
+ );
206
218
  surface.style.overflow = "visible";
207
219
  surface.style.setProperty(
208
220
  "--qti3-graphic-gap-label-block-size",
@@ -213,11 +225,14 @@ function renderGraphicGapMatchResponse(
213
225
  appendGraphicObjectImage(
214
226
  surface,
215
227
  interaction.object,
216
- interaction.object.text || `${readableType(interaction.type)} image`,
228
+ interaction.object.text ||
229
+ messages.message("interactionImageAlt", { type: interaction.type }),
217
230
  );
218
231
  }
219
232
 
220
- const sourceRegion = tokenRegion(`${readableType(interaction.type)} choices`);
233
+ const sourceRegion = tokenRegion(
234
+ messages.message("interactionChoicesBank", { type: interaction.type }),
235
+ );
221
236
  sourceRegion.classList.add("qti3-graphic-gap-source-region");
222
237
  const choicesWidth = positivePixelValue(interaction.attributes["data-choices-container-width"]);
223
238
  if (choicesWidth !== undefined) sourceRegion.style.maxInlineSize = `${choicesWidth}px`;
@@ -260,7 +275,9 @@ function renderGraphicGapMatchResponse(
260
275
  commit();
261
276
  };
262
277
  const targetLabel = (gap: QtiChoice, index: number) =>
263
- gap.attributes["aria-label"] || gap.attributes["hotspot-label"] || `Target ${index + 1}`;
278
+ gap.attributes["aria-label"] ||
279
+ gap.attributes["hotspot-label"] ||
280
+ messages.message("graphicGapTargetLabel", { index: index + 1 });
264
281
  const renderTargetButton = (gap: QtiChoice, index: number): HTMLButtonElement => {
265
282
  const assigned = assignments.get(gap.identifier);
266
283
  const label = targetLabel(gap, index);
@@ -271,7 +288,9 @@ function renderGraphicGapMatchResponse(
271
288
  button.dataset.selected = assigned ? "true" : "false";
272
289
  button.setAttribute(
273
290
  "aria-label",
274
- assigned ? `${label}, assigned ${assigned.text}` : `${label}, empty`,
291
+ assigned
292
+ ? messages.message("gapAssignedState", { label, assigned: assigned.text })
293
+ : messages.message("gapEmptyState", { label }),
275
294
  );
276
295
  button.addEventListener("dragover", (event) => {
277
296
  event.preventDefault();
@@ -308,8 +327,8 @@ function renderGraphicGapMatchResponse(
308
327
  }
309
328
  summary.textContent =
310
329
  assignments.size > 0
311
- ? `${assignments.size} ${assignments.size === 1 ? "label" : "labels"} placed.`
312
- : "No labels placed.";
330
+ ? messages.message("gapLabelsPlacedCount", { count: assignments.size })
331
+ : messages.message("gapNoLabelsPlaced");
313
332
  };
314
333
 
315
334
  for (const source of sources) {
@@ -11,18 +11,17 @@ import {
11
11
  objectHeight,
12
12
  objectWidth,
13
13
  placeHotspotButton,
14
- readableType,
15
14
  responseGroup,
16
15
  valueToStrings,
17
16
  } from "../interaction-support.js";
18
- import type { QtiPlayerMessages } from "../player-messages.js";
17
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
19
18
  import { exceedsHotspotMatchMax, maximumAllowedResponses } from "../response-limits.js";
20
19
 
21
20
  export function renderGraphicAssociateResponse(
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
 
@@ -47,14 +46,17 @@ export function renderGraphicAssociateResponse(
47
46
  const surface = document.createElement("div");
48
47
  applyGraphicSurfaceLayout(surface, width, height, "qti3-graphic-associate-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
 
@@ -69,7 +71,10 @@ export function renderGraphicAssociateResponse(
69
71
  summary.setAttribute("aria-live", "polite");
70
72
  const pairList = document.createElement("ul");
71
73
  pairList.className = "qti3-pair-list";
72
- pairList.setAttribute("aria-label", `${readableType(interaction.type)} selected pairs`);
74
+ pairList.setAttribute(
75
+ "aria-label",
76
+ messages.message("interactionSelectedPairsList", { type: interaction.type }),
77
+ );
73
78
 
74
79
  const commit = () => {
75
80
  if (interaction.responseCardinality === "single") update(selectedPairs[0] ?? null);
@@ -214,18 +219,18 @@ export function renderGraphicAssociateResponse(
214
219
  button.dataset.selected = isActive || isPaired ? "true" : "false";
215
220
  }
216
221
  summary.textContent = selectedHotspot
217
- ? messages.hotspotSelectedChooseAnother({
222
+ ? messages.message("hotspotSelectedChooseAnother", {
218
223
  label: hotspotDisplayLabel(selectedHotspot, choices),
219
224
  })
220
225
  : selectedPairs.length > 0
221
- ? messages.associationsMade({ count: selectedPairs.length })
222
- : messages.noAssociationsMade();
226
+ ? messages.message("associationsMade", { count: selectedPairs.length })
227
+ : messages.message("noAssociationsMade");
223
228
  pairList.replaceChildren(
224
229
  ...selectedPairs.map((pair) => {
225
230
  const [source = "", target = ""] = pair.split(" ");
226
231
  const sourceChoice = choices.find((choice) => choice.identifier === source);
227
232
  const targetChoice = choices.find((choice) => choice.identifier === target);
228
- const pairLabel = messages.associationPairLabel({
233
+ const pairLabel = messages.message("associationPairLabel", {
229
234
  source: sourceChoice ? hotspotDisplayLabel(sourceChoice, choices) : source,
230
235
  target: targetChoice ? hotspotDisplayLabel(targetChoice, choices) : target,
231
236
  });
@@ -7,17 +7,16 @@ import {
7
7
  objectHeight,
8
8
  objectWidth,
9
9
  placeHotspotButton,
10
- readableType,
11
10
  responseGroup,
12
11
  valueToStrings,
13
12
  } from "../interaction-support.js";
14
- import type { QtiPlayerMessages } from "../player-messages.js";
13
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
15
14
 
16
15
  export function renderHotspotResponse(
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
 
@@ -37,7 +36,7 @@ export function renderHotspotResponse(
37
36
  appendGraphicObjectImage(
38
37
  surface,
39
38
  object,
40
- object.text || `${readableType(interaction.type)} image`,
39
+ object.text || messages.message("interactionImageAlt", { type: interaction.type }),
41
40
  );
42
41
  }
43
42
 
@@ -46,7 +45,7 @@ export function renderHotspotResponse(
46
45
  const selectedSummary = document.createElement("p");
47
46
  selectedSummary.className = "qti3-selection-summary";
48
47
  selectedSummary.setAttribute("aria-live", "polite");
49
- selectedSummary.textContent = messages.noRegionSelected();
48
+ selectedSummary.textContent = messages.message("noRegionSelected");
50
49
  const syncSelected = () => {
51
50
  for (const button of surface.querySelectorAll<HTMLButtonElement>("button")) {
52
51
  const isSelected = selected.has(button.dataset.choiceIdentifier ?? "");
@@ -54,7 +53,12 @@ export function renderHotspotResponse(
54
53
  button.dataset.selected = isSelected ? "true" : "false";
55
54
  }
56
55
  selectedSummary.textContent =
57
- selected.size > 0 ? `Selected ${[...selected].join(", ")}` : messages.noRegionSelected();
56
+ selected.size > 0
57
+ ? messages.message("hotspotSelectionSummary", {
58
+ selection: [...selected].join(", "),
59
+ count: selected.size,
60
+ })
61
+ : messages.message("noRegionSelected");
58
62
  };
59
63
  for (const choice of choices) {
60
64
  const button = document.createElement("button");
@@ -4,17 +4,17 @@ import {
4
4
  missingChoicesMessage,
5
5
  valueToStrings,
6
6
  } from "../interaction-support.js";
7
- import type { QtiPlayerMessages } from "../player-messages.js";
7
+ import type { PlayerMessageResolver } from "../player-message-resolver.js";
8
8
  import { interactionLabel } from "./interaction-label.js";
9
9
 
10
10
  function appendOptions(
11
11
  select: HTMLSelectElement,
12
12
  choices: QtiChoice[],
13
- messages: QtiPlayerMessages,
13
+ messages: PlayerMessageResolver,
14
14
  ): void {
15
15
  const empty = document.createElement("option");
16
16
  empty.value = "";
17
- empty.textContent = messages.inlineChoicePrompt();
17
+ empty.textContent = messages.message("inlineChoicePrompt");
18
18
  select.append(empty);
19
19
  for (const choice of choices) {
20
20
  const option = document.createElement("option");
@@ -28,7 +28,7 @@ export function renderSelect(
28
28
  interaction: QtiInteraction,
29
29
  update: (value: QtiValue) => void,
30
30
  currentValue: QtiValue,
31
- messages: QtiPlayerMessages,
31
+ messages: PlayerMessageResolver,
32
32
  ): HTMLElement {
33
33
  const choices = interactionChoices(interaction);
34
34
  if (choices.length === 0) return missingChoicesMessage(interaction);
@@ -1,5 +1,5 @@
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 { renderGraphicOrderResponse } from "../reorder/graphic-order-interaction.js";
4
4
  import { renderOrderedResponse } from "../reorder/order-interaction.js";
5
5
  import { renderChoice } from "./choice-interaction.js";
@@ -24,7 +24,7 @@ export interface InteractionResponseContext {
24
24
  interaction: QtiInteraction;
25
25
  update: (value: QtiValue) => void;
26
26
  currentValue: QtiValue;
27
- messages: QtiPlayerMessages;
27
+ messages: PlayerMessageResolver;
28
28
  isCompleted: () => boolean;
29
29
  endAttempt: () => void;
30
30
  renderPortableCustom: (
@@ -81,8 +81,8 @@ export const interactionRegistry: InteractionRegistryEntry[] = [
81
81
  id: "gapMatch",
82
82
  matches: (interaction) =>
83
83
  interaction.type === "gapMatch" || interaction.type === "graphicGapMatch",
84
- render: ({ interaction, update, currentValue }) =>
85
- renderGapMatchResponse(interaction, update, currentValue),
84
+ render: ({ interaction, update, currentValue, messages }) =>
85
+ renderGapMatchResponse(interaction, update, currentValue, messages),
86
86
  },
87
87
  {
88
88
  id: "graphicAssociate",
@@ -117,8 +117,8 @@ export const interactionRegistry: InteractionRegistryEntry[] = [
117
117
  {
118
118
  id: "choice",
119
119
  matches: usesChoiceSet,
120
- render: ({ interaction, update, currentValue }) =>
121
- renderChoice(interaction, update, currentValue),
120
+ render: ({ interaction, update, currentValue, messages }) =>
121
+ renderChoice(interaction, update, currentValue, messages),
122
122
  },
123
123
  {
124
124
  id: "inlineChoice",
@@ -129,8 +129,8 @@ export const interactionRegistry: InteractionRegistryEntry[] = [
129
129
  {
130
130
  id: "extendedText",
131
131
  matches: (interaction) => interaction.type === "extendedText",
132
- render: ({ interaction, update, currentValue }) =>
133
- renderTextResponse(interaction, update, "extended", currentValue),
132
+ render: ({ interaction, update, currentValue, messages }) =>
133
+ renderTextResponse(interaction, update, "extended", currentValue, messages),
134
134
  },
135
135
  {
136
136
  id: "selectPoint",
@@ -159,14 +159,14 @@ export const interactionRegistry: InteractionRegistryEntry[] = [
159
159
  {
160
160
  id: "textEntry",
161
161
  matches: (interaction) => interaction.type === "textEntry",
162
- render: ({ interaction, update, currentValue }) =>
163
- renderTextResponse(interaction, update, "entry", currentValue),
162
+ render: ({ interaction, update, currentValue, messages }) =>
163
+ renderTextResponse(interaction, update, "entry", currentValue, messages),
164
164
  },
165
165
  {
166
166
  id: "slider",
167
167
  matches: (interaction) => interaction.type === "slider",
168
- render: ({ interaction, update, currentValue }) =>
169
- renderSliderResponse(interaction, update, currentValue),
168
+ render: ({ interaction, update, currentValue, messages }) =>
169
+ renderSliderResponse(interaction, update, currentValue, messages),
170
170
  },
171
171
  {
172
172
  id: "upload",