@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 +19 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +240 -31
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +281 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longsightgroup/qti3-player",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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-core": "0.2.
|
|
49
|
-
"@longsightgroup/qti3-fixtures": "0.2.
|
|
48
|
+
"@longsightgroup/qti3-core": "0.2.1",
|
|
49
|
+
"@longsightgroup/qti3-fixtures": "0.2.1"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {}
|
|
52
52
|
}
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,17 @@ export interface QtiPlayerLoadOptions {
|
|
|
44
44
|
resolveAsset?: QtiPlayerResolveAsset | undefined;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export interface QtiPlayerRemoveMessageParams {
|
|
48
|
+
label: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface QtiPlayerMessages {
|
|
52
|
+
remove: () => string;
|
|
53
|
+
removePair: (params: QtiPlayerRemoveMessageParams) => string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type QtiPlayerMessageOverrides = Partial<QtiPlayerMessages>;
|
|
57
|
+
|
|
47
58
|
export interface QtiReadyEventDetail {
|
|
48
59
|
item: QtiAssessmentItem;
|
|
49
60
|
}
|
|
@@ -112,15 +123,59 @@ const HTMLElementBase: typeof HTMLElement =
|
|
|
112
123
|
} as unknown as typeof HTMLElement);
|
|
113
124
|
|
|
114
125
|
export class QtiAssessmentItemPlayer extends HTMLElementBase {
|
|
126
|
+
static get observedAttributes(): string[] {
|
|
127
|
+
return ["language-of-interface", "locale"];
|
|
128
|
+
}
|
|
129
|
+
|
|
115
130
|
private documentModel?: QtiDocument;
|
|
116
131
|
private session?: QtiItemSession;
|
|
117
132
|
private resolveAsset: QtiPlayerResolveAsset | undefined;
|
|
118
133
|
private validationMessages: QtiDiagnostic[] = [];
|
|
134
|
+
private languageOfInterfaceOverride: string | undefined;
|
|
135
|
+
private messageOverrides: QtiPlayerMessageOverrides = {};
|
|
119
136
|
private sessionControl: Required<QtiPlayerSessionControl> = {
|
|
120
137
|
validateResponses: true,
|
|
121
138
|
showFeedback: true,
|
|
122
139
|
};
|
|
123
140
|
|
|
141
|
+
get languageOfInterface(): string {
|
|
142
|
+
return (
|
|
143
|
+
this.languageOfInterfaceOverride ??
|
|
144
|
+
this.getAttribute?.("language-of-interface") ??
|
|
145
|
+
this.getAttribute?.("locale") ??
|
|
146
|
+
defaultPlayerLocale(this)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
set languageOfInterface(value: string | undefined) {
|
|
151
|
+
this.languageOfInterfaceOverride = normalizedLocale(value);
|
|
152
|
+
this.rerenderIfLoaded();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get locale(): string {
|
|
156
|
+
return this.languageOfInterface;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
set locale(value: string | undefined) {
|
|
160
|
+
this.languageOfInterface = value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get messages(): QtiPlayerMessageOverrides {
|
|
164
|
+
return this.messageOverrides;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
set messages(value: QtiPlayerMessageOverrides | undefined) {
|
|
168
|
+
this.messageOverrides = value ?? {};
|
|
169
|
+
this.rerenderIfLoaded();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
|
|
173
|
+
if ((name !== "language-of-interface" && name !== "locale") || oldValue === newValue) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.rerenderIfLoaded();
|
|
177
|
+
}
|
|
178
|
+
|
|
124
179
|
async loadXml(xml: string, options: QtiPlayerLoadOptions = {}): Promise<void> {
|
|
125
180
|
this.sessionControl = {
|
|
126
181
|
validateResponses: options.sessionControl?.validateResponses ?? true,
|
|
@@ -265,6 +320,17 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
|
|
|
265
320
|
this.dispatchEvent(new CustomEvent<QtiAssessmentItemPlayerEventDetailMap[T]>(type, { detail }));
|
|
266
321
|
}
|
|
267
322
|
|
|
323
|
+
private playerMessages(): QtiPlayerMessages {
|
|
324
|
+
return resolvePlayerMessages(this.languageOfInterface, this.messageOverrides);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private rerenderIfLoaded(): void {
|
|
328
|
+
if (!this.documentModel) return;
|
|
329
|
+
this.render();
|
|
330
|
+
this.renderValidationMessages();
|
|
331
|
+
this.updateAttemptAvailability();
|
|
332
|
+
}
|
|
333
|
+
|
|
268
334
|
private render(): void {
|
|
269
335
|
const documentModel = this.documentModel;
|
|
270
336
|
if (!documentModel) return;
|
|
@@ -308,6 +374,7 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
|
|
|
308
374
|
}
|
|
309
375
|
|
|
310
376
|
private renderInteraction(interaction: QtiInteraction): HTMLElement {
|
|
377
|
+
const messages = this.playerMessages();
|
|
311
378
|
const field = document.createElement("section");
|
|
312
379
|
field.className = `qti3-interaction qti3-${interaction.type}`;
|
|
313
380
|
field.classList.add(...qtiSharedClassNames(interaction.attributes.class));
|
|
@@ -335,7 +402,7 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
|
|
|
335
402
|
const currentValue = responseIdentifier ? this.currentResponseValue(responseIdentifier) : null;
|
|
336
403
|
|
|
337
404
|
if (interaction.type === "graphicOrder") {
|
|
338
|
-
field.append(renderGraphicOrderResponse(interaction, update, currentValue));
|
|
405
|
+
field.append(renderGraphicOrderResponse(interaction, update, currentValue, messages));
|
|
339
406
|
return field;
|
|
340
407
|
}
|
|
341
408
|
|
|
@@ -350,17 +417,17 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
|
|
|
350
417
|
}
|
|
351
418
|
|
|
352
419
|
if (interaction.type === "graphicAssociate") {
|
|
353
|
-
field.append(renderGraphicAssociateResponse(interaction, update, currentValue));
|
|
420
|
+
field.append(renderGraphicAssociateResponse(interaction, update, currentValue, messages));
|
|
354
421
|
return field;
|
|
355
422
|
}
|
|
356
423
|
|
|
357
424
|
if (interaction.type === "match") {
|
|
358
|
-
field.append(renderMatchResponse(interaction, update, currentValue));
|
|
425
|
+
field.append(renderMatchResponse(interaction, update, currentValue, messages));
|
|
359
426
|
return field;
|
|
360
427
|
}
|
|
361
428
|
|
|
362
429
|
if (usesPairResponse(interaction)) {
|
|
363
|
-
field.append(renderPairResponse(interaction, update, currentValue));
|
|
430
|
+
field.append(renderPairResponse(interaction, update, currentValue, messages));
|
|
364
431
|
return field;
|
|
365
432
|
}
|
|
366
433
|
|
|
@@ -951,6 +1018,154 @@ declare global {
|
|
|
951
1018
|
}
|
|
952
1019
|
}
|
|
953
1020
|
|
|
1021
|
+
const defaultEnglishPlayerMessages: QtiPlayerMessages = {
|
|
1022
|
+
remove: () => "Remove",
|
|
1023
|
+
removePair: ({ label }) => `Remove ${label}`,
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
const playerMessages = {
|
|
1027
|
+
defaultEnglish: defaultEnglishPlayerMessages,
|
|
1028
|
+
spanish: playerMessageCatalog("Quitar", ({ label }) => `Quitar ${label}`),
|
|
1029
|
+
swedish: playerMessageCatalog("Ta bort", ({ label }) => `Ta bort ${label}`),
|
|
1030
|
+
german: playerMessageCatalog("Entfernen", ({ label }) => `${label} entfernen`),
|
|
1031
|
+
portuguese: playerMessageCatalog("Remover", ({ label }) => `Remover ${label}`),
|
|
1032
|
+
french: playerMessageCatalog("Supprimer", ({ label }) => `Supprimer ${label}`),
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
const builtInPlayerMessageCatalogs: ReadonlyMap<string, QtiPlayerMessages> = new Map([
|
|
1036
|
+
["en", playerMessages.defaultEnglish],
|
|
1037
|
+
["es", playerMessages.spanish],
|
|
1038
|
+
["es-es", playerMessages.spanish],
|
|
1039
|
+
["es-mx", playerMessages.spanish],
|
|
1040
|
+
["sv", playerMessages.swedish],
|
|
1041
|
+
["sv-se", playerMessages.swedish],
|
|
1042
|
+
["de", playerMessages.german],
|
|
1043
|
+
["de-de", playerMessages.german],
|
|
1044
|
+
["pt", playerMessages.portuguese],
|
|
1045
|
+
["pt-br", playerMessages.portuguese],
|
|
1046
|
+
["pt-pt", playerMessages.portuguese],
|
|
1047
|
+
["fr", playerMessages.french],
|
|
1048
|
+
["fr-ca", playerMessages.french],
|
|
1049
|
+
["fr-fr", playerMessages.french],
|
|
1050
|
+
]);
|
|
1051
|
+
|
|
1052
|
+
function playerMessageCatalog(
|
|
1053
|
+
remove: string,
|
|
1054
|
+
removePair: QtiPlayerMessages["removePair"],
|
|
1055
|
+
): QtiPlayerMessages {
|
|
1056
|
+
return {
|
|
1057
|
+
remove: () => remove,
|
|
1058
|
+
removePair,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function resolvePlayerMessages(
|
|
1063
|
+
locale: string,
|
|
1064
|
+
overrides: QtiPlayerMessageOverrides,
|
|
1065
|
+
): QtiPlayerMessages {
|
|
1066
|
+
const catalog = builtInPlayerMessageCatalog(locale);
|
|
1067
|
+
return {
|
|
1068
|
+
remove: overrides.remove ?? catalog?.remove ?? defaultEnglishPlayerMessages.remove,
|
|
1069
|
+
removePair:
|
|
1070
|
+
overrides.removePair ?? catalog?.removePair ?? defaultEnglishPlayerMessages.removePair,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function builtInPlayerMessageCatalog(locale: string): QtiPlayerMessages | undefined {
|
|
1075
|
+
for (const candidate of localeFallbacks(locale)) {
|
|
1076
|
+
const catalog = builtInPlayerMessageCatalogs.get(candidate);
|
|
1077
|
+
if (catalog) return catalog;
|
|
1078
|
+
}
|
|
1079
|
+
return undefined;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function localeFallbacks(locale: string): string[] {
|
|
1083
|
+
const normalized = normalizedLocale(locale)?.toLowerCase();
|
|
1084
|
+
if (!normalized) return ["en"];
|
|
1085
|
+
const parts = normalized.split("-");
|
|
1086
|
+
const fallbacks: string[] = [];
|
|
1087
|
+
for (let length = parts.length; length > 0; length -= 1) {
|
|
1088
|
+
fallbacks.push(parts.slice(0, length).join("-"));
|
|
1089
|
+
}
|
|
1090
|
+
return fallbacks.includes("en") ? fallbacks : [...fallbacks, "en"];
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function normalizedLocale(value: string | undefined | null): string | undefined {
|
|
1094
|
+
const trimmed = value?.trim();
|
|
1095
|
+
if (!trimmed) return undefined;
|
|
1096
|
+
try {
|
|
1097
|
+
return Intl.getCanonicalLocales(trimmed)[0] ?? trimmed;
|
|
1098
|
+
} catch {
|
|
1099
|
+
return trimmed;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function defaultPlayerLocale(host?: Element): string {
|
|
1104
|
+
const elementLanguage = normalizedLocale(host?.getAttribute("lang"));
|
|
1105
|
+
if (elementLanguage) return elementLanguage;
|
|
1106
|
+
|
|
1107
|
+
const navigatorLanguages = globalThis.navigator?.languages ?? [];
|
|
1108
|
+
for (const language of navigatorLanguages) {
|
|
1109
|
+
const normalized = normalizedLocale(language);
|
|
1110
|
+
if (normalized) return normalized;
|
|
1111
|
+
}
|
|
1112
|
+
return (
|
|
1113
|
+
normalizedLocale(globalThis.navigator?.language) ??
|
|
1114
|
+
normalizedLocale(host?.closest("[lang]")?.getAttribute("lang")) ??
|
|
1115
|
+
normalizedLocale(host?.ownerDocument?.documentElement.lang) ??
|
|
1116
|
+
normalizedLocale(globalThis.document?.documentElement.lang) ??
|
|
1117
|
+
"en"
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function removeButton(label: string | null, messages: QtiPlayerMessages): HTMLButtonElement {
|
|
1122
|
+
const safeLabel = label?.trim() || messages.remove();
|
|
1123
|
+
const button = document.createElement("button");
|
|
1124
|
+
button.type = "button";
|
|
1125
|
+
button.className = "qti3-icon-button qti3-remove-button";
|
|
1126
|
+
button.title = messages.remove();
|
|
1127
|
+
button.setAttribute("aria-label", messages.removePair({ label: safeLabel }));
|
|
1128
|
+
button.append(trashIcon());
|
|
1129
|
+
return button;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
type IconPath = string | { d: string; fill?: string; stroke?: string };
|
|
1133
|
+
|
|
1134
|
+
function inlineIcon(className: string, paths: IconPath[]): SVGSVGElement {
|
|
1135
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1136
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
1137
|
+
svg.setAttribute("width", "24");
|
|
1138
|
+
svg.setAttribute("height", "24");
|
|
1139
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
1140
|
+
svg.setAttribute("fill", "none");
|
|
1141
|
+
svg.setAttribute("stroke", "currentColor");
|
|
1142
|
+
svg.setAttribute("stroke-width", "2");
|
|
1143
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
1144
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
1145
|
+
svg.setAttribute("aria-hidden", "true");
|
|
1146
|
+
svg.setAttribute("focusable", "false");
|
|
1147
|
+
svg.setAttribute("class", className);
|
|
1148
|
+
|
|
1149
|
+
for (const entry of paths) {
|
|
1150
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1151
|
+
if (typeof entry === "string") {
|
|
1152
|
+
path.setAttribute("d", entry);
|
|
1153
|
+
} else {
|
|
1154
|
+
path.setAttribute("d", entry.d);
|
|
1155
|
+
if (entry.stroke) {
|
|
1156
|
+
path.setAttribute("stroke", entry.stroke);
|
|
1157
|
+
path.style.stroke = entry.stroke;
|
|
1158
|
+
}
|
|
1159
|
+
if (entry.fill) {
|
|
1160
|
+
path.setAttribute("fill", entry.fill);
|
|
1161
|
+
path.style.fill = entry.fill;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
svg.append(path);
|
|
1165
|
+
}
|
|
1166
|
+
return svg;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
954
1169
|
function renderChoice(
|
|
955
1170
|
interaction: QtiInteraction,
|
|
956
1171
|
update: (value: QtiValue) => void,
|
|
@@ -1021,13 +1236,28 @@ function responseGroup(className?: string): HTMLElement {
|
|
|
1021
1236
|
|
|
1022
1237
|
type MovementDirection = "up" | "down" | "left" | "right";
|
|
1023
1238
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1239
|
+
function trashIcon(): SVGSVGElement {
|
|
1240
|
+
return inlineIcon("qti3-trash-icon", [
|
|
1241
|
+
{ d: "M0 0h24v24H0z", stroke: "none", fill: "none" },
|
|
1242
|
+
"M4 7l16 0",
|
|
1243
|
+
"M10 11l0 6",
|
|
1244
|
+
"M14 11l0 6",
|
|
1245
|
+
"M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12",
|
|
1246
|
+
"M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3",
|
|
1247
|
+
]);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const movementIconPaths: Record<MovementDirection, string[]> = {
|
|
1251
|
+
up: ["M12 5l0 14", "M18 11l-6 -6", "M6 11l6 -6"],
|
|
1252
|
+
down: ["M12 5l0 14", "M18 13l-6 6", "M6 13l6 6"],
|
|
1253
|
+
left: ["M5 12l14 0", "M5 12l6 6", "M5 12l6 -6"],
|
|
1254
|
+
right: ["M5 12l14 0", "M13 18l6 -6", "M13 6l6 6"],
|
|
1029
1255
|
};
|
|
1030
1256
|
|
|
1257
|
+
function movementIcon(direction: MovementDirection): SVGSVGElement {
|
|
1258
|
+
return inlineIcon("qti3-movement-icon", movementIconPaths[direction]);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1031
1261
|
function movementButton(
|
|
1032
1262
|
direction: MovementDirection,
|
|
1033
1263
|
accessibleName: string,
|
|
@@ -1035,10 +1265,10 @@ function movementButton(
|
|
|
1035
1265
|
): HTMLButtonElement {
|
|
1036
1266
|
const button = document.createElement("button");
|
|
1037
1267
|
button.type = "button";
|
|
1038
|
-
button.className = "qti3-icon-button";
|
|
1268
|
+
button.className = "qti3-icon-button qti3-move-button";
|
|
1039
1269
|
button.dataset.moveDirection = direction;
|
|
1040
|
-
button.textContent = movementGlyphs[direction];
|
|
1041
1270
|
button.setAttribute("aria-label", accessibleName);
|
|
1271
|
+
button.append(movementIcon(direction));
|
|
1042
1272
|
button.addEventListener("click", onClick);
|
|
1043
1273
|
return button;
|
|
1044
1274
|
}
|
|
@@ -1282,6 +1512,7 @@ function renderPairResponse(
|
|
|
1282
1512
|
interaction: QtiInteraction,
|
|
1283
1513
|
update: (value: QtiValue) => void,
|
|
1284
1514
|
currentValue: QtiValue,
|
|
1515
|
+
messages: QtiPlayerMessages,
|
|
1285
1516
|
): HTMLElement {
|
|
1286
1517
|
const group = responseGroup();
|
|
1287
1518
|
appendGraphicContext(group, interaction);
|
|
@@ -1346,10 +1577,7 @@ function renderPairResponse(
|
|
|
1346
1577
|
item.className = "qti3-pair-chip";
|
|
1347
1578
|
const text = document.createElement("span");
|
|
1348
1579
|
text.textContent = `${choiceText(sources, source)} to ${choiceText(targets, target)}`;
|
|
1349
|
-
const remove =
|
|
1350
|
-
remove.type = "button";
|
|
1351
|
-
remove.textContent = "Remove";
|
|
1352
|
-
remove.setAttribute("aria-label", `Remove ${text.textContent}`);
|
|
1580
|
+
const remove = removeButton(text.textContent, messages);
|
|
1353
1581
|
remove.addEventListener("click", () => {
|
|
1354
1582
|
const index = selectedPairs.indexOf(pair);
|
|
1355
1583
|
if (index >= 0) selectedPairs.splice(index, 1);
|
|
@@ -1411,6 +1639,7 @@ function renderMatchResponse(
|
|
|
1411
1639
|
interaction: QtiInteraction,
|
|
1412
1640
|
update: (value: QtiValue) => void,
|
|
1413
1641
|
currentValue: QtiValue,
|
|
1642
|
+
messages: QtiPlayerMessages,
|
|
1414
1643
|
): HTMLElement {
|
|
1415
1644
|
const group = responseGroup();
|
|
1416
1645
|
|
|
@@ -1470,10 +1699,7 @@ function renderMatchResponse(
|
|
|
1470
1699
|
item.className = "qti3-pair-chip";
|
|
1471
1700
|
const text = document.createElement("span");
|
|
1472
1701
|
text.textContent = label;
|
|
1473
|
-
const remove =
|
|
1474
|
-
remove.type = "button";
|
|
1475
|
-
remove.textContent = "Remove";
|
|
1476
|
-
remove.setAttribute("aria-label", `Remove ${label}`);
|
|
1702
|
+
const remove = removeButton(label, messages);
|
|
1477
1703
|
remove.addEventListener("click", () => {
|
|
1478
1704
|
removePair(pair);
|
|
1479
1705
|
syncPressed();
|
|
@@ -1608,6 +1834,7 @@ function renderGraphicOrderResponse(
|
|
|
1608
1834
|
interaction: QtiInteraction,
|
|
1609
1835
|
update: (value: QtiValue) => void,
|
|
1610
1836
|
currentValue: QtiValue,
|
|
1837
|
+
messages: QtiPlayerMessages,
|
|
1611
1838
|
): HTMLElement {
|
|
1612
1839
|
const group = responseGroup();
|
|
1613
1840
|
|
|
@@ -1783,10 +2010,7 @@ function renderGraphicOrderResponse(
|
|
|
1783
2010
|
);
|
|
1784
2011
|
down.disabled = index === currentChoices.length - 1;
|
|
1785
2012
|
|
|
1786
|
-
const remove =
|
|
1787
|
-
remove.type = "button";
|
|
1788
|
-
remove.textContent = "Remove";
|
|
1789
|
-
remove.setAttribute("aria-label", `Remove ${choiceLabel}`);
|
|
2013
|
+
const remove = removeButton(choiceLabel, messages);
|
|
1790
2014
|
remove.addEventListener("click", () => removeHotspot(choice.identifier));
|
|
1791
2015
|
|
|
1792
2016
|
item.append(label, up, down, remove);
|
|
@@ -1837,6 +2061,7 @@ function renderGraphicAssociateResponse(
|
|
|
1837
2061
|
interaction: QtiInteraction,
|
|
1838
2062
|
update: (value: QtiValue) => void,
|
|
1839
2063
|
currentValue: QtiValue,
|
|
2064
|
+
messages: QtiPlayerMessages,
|
|
1840
2065
|
): HTMLElement {
|
|
1841
2066
|
const group = responseGroup();
|
|
1842
2067
|
|
|
@@ -2047,10 +2272,7 @@ function renderGraphicAssociateResponse(
|
|
|
2047
2272
|
item.className = "qti3-pair-chip";
|
|
2048
2273
|
const text = document.createElement("span");
|
|
2049
2274
|
text.textContent = pairLabel;
|
|
2050
|
-
const remove =
|
|
2051
|
-
remove.type = "button";
|
|
2052
|
-
remove.textContent = "Remove";
|
|
2053
|
-
remove.setAttribute("aria-label", `Remove ${pairLabel}`);
|
|
2275
|
+
const remove = removeButton(pairLabel, messages);
|
|
2054
2276
|
remove.addEventListener("click", () => removePair(pair));
|
|
2055
2277
|
item.append(text, remove);
|
|
2056
2278
|
return item;
|
|
@@ -2403,6 +2625,7 @@ function renderGraphicGapMatchResponse(
|
|
|
2403
2625
|
renderTargets();
|
|
2404
2626
|
commit();
|
|
2405
2627
|
});
|
|
2628
|
+
button.style.position = "absolute";
|
|
2406
2629
|
placeHotspotButton(button, gap, width, height);
|
|
2407
2630
|
if (assigned) {
|
|
2408
2631
|
const assignedLabel = document.createElement("span");
|
|
@@ -4664,6 +4887,37 @@ function playerStyleElement(): HTMLStyleElement {
|
|
|
4664
4887
|
line-height: 1;
|
|
4665
4888
|
}
|
|
4666
4889
|
|
|
4890
|
+
.qti3-remove-button {
|
|
4891
|
+
border: 1px solid currentColor;
|
|
4892
|
+
background: transparent;
|
|
4893
|
+
color: inherit;
|
|
4894
|
+
cursor: pointer;
|
|
4895
|
+
}
|
|
4896
|
+
|
|
4897
|
+
.qti3-remove-button:hover {
|
|
4898
|
+
background: color-mix(in srgb, currentColor 14%, transparent);
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4901
|
+
.qti3-trash-icon {
|
|
4902
|
+
inline-size: 1.125rem;
|
|
4903
|
+
block-size: 1.125rem;
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
.qti3-movement-icon {
|
|
4907
|
+
inline-size: 1rem;
|
|
4908
|
+
block-size: 1rem;
|
|
4909
|
+
}
|
|
4910
|
+
|
|
4911
|
+
.qti3-trash-icon path,
|
|
4912
|
+
.qti3-movement-icon path {
|
|
4913
|
+
fill: none;
|
|
4914
|
+
stroke: currentColor;
|
|
4915
|
+
stroke-width: 2;
|
|
4916
|
+
stroke-linecap: round;
|
|
4917
|
+
stroke-linejoin: round;
|
|
4918
|
+
vector-effect: non-scaling-stroke;
|
|
4919
|
+
}
|
|
4920
|
+
|
|
4667
4921
|
.qti3-token[aria-pressed="true"],
|
|
4668
4922
|
.qti3-pair-chip {
|
|
4669
4923
|
background: Highlight;
|