@longsightgroup/qti3-player 0.2.0 → 0.2.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.
package/README.md CHANGED
@@ -39,6 +39,25 @@ player?.addEventListener("qti-statechange", (event) => {
39
39
  });
40
40
  ```
41
41
 
42
+ ## Player Chrome Messages
43
+
44
+ The player keeps authored QTI content language separate from player chrome such as
45
+ remove buttons. Chrome language defaults to the player `languageOfInterface` property,
46
+ the `language-of-interface` attribute, browser language, document language, then
47
+ English. Hosts can override it when delivery settings require a fixed interface language.
48
+
49
+ Built-in catalogs are currently available for English, Spanish (`es-MX`, `es-ES`),
50
+ Swedish (`sv-SE`), German (`de-DE`), Portuguese (`pt-BR`, `pt-PT`), and French
51
+ (`fr-FR`, `fr-CA`).
52
+
53
+ ```ts
54
+ player.languageOfInterface = "es-MX";
55
+ player.messages = {
56
+ remove: () => "Eliminar",
57
+ removePair: ({ label }) => `Eliminar ${label}`,
58
+ };
59
+ ```
60
+
42
61
  ## Portable Custom Interactions
43
62
 
44
63
  For `qti-portable-custom-interaction`, the player renders a
package/dist/index.d.ts CHANGED
@@ -15,6 +15,14 @@ export interface QtiPlayerLoadOptions {
15
15
  fetchXml?: QtiPlayerFetchXml | undefined;
16
16
  resolveAsset?: QtiPlayerResolveAsset | undefined;
17
17
  }
18
+ export interface QtiPlayerRemoveMessageParams {
19
+ label: string;
20
+ }
21
+ export interface QtiPlayerMessages {
22
+ remove: () => string;
23
+ removePair: (params: QtiPlayerRemoveMessageParams) => string;
24
+ }
25
+ export type QtiPlayerMessageOverrides = Partial<QtiPlayerMessages>;
18
26
  export interface QtiReadyEventDetail {
19
27
  item: QtiAssessmentItem;
20
28
  }
@@ -61,11 +69,21 @@ export type QtiAssessmentItemPlayerCustomEventMap = {
61
69
  };
62
70
  declare const HTMLElementBase: typeof HTMLElement;
63
71
  export declare class QtiAssessmentItemPlayer extends HTMLElementBase {
72
+ static get observedAttributes(): string[];
64
73
  private documentModel?;
65
74
  private session?;
66
75
  private resolveAsset;
67
76
  private validationMessages;
77
+ private languageOfInterfaceOverride;
78
+ private messageOverrides;
68
79
  private sessionControl;
80
+ get languageOfInterface(): string;
81
+ set languageOfInterface(value: string | undefined);
82
+ get locale(): string;
83
+ set locale(value: string | undefined);
84
+ get messages(): QtiPlayerMessageOverrides;
85
+ set messages(value: QtiPlayerMessageOverrides | undefined);
86
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
69
87
  loadXml(xml: string, options?: QtiPlayerLoadOptions): Promise<void>;
70
88
  loadUrl(url: string, options?: QtiPlayerLoadOptions): Promise<void>;
71
89
  scoreAttempt(options?: QtiScoreAttemptOptions): QtiScoreResult | undefined;
@@ -78,6 +96,8 @@ export declare class QtiAssessmentItemPlayer extends HTMLElementBase {
78
96
  getCatalogSupportResolution(options?: QtiCatalogSupportResolutionOptions): QtiCatalogSupportResolution | undefined;
79
97
  private emitStateChange;
80
98
  private dispatchPlayerEvent;
99
+ private playerMessages;
100
+ private rerenderIfLoaded;
81
101
  private render;
82
102
  private renderInteraction;
83
103
  private renderPortableCustomResponse;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EAGtB,KAAK,aAAa,EAElB,KAAK,cAAc,EAGnB,KAAK,2BAA2B,EAChC,KAAK,2BAA2B,EAChC,KAAK,cAAc,EACnB,KAAK,2BAA2B,EAChC,KAAK,kCAAkC,EACvC,KAAK,wBAAwB,EAC7B,KAAK,QAAQ,EACd,MAAM,2BAA2B,CAAC;AAEnC,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,WAAW,mBAAmB;IAClC,IAAI,EAAE,iBAAiB,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IACxC,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,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;AAEF,QAAA,MAAM,eAAe,EAAE,OAAO,WAOO,CAAC;AAEtC,qBAAa,uBAAwB,SAAQ,eAAe;IAC1D,OAAO,CAAC,aAAa,CAAC,CAAc;IACpC,OAAO,CAAC,OAAO,CAAC,CAAiB;IACjC,OAAO,CAAC,YAAY,CAAoC;IACxD,OAAO,CAAC,kBAAkB,CAAuB;IACjD,OAAO,CAAC,cAAc,CAGpB;IAEI,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BvE,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7E,YAAY,CAAC,OAAO,GAAE,sBAA2B,GAAG,cAAc,GAAG,SAAS;IA6B9E,KAAK,IAAI,IAAI;IAUb,OAAO,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAmBvC,OAAO,IAAI,IAAI;IASf,UAAU,CAAC,OAAO,GAAE,sBAA2B,GAAG,IAAI;IAgBtD,SAAS,IAAI,iBAAiB,GAAG,SAAS;IAM1C,wBAAwB,IAAI,wBAAwB,GAAG,SAAS;IAKhE,2BAA2B,CACzB,OAAO,GAAE,kCAAuC,GAC/C,2BAA2B,GAAG,SAAS;IAK1C,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,MAAM;IA0Cd,OAAO,CAAC,iBAAiB;IAoJzB,OAAO,CAAC,4BAA4B;IA6FpC,OAAO,CAAC,yBAAyB;IAiCjC,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,iBAAiB;IA0CzB,OAAO,CAAC,qBAAqB;IAc7B,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,sBAAsB;IA+B9B,OAAO,CAAC,yBAAyB;IAgCjC,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,wBAAwB;IAWhC,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,uBAAuB;IAI/B,OAAO,CAAC,yBAAyB;IAsBjC,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,iBAAiB;IAyDzB,OAAO,CAAC,wBAAwB;IAgChC,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,cAAc;CAgBvB;AAED,wBAAgB,6BAA6B,IAAI,IAAI,CAIpD;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,4BAA4B,EAAE,uBAAuB,CAAC;KACvD;CACF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -7,14 +7,48 @@ const HTMLElementBase = globalThis.HTMLElement ??
7
7
  }
8
8
  };
9
9
  export class QtiAssessmentItemPlayer extends HTMLElementBase {
10
+ static get observedAttributes() {
11
+ return ["language-of-interface", "locale"];
12
+ }
10
13
  documentModel;
11
14
  session;
12
15
  resolveAsset;
13
16
  validationMessages = [];
17
+ languageOfInterfaceOverride;
18
+ messageOverrides = {};
14
19
  sessionControl = {
15
20
  validateResponses: true,
16
21
  showFeedback: true,
17
22
  };
23
+ get languageOfInterface() {
24
+ return (this.languageOfInterfaceOverride ??
25
+ this.getAttribute?.("language-of-interface") ??
26
+ this.getAttribute?.("locale") ??
27
+ defaultPlayerLocale(this));
28
+ }
29
+ set languageOfInterface(value) {
30
+ this.languageOfInterfaceOverride = normalizedLocale(value);
31
+ this.rerenderIfLoaded();
32
+ }
33
+ get locale() {
34
+ return this.languageOfInterface;
35
+ }
36
+ set locale(value) {
37
+ this.languageOfInterface = value;
38
+ }
39
+ get messages() {
40
+ return this.messageOverrides;
41
+ }
42
+ set messages(value) {
43
+ this.messageOverrides = value ?? {};
44
+ this.rerenderIfLoaded();
45
+ }
46
+ attributeChangedCallback(name, oldValue, newValue) {
47
+ if ((name !== "language-of-interface" && name !== "locale") || oldValue === newValue) {
48
+ return;
49
+ }
50
+ this.rerenderIfLoaded();
51
+ }
18
52
  async loadXml(xml, options = {}) {
19
53
  this.sessionControl = {
20
54
  validateResponses: options.sessionControl?.validateResponses ?? true,
@@ -147,6 +181,16 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
147
181
  dispatchPlayerEvent(type, detail) {
148
182
  this.dispatchEvent(new CustomEvent(type, { detail }));
149
183
  }
184
+ playerMessages() {
185
+ return resolvePlayerMessages(this.languageOfInterface, this.messageOverrides);
186
+ }
187
+ rerenderIfLoaded() {
188
+ if (!this.documentModel)
189
+ return;
190
+ this.render();
191
+ this.renderValidationMessages();
192
+ this.updateAttemptAvailability();
193
+ }
150
194
  render() {
151
195
  const documentModel = this.documentModel;
152
196
  if (!documentModel)
@@ -186,6 +230,7 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
186
230
  this.replaceChildren(root);
187
231
  }
188
232
  renderInteraction(interaction) {
233
+ const messages = this.playerMessages();
189
234
  const field = document.createElement("section");
190
235
  field.className = `qti3-interaction qti3-${interaction.type}`;
191
236
  field.classList.add(...qtiSharedClassNames(interaction.attributes.class));
@@ -212,7 +257,7 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
212
257
  };
213
258
  const currentValue = responseIdentifier ? this.currentResponseValue(responseIdentifier) : null;
214
259
  if (interaction.type === "graphicOrder") {
215
- field.append(renderGraphicOrderResponse(interaction, update, currentValue));
260
+ field.append(renderGraphicOrderResponse(interaction, update, currentValue, messages));
216
261
  return field;
217
262
  }
218
263
  if (usesOrderedResponse(interaction)) {
@@ -224,15 +269,15 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
224
269
  return field;
225
270
  }
226
271
  if (interaction.type === "graphicAssociate") {
227
- field.append(renderGraphicAssociateResponse(interaction, update, currentValue));
272
+ field.append(renderGraphicAssociateResponse(interaction, update, currentValue, messages));
228
273
  return field;
229
274
  }
230
275
  if (interaction.type === "match") {
231
- field.append(renderMatchResponse(interaction, update, currentValue));
276
+ field.append(renderMatchResponse(interaction, update, currentValue, messages));
232
277
  return field;
233
278
  }
234
279
  if (usesPairResponse(interaction)) {
235
- field.append(renderPairResponse(interaction, update, currentValue));
280
+ field.append(renderPairResponse(interaction, update, currentValue, messages));
236
281
  return field;
237
282
  }
238
283
  if (interaction.type === "hotspot" && interaction.object) {
@@ -727,6 +772,137 @@ export function defineQtiAssessmentItemPlayer() {
727
772
  customElements.define("qti-assessment-item-player", QtiAssessmentItemPlayer);
728
773
  }
729
774
  }
775
+ const defaultEnglishPlayerMessages = {
776
+ remove: () => "Remove",
777
+ removePair: ({ label }) => `Remove ${label}`,
778
+ };
779
+ const playerMessages = {
780
+ defaultEnglish: defaultEnglishPlayerMessages,
781
+ spanish: playerMessageCatalog("Quitar", ({ label }) => `Quitar ${label}`),
782
+ swedish: playerMessageCatalog("Ta bort", ({ label }) => `Ta bort ${label}`),
783
+ german: playerMessageCatalog("Entfernen", ({ label }) => `${label} entfernen`),
784
+ portuguese: playerMessageCatalog("Remover", ({ label }) => `Remover ${label}`),
785
+ french: playerMessageCatalog("Supprimer", ({ label }) => `Supprimer ${label}`),
786
+ };
787
+ const builtInPlayerMessageCatalogs = new Map([
788
+ ["en", playerMessages.defaultEnglish],
789
+ ["es", playerMessages.spanish],
790
+ ["es-es", playerMessages.spanish],
791
+ ["es-mx", playerMessages.spanish],
792
+ ["sv", playerMessages.swedish],
793
+ ["sv-se", playerMessages.swedish],
794
+ ["de", playerMessages.german],
795
+ ["de-de", playerMessages.german],
796
+ ["pt", playerMessages.portuguese],
797
+ ["pt-br", playerMessages.portuguese],
798
+ ["pt-pt", playerMessages.portuguese],
799
+ ["fr", playerMessages.french],
800
+ ["fr-ca", playerMessages.french],
801
+ ["fr-fr", playerMessages.french],
802
+ ]);
803
+ function playerMessageCatalog(remove, removePair) {
804
+ return {
805
+ remove: () => remove,
806
+ removePair,
807
+ };
808
+ }
809
+ function resolvePlayerMessages(locale, overrides) {
810
+ const catalog = builtInPlayerMessageCatalog(locale);
811
+ return {
812
+ remove: overrides.remove ?? catalog?.remove ?? defaultEnglishPlayerMessages.remove,
813
+ removePair: overrides.removePair ?? catalog?.removePair ?? defaultEnglishPlayerMessages.removePair,
814
+ };
815
+ }
816
+ function builtInPlayerMessageCatalog(locale) {
817
+ for (const candidate of localeFallbacks(locale)) {
818
+ const catalog = builtInPlayerMessageCatalogs.get(candidate);
819
+ if (catalog)
820
+ return catalog;
821
+ }
822
+ return undefined;
823
+ }
824
+ function localeFallbacks(locale) {
825
+ const normalized = normalizedLocale(locale)?.toLowerCase();
826
+ if (!normalized)
827
+ return ["en"];
828
+ const parts = normalized.split("-");
829
+ const fallbacks = [];
830
+ for (let length = parts.length; length > 0; length -= 1) {
831
+ fallbacks.push(parts.slice(0, length).join("-"));
832
+ }
833
+ return fallbacks.includes("en") ? fallbacks : [...fallbacks, "en"];
834
+ }
835
+ function normalizedLocale(value) {
836
+ const trimmed = value?.trim();
837
+ if (!trimmed)
838
+ return undefined;
839
+ try {
840
+ return Intl.getCanonicalLocales(trimmed)[0] ?? trimmed;
841
+ }
842
+ catch {
843
+ return trimmed;
844
+ }
845
+ }
846
+ function defaultPlayerLocale(host) {
847
+ const elementLanguage = normalizedLocale(host?.getAttribute("lang"));
848
+ if (elementLanguage)
849
+ return elementLanguage;
850
+ const navigatorLanguages = globalThis.navigator?.languages ?? [];
851
+ for (const language of navigatorLanguages) {
852
+ const normalized = normalizedLocale(language);
853
+ if (normalized)
854
+ return normalized;
855
+ }
856
+ return (normalizedLocale(globalThis.navigator?.language) ??
857
+ normalizedLocale(host?.closest("[lang]")?.getAttribute("lang")) ??
858
+ normalizedLocale(host?.ownerDocument?.documentElement.lang) ??
859
+ normalizedLocale(globalThis.document?.documentElement.lang) ??
860
+ "en");
861
+ }
862
+ function removeButton(label, messages) {
863
+ const safeLabel = label?.trim() || messages.remove();
864
+ const button = document.createElement("button");
865
+ button.type = "button";
866
+ button.className = "qti3-icon-button qti3-remove-button";
867
+ button.title = messages.remove();
868
+ button.setAttribute("aria-label", messages.removePair({ label: safeLabel }));
869
+ button.append(trashIcon());
870
+ return button;
871
+ }
872
+ function inlineIcon(className, paths) {
873
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
874
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
875
+ svg.setAttribute("width", "24");
876
+ svg.setAttribute("height", "24");
877
+ svg.setAttribute("viewBox", "0 0 24 24");
878
+ svg.setAttribute("fill", "none");
879
+ svg.setAttribute("stroke", "currentColor");
880
+ svg.setAttribute("stroke-width", "2");
881
+ svg.setAttribute("stroke-linecap", "round");
882
+ svg.setAttribute("stroke-linejoin", "round");
883
+ svg.setAttribute("aria-hidden", "true");
884
+ svg.setAttribute("focusable", "false");
885
+ svg.setAttribute("class", className);
886
+ for (const entry of paths) {
887
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
888
+ if (typeof entry === "string") {
889
+ path.setAttribute("d", entry);
890
+ }
891
+ else {
892
+ path.setAttribute("d", entry.d);
893
+ if (entry.stroke) {
894
+ path.setAttribute("stroke", entry.stroke);
895
+ path.style.stroke = entry.stroke;
896
+ }
897
+ if (entry.fill) {
898
+ path.setAttribute("fill", entry.fill);
899
+ path.style.fill = entry.fill;
900
+ }
901
+ }
902
+ svg.append(path);
903
+ }
904
+ return svg;
905
+ }
730
906
  function renderChoice(interaction, update, currentValue) {
731
907
  const group = responseGroup("qti3-choice-group");
732
908
  const multiple = interaction.responseCardinality === "multiple" || interaction.responseCardinality === "ordered";
@@ -790,19 +966,32 @@ function responseGroup(className) {
790
966
  group.className = ["qti3-response-group", className].filter(Boolean).join(" ");
791
967
  return group;
792
968
  }
793
- const movementGlyphs = {
794
- up: "\u2191",
795
- down: "\u2193",
796
- left: "\u2190",
797
- right: "\u2192",
969
+ function trashIcon() {
970
+ return inlineIcon("qti3-trash-icon", [
971
+ { d: "M0 0h24v24H0z", stroke: "none", fill: "none" },
972
+ "M4 7l16 0",
973
+ "M10 11l0 6",
974
+ "M14 11l0 6",
975
+ "M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12",
976
+ "M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3",
977
+ ]);
978
+ }
979
+ const movementIconPaths = {
980
+ up: ["M12 5l0 14", "M18 11l-6 -6", "M6 11l6 -6"],
981
+ down: ["M12 5l0 14", "M18 13l-6 6", "M6 13l6 6"],
982
+ left: ["M5 12l14 0", "M5 12l6 6", "M5 12l6 -6"],
983
+ right: ["M5 12l14 0", "M13 18l6 -6", "M13 6l6 6"],
798
984
  };
985
+ function movementIcon(direction) {
986
+ return inlineIcon("qti3-movement-icon", movementIconPaths[direction]);
987
+ }
799
988
  function movementButton(direction, accessibleName, onClick) {
800
989
  const button = document.createElement("button");
801
990
  button.type = "button";
802
- button.className = "qti3-icon-button";
991
+ button.className = "qti3-icon-button qti3-move-button";
803
992
  button.dataset.moveDirection = direction;
804
- button.textContent = movementGlyphs[direction];
805
993
  button.setAttribute("aria-label", accessibleName);
994
+ button.append(movementIcon(direction));
806
995
  button.addEventListener("click", onClick);
807
996
  return button;
808
997
  }
@@ -1007,7 +1196,7 @@ function renderOrderedResponse(interaction, update, currentValue) {
1007
1196
  group.append(list);
1008
1197
  return group;
1009
1198
  }
1010
- function renderPairResponse(interaction, update, currentValue) {
1199
+ function renderPairResponse(interaction, update, currentValue, messages) {
1011
1200
  const group = responseGroup();
1012
1201
  appendGraphicContext(group, interaction);
1013
1202
  const sources = sourceChoices(interaction);
@@ -1066,10 +1255,7 @@ function renderPairResponse(interaction, update, currentValue) {
1066
1255
  item.className = "qti3-pair-chip";
1067
1256
  const text = document.createElement("span");
1068
1257
  text.textContent = `${choiceText(sources, source)} to ${choiceText(targets, target)}`;
1069
- const remove = document.createElement("button");
1070
- remove.type = "button";
1071
- remove.textContent = "Remove";
1072
- remove.setAttribute("aria-label", `Remove ${text.textContent}`);
1258
+ const remove = removeButton(text.textContent, messages);
1073
1259
  remove.addEventListener("click", () => {
1074
1260
  const index = selectedPairs.indexOf(pair);
1075
1261
  if (index >= 0)
@@ -1124,7 +1310,7 @@ function renderPairResponse(interaction, update, currentValue) {
1124
1310
  group.append(selector, pairList);
1125
1311
  return group;
1126
1312
  }
1127
- function renderMatchResponse(interaction, update, currentValue) {
1313
+ function renderMatchResponse(interaction, update, currentValue, messages) {
1128
1314
  const group = responseGroup();
1129
1315
  const sources = sourceChoices(interaction);
1130
1316
  const targets = targetChoices(interaction);
@@ -1176,10 +1362,7 @@ function renderMatchResponse(interaction, update, currentValue) {
1176
1362
  item.className = "qti3-pair-chip";
1177
1363
  const text = document.createElement("span");
1178
1364
  text.textContent = label;
1179
- const remove = document.createElement("button");
1180
- remove.type = "button";
1181
- remove.textContent = "Remove";
1182
- remove.setAttribute("aria-label", `Remove ${label}`);
1365
+ const remove = removeButton(label, messages);
1183
1366
  remove.addEventListener("click", () => {
1184
1367
  removePair(pair);
1185
1368
  syncPressed();
@@ -1310,7 +1493,7 @@ function pairRegionLabels(interaction) {
1310
1493
  return { source: "Prompt", target: "Match" };
1311
1494
  return { source: "Source", target: "Target" };
1312
1495
  }
1313
- function renderGraphicOrderResponse(interaction, update, currentValue) {
1496
+ function renderGraphicOrderResponse(interaction, update, currentValue, messages) {
1314
1497
  const group = responseGroup();
1315
1498
  const width = objectWidth(interaction);
1316
1499
  const height = objectHeight(interaction);
@@ -1471,10 +1654,7 @@ function renderGraphicOrderResponse(interaction, update, currentValue) {
1471
1654
  up.disabled = index === 0;
1472
1655
  const down = movementButton("down", movementLabel(choiceLabel, "down"), () => moveHotspot(choice.identifier, 1));
1473
1656
  down.disabled = index === currentChoices.length - 1;
1474
- const remove = document.createElement("button");
1475
- remove.type = "button";
1476
- remove.textContent = "Remove";
1477
- remove.setAttribute("aria-label", `Remove ${choiceLabel}`);
1657
+ const remove = removeButton(choiceLabel, messages);
1478
1658
  remove.addEventListener("click", () => removeHotspot(choice.identifier));
1479
1659
  item.append(label, up, down, remove);
1480
1660
  return item;
@@ -1518,7 +1698,7 @@ function renderGraphicOrderResponse(interaction, update, currentValue) {
1518
1698
  group.append(surface, summary, list);
1519
1699
  return group;
1520
1700
  }
1521
- function renderGraphicAssociateResponse(interaction, update, currentValue) {
1701
+ function renderGraphicAssociateResponse(interaction, update, currentValue, messages) {
1522
1702
  const group = responseGroup();
1523
1703
  const width = objectWidth(interaction);
1524
1704
  const height = objectHeight(interaction);
@@ -1721,10 +1901,7 @@ function renderGraphicAssociateResponse(interaction, update, currentValue) {
1721
1901
  item.className = "qti3-pair-chip";
1722
1902
  const text = document.createElement("span");
1723
1903
  text.textContent = pairLabel;
1724
- const remove = document.createElement("button");
1725
- remove.type = "button";
1726
- remove.textContent = "Remove";
1727
- remove.setAttribute("aria-label", `Remove ${pairLabel}`);
1904
+ const remove = removeButton(pairLabel, messages);
1728
1905
  remove.addEventListener("click", () => removePair(pair));
1729
1906
  item.append(text, remove);
1730
1907
  return item;
@@ -2041,6 +2218,7 @@ function renderGraphicGapMatchResponse(interaction, update, currentValue) {
2041
2218
  renderTargets();
2042
2219
  commit();
2043
2220
  });
2221
+ button.style.position = "absolute";
2044
2222
  placeHotspotButton(button, gap, width, height);
2045
2223
  if (assigned) {
2046
2224
  const assignedLabel = document.createElement("span");
@@ -4118,6 +4296,37 @@ function playerStyleElement() {
4118
4296
  line-height: 1;
4119
4297
  }
4120
4298
 
4299
+ .qti3-remove-button {
4300
+ border: 1px solid currentColor;
4301
+ background: transparent;
4302
+ color: inherit;
4303
+ cursor: pointer;
4304
+ }
4305
+
4306
+ .qti3-remove-button:hover {
4307
+ background: color-mix(in srgb, currentColor 14%, transparent);
4308
+ }
4309
+
4310
+ .qti3-trash-icon {
4311
+ inline-size: 1.125rem;
4312
+ block-size: 1.125rem;
4313
+ }
4314
+
4315
+ .qti3-movement-icon {
4316
+ inline-size: 1rem;
4317
+ block-size: 1rem;
4318
+ }
4319
+
4320
+ .qti3-trash-icon path,
4321
+ .qti3-movement-icon path {
4322
+ fill: none;
4323
+ stroke: currentColor;
4324
+ stroke-width: 2;
4325
+ stroke-linecap: round;
4326
+ stroke-linejoin: round;
4327
+ vector-effect: non-scaling-stroke;
4328
+ }
4329
+
4121
4330
  .qti3-token[aria-pressed="true"],
4122
4331
  .qti3-pair-chip {
4123
4332
  background: Highlight;