@longsightgroup/qti3-player 0.1.0 → 0.1.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/dist/index.d.ts +37 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +284 -187
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -35,33 +35,36 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
|
|
|
35
35
|
this.render();
|
|
36
36
|
this.renderValidationMessages();
|
|
37
37
|
this.updateAttemptAvailability();
|
|
38
|
-
this.
|
|
38
|
+
this.dispatchPlayerEvent("qti-ready", { item: result.document.item });
|
|
39
39
|
this.emitStateChange();
|
|
40
40
|
}
|
|
41
41
|
async loadUrl(url, options = {}) {
|
|
42
42
|
const fetchXml = options.fetchXml ?? defaultFetchXml;
|
|
43
43
|
await this.loadXml(await fetchXml(url), options);
|
|
44
44
|
}
|
|
45
|
-
scoreAttempt() {
|
|
45
|
+
scoreAttempt(options = {}) {
|
|
46
46
|
const session = this.session;
|
|
47
47
|
if (!session)
|
|
48
48
|
return undefined;
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
: [];
|
|
49
|
+
const shouldValidateResponses = options.validateResponses ?? this.sessionControl.validateResponses;
|
|
50
|
+
const validationMessages = shouldValidateResponses ? this.validateResponses() : [];
|
|
52
51
|
if (validationMessages.length > 0) {
|
|
53
|
-
this.validationMessages = validationMessages;
|
|
52
|
+
this.validationMessages = cloneDiagnostics(validationMessages);
|
|
54
53
|
this.renderValidationMessages();
|
|
55
|
-
const state =
|
|
56
|
-
state
|
|
57
|
-
|
|
54
|
+
const state = this.serialize();
|
|
55
|
+
if (!state)
|
|
56
|
+
return undefined;
|
|
57
|
+
this.dispatchPlayerEvent("qti-validation", {
|
|
58
|
+
validationMessages: cloneDiagnostics(this.validationMessages),
|
|
59
|
+
state,
|
|
60
|
+
});
|
|
58
61
|
this.emitStateChange(state);
|
|
59
62
|
return undefined;
|
|
60
63
|
}
|
|
61
64
|
this.validationMessages = [];
|
|
62
65
|
this.renderValidationMessages();
|
|
63
66
|
const result = session.score();
|
|
64
|
-
this.
|
|
67
|
+
this.dispatchPlayerEvent("qti-score", result);
|
|
65
68
|
this.updateDynamicBodyState();
|
|
66
69
|
this.updateAttemptAvailability();
|
|
67
70
|
if (this.sessionControl.showFeedback)
|
|
@@ -96,12 +99,17 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
|
|
|
96
99
|
this.emitStateChange();
|
|
97
100
|
}
|
|
98
101
|
suspend() {
|
|
99
|
-
this.session
|
|
100
|
-
|
|
101
|
-
this.
|
|
102
|
+
if (!this.session)
|
|
103
|
+
return;
|
|
104
|
+
this.session.setStatus("suspended");
|
|
105
|
+
const state = this.serialize();
|
|
106
|
+
if (!state)
|
|
107
|
+
return;
|
|
108
|
+
this.dispatchPlayerEvent("qti-suspend", { state });
|
|
109
|
+
this.emitStateChange(state);
|
|
102
110
|
}
|
|
103
|
-
endAttempt() {
|
|
104
|
-
const result = this.scoreAttempt();
|
|
111
|
+
endAttempt(options = {}) {
|
|
112
|
+
const result = this.scoreAttempt(options);
|
|
105
113
|
if (!result)
|
|
106
114
|
return;
|
|
107
115
|
if (!this.documentModel?.item.adaptive ||
|
|
@@ -109,8 +117,11 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
|
|
|
109
117
|
this.session?.setStatus("completed");
|
|
110
118
|
}
|
|
111
119
|
this.updateAttemptAvailability();
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
const state = this.serialize();
|
|
121
|
+
if (!state)
|
|
122
|
+
return;
|
|
123
|
+
this.dispatchPlayerEvent("qti-endattempt", { state });
|
|
124
|
+
this.emitStateChange(state);
|
|
114
125
|
}
|
|
115
126
|
serialize() {
|
|
116
127
|
const state = this.session?.serialize();
|
|
@@ -119,7 +130,12 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
|
|
|
119
130
|
return state;
|
|
120
131
|
}
|
|
121
132
|
emitStateChange(state = this.serialize()) {
|
|
122
|
-
|
|
133
|
+
if (!state)
|
|
134
|
+
return;
|
|
135
|
+
this.dispatchPlayerEvent("qti-statechange", { state });
|
|
136
|
+
}
|
|
137
|
+
dispatchPlayerEvent(type, detail) {
|
|
138
|
+
this.dispatchEvent(new CustomEvent(type, { detail }));
|
|
123
139
|
}
|
|
124
140
|
render() {
|
|
125
141
|
const documentModel = this.documentModel;
|
|
@@ -128,12 +144,7 @@ export class QtiAssessmentItemPlayer extends HTMLElementBase {
|
|
|
128
144
|
this.applyDefaultStyles();
|
|
129
145
|
const root = document.createElement("article");
|
|
130
146
|
root.className = "qti3-player";
|
|
131
|
-
root.setAttribute("aria-labelledby", "qti3-item-title");
|
|
132
147
|
root.append(playerStyleElement());
|
|
133
|
-
const title = document.createElement("h2");
|
|
134
|
-
title.id = "qti3-item-title";
|
|
135
|
-
title.textContent = documentModel.item.title ?? documentModel.item.identifier;
|
|
136
|
-
root.append(title);
|
|
137
148
|
if (documentModel.item.prompt && documentModel.item.body.length === 0) {
|
|
138
149
|
const prompt = document.createElement("p");
|
|
139
150
|
prompt.className = "qti3-item-prompt";
|
|
@@ -600,11 +611,7 @@ export function defineQtiAssessmentItemPlayer() {
|
|
|
600
611
|
}
|
|
601
612
|
}
|
|
602
613
|
function renderChoice(interaction, update, currentValue) {
|
|
603
|
-
const group =
|
|
604
|
-
group.className = "qti3-choice-group";
|
|
605
|
-
const legend = document.createElement("legend");
|
|
606
|
-
legend.textContent = readableType(interaction.type);
|
|
607
|
-
group.append(legend);
|
|
614
|
+
const group = responseGroup("qti3-choice-group");
|
|
608
615
|
const multiple = interaction.responseCardinality === "multiple" || interaction.responseCardinality === "ordered";
|
|
609
616
|
const selected = new Set(valueToStrings(currentValue));
|
|
610
617
|
const list = document.createElement("div");
|
|
@@ -661,6 +668,30 @@ function renderChoice(interaction, update, currentValue) {
|
|
|
661
668
|
group.append(list);
|
|
662
669
|
return group;
|
|
663
670
|
}
|
|
671
|
+
function responseGroup(className) {
|
|
672
|
+
const group = document.createElement("div");
|
|
673
|
+
group.className = ["qti3-response-group", className].filter(Boolean).join(" ");
|
|
674
|
+
return group;
|
|
675
|
+
}
|
|
676
|
+
const movementGlyphs = {
|
|
677
|
+
up: "\u2191",
|
|
678
|
+
down: "\u2193",
|
|
679
|
+
left: "\u2190",
|
|
680
|
+
right: "\u2192",
|
|
681
|
+
};
|
|
682
|
+
function movementButton(direction, accessibleName, onClick) {
|
|
683
|
+
const button = document.createElement("button");
|
|
684
|
+
button.type = "button";
|
|
685
|
+
button.className = "qti3-icon-button";
|
|
686
|
+
button.dataset.moveDirection = direction;
|
|
687
|
+
button.textContent = movementGlyphs[direction];
|
|
688
|
+
button.setAttribute("aria-label", accessibleName);
|
|
689
|
+
button.addEventListener("click", onClick);
|
|
690
|
+
return button;
|
|
691
|
+
}
|
|
692
|
+
function movementLabel(target, direction) {
|
|
693
|
+
return `Move ${target} ${direction}`;
|
|
694
|
+
}
|
|
664
695
|
function renderHottextResponse(interaction, update, currentValue) {
|
|
665
696
|
const group = document.createElement("div");
|
|
666
697
|
group.className = "qti3-hottext-group";
|
|
@@ -757,10 +788,7 @@ function usesPairResponse(interaction) {
|
|
|
757
788
|
interaction.type === "graphicGapMatch");
|
|
758
789
|
}
|
|
759
790
|
function renderOrderedResponse(interaction, update, currentValue) {
|
|
760
|
-
const group =
|
|
761
|
-
const legend = document.createElement("legend");
|
|
762
|
-
legend.textContent = orderedResponseLegend(interaction.type);
|
|
763
|
-
group.append(legend);
|
|
791
|
+
const group = responseGroup();
|
|
764
792
|
appendGraphicContext(group, interaction);
|
|
765
793
|
const choices = choicesOrFallback(interaction).filter((choice) => choice.role !== "gap");
|
|
766
794
|
const ordered = orderChoicesFromValue(choices, currentValue);
|
|
@@ -850,20 +878,10 @@ function renderOrderedResponse(interaction, update, currentValue) {
|
|
|
850
878
|
moveChoice(index, index + 1);
|
|
851
879
|
}
|
|
852
880
|
});
|
|
853
|
-
const up =
|
|
854
|
-
up.type = "button";
|
|
855
|
-
up.className = "qti3-icon-button";
|
|
856
|
-
up.textContent = "Up";
|
|
881
|
+
const up = movementButton("up", movementLabel(choice.text, "up"), () => moveChoice(index, index - 1));
|
|
857
882
|
up.disabled = index === 0;
|
|
858
|
-
|
|
859
|
-
up.addEventListener("click", () => moveChoice(index, index - 1));
|
|
860
|
-
const down = document.createElement("button");
|
|
861
|
-
down.type = "button";
|
|
862
|
-
down.className = "qti3-icon-button";
|
|
863
|
-
down.textContent = "Down";
|
|
883
|
+
const down = movementButton("down", movementLabel(choice.text, "down"), () => moveChoice(index, index + 1));
|
|
864
884
|
down.disabled = index === ordered.length - 1;
|
|
865
|
-
down.setAttribute("aria-label", `Move ${choice.text} down`);
|
|
866
|
-
down.addEventListener("click", () => moveChoice(index, index + 1));
|
|
867
885
|
item.append(handle, up, down);
|
|
868
886
|
return item;
|
|
869
887
|
}));
|
|
@@ -873,10 +891,7 @@ function renderOrderedResponse(interaction, update, currentValue) {
|
|
|
873
891
|
return group;
|
|
874
892
|
}
|
|
875
893
|
function renderPairResponse(interaction, update, currentValue) {
|
|
876
|
-
const group =
|
|
877
|
-
const legend = document.createElement("legend");
|
|
878
|
-
legend.textContent = `${readableType(interaction.type)} pairs`;
|
|
879
|
-
group.append(legend);
|
|
894
|
+
const group = responseGroup();
|
|
880
895
|
appendGraphicContext(group, interaction);
|
|
881
896
|
const sources = sourceChoices(interaction);
|
|
882
897
|
const targets = targetChoices(interaction);
|
|
@@ -993,17 +1008,19 @@ function renderPairResponse(interaction, update, currentValue) {
|
|
|
993
1008
|
return group;
|
|
994
1009
|
}
|
|
995
1010
|
function renderMatchResponse(interaction, update, currentValue) {
|
|
996
|
-
const group =
|
|
997
|
-
const legend = document.createElement("legend");
|
|
998
|
-
legend.textContent = "Match rows";
|
|
999
|
-
group.append(legend);
|
|
1011
|
+
const group = responseGroup();
|
|
1000
1012
|
const sources = sourceChoices(interaction);
|
|
1001
1013
|
const targets = targetChoices(interaction);
|
|
1002
1014
|
const selectedPairs = valueToStrings(currentValue);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1015
|
+
let selectedSource;
|
|
1016
|
+
let selectedTarget;
|
|
1017
|
+
let draggedSource;
|
|
1018
|
+
const selector = document.createElement("div");
|
|
1019
|
+
selector.className = "qti3-match-selector";
|
|
1020
|
+
const sourceRegion = tokenRegion("Match sources");
|
|
1021
|
+
sourceRegion.classList.add("qti3-match-source-bank");
|
|
1022
|
+
const targetRegion = tokenRegion("Match targets");
|
|
1023
|
+
targetRegion.classList.add("qti3-match-target-bank");
|
|
1007
1024
|
const pairList = document.createElement("ul");
|
|
1008
1025
|
pairList.className = "qti3-pair-list";
|
|
1009
1026
|
pairList.setAttribute("aria-label", "Match selected pairs");
|
|
@@ -1013,17 +1030,27 @@ function renderMatchResponse(interaction, update, currentValue) {
|
|
|
1013
1030
|
else
|
|
1014
1031
|
update([...selectedPairs]);
|
|
1015
1032
|
};
|
|
1016
|
-
const syncPressed = () => {
|
|
1017
|
-
for (const button of grid.querySelectorAll(".qti3-match-target")) {
|
|
1018
|
-
const pair = `${button.dataset.sourceIdentifier} ${button.dataset.choiceIdentifier}`;
|
|
1019
|
-
button.setAttribute("aria-pressed", selectedPairs.includes(pair) ? "true" : "false");
|
|
1020
|
-
}
|
|
1021
|
-
};
|
|
1022
1033
|
const removePair = (pair) => {
|
|
1023
1034
|
const index = selectedPairs.indexOf(pair);
|
|
1024
1035
|
if (index >= 0)
|
|
1025
1036
|
selectedPairs.splice(index, 1);
|
|
1026
1037
|
};
|
|
1038
|
+
const syncPressed = () => {
|
|
1039
|
+
for (const button of sourceRegion.querySelectorAll("button")) {
|
|
1040
|
+
const identifier = button.dataset.choiceIdentifier ?? "";
|
|
1041
|
+
button.setAttribute("aria-pressed", identifier === selectedSource?.identifier ||
|
|
1042
|
+
selectedPairs.some((pair) => pair.startsWith(`${identifier} `))
|
|
1043
|
+
? "true"
|
|
1044
|
+
: "false");
|
|
1045
|
+
}
|
|
1046
|
+
for (const button of targetRegion.querySelectorAll("button")) {
|
|
1047
|
+
const identifier = button.dataset.choiceIdentifier ?? "";
|
|
1048
|
+
button.setAttribute("aria-pressed", identifier === selectedTarget?.identifier ||
|
|
1049
|
+
selectedPairs.some((pair) => pair.endsWith(` ${identifier}`))
|
|
1050
|
+
? "true"
|
|
1051
|
+
: "false");
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1027
1054
|
const renderPairs = () => {
|
|
1028
1055
|
pairList.replaceChildren(...selectedPairs.map((pair) => {
|
|
1029
1056
|
const [source, target] = pair.split(" ");
|
|
@@ -1046,53 +1073,117 @@ function renderMatchResponse(interaction, update, currentValue) {
|
|
|
1046
1073
|
return item;
|
|
1047
1074
|
}));
|
|
1048
1075
|
};
|
|
1076
|
+
const clearSelection = () => {
|
|
1077
|
+
selectedSource = undefined;
|
|
1078
|
+
selectedTarget = undefined;
|
|
1079
|
+
};
|
|
1080
|
+
const removePairsForSource = (source) => {
|
|
1081
|
+
for (const existing of selectedPairs.filter((pair) => pair.startsWith(`${source.identifier} `))) {
|
|
1082
|
+
removePair(existing);
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
const removePairsForTarget = (target) => {
|
|
1086
|
+
for (const existing of selectedPairs.filter((pair) => pair.endsWith(` ${target.identifier}`))) {
|
|
1087
|
+
removePair(existing);
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1049
1090
|
const togglePair = (source, target) => {
|
|
1050
1091
|
const pair = `${source.identifier} ${target.identifier}`;
|
|
1051
1092
|
if (selectedPairs.includes(pair)) {
|
|
1052
1093
|
removePair(pair);
|
|
1053
1094
|
}
|
|
1054
1095
|
else {
|
|
1096
|
+
if (interaction.responseCardinality === "single")
|
|
1097
|
+
selectedPairs.splice(0);
|
|
1055
1098
|
if (parseUnlimitedMaximum(source.attributes["match-max"]) === 1) {
|
|
1056
|
-
|
|
1057
|
-
for (const existing of existingSourcePairs)
|
|
1058
|
-
removePair(existing);
|
|
1099
|
+
removePairsForSource(source);
|
|
1059
1100
|
}
|
|
1060
1101
|
if (parseUnlimitedMaximum(target.attributes["match-max"]) === 1) {
|
|
1061
|
-
|
|
1062
|
-
for (const existing of existingTargetPairs)
|
|
1063
|
-
removePair(existing);
|
|
1102
|
+
removePairsForTarget(target);
|
|
1064
1103
|
}
|
|
1065
1104
|
selectedPairs.push(pair);
|
|
1066
1105
|
}
|
|
1106
|
+
clearSelection();
|
|
1067
1107
|
syncPressed();
|
|
1068
1108
|
renderPairs();
|
|
1069
1109
|
commit();
|
|
1070
1110
|
};
|
|
1111
|
+
const addSelectedPair = () => {
|
|
1112
|
+
if (!selectedSource || !selectedTarget)
|
|
1113
|
+
return;
|
|
1114
|
+
togglePair(selectedSource, selectedTarget);
|
|
1115
|
+
};
|
|
1116
|
+
const addPair = (sourceIdentifier, targetIdentifier) => {
|
|
1117
|
+
const source = sources.find((choice) => choice.identifier === sourceIdentifier);
|
|
1118
|
+
const target = targets.find((choice) => choice.identifier === targetIdentifier);
|
|
1119
|
+
if (!source || !target)
|
|
1120
|
+
return;
|
|
1121
|
+
togglePair(source, target);
|
|
1122
|
+
};
|
|
1071
1123
|
for (const source of sources) {
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1124
|
+
const button = tokenButton(source);
|
|
1125
|
+
button.classList.add("qti3-match-source");
|
|
1126
|
+
button.draggable = true;
|
|
1127
|
+
button.addEventListener("dragstart", (event) => {
|
|
1128
|
+
draggedSource = source.identifier;
|
|
1129
|
+
event.dataTransfer?.setData("text/plain", source.identifier);
|
|
1130
|
+
event.dataTransfer?.setDragImage(button, 8, 8);
|
|
1131
|
+
});
|
|
1132
|
+
button.addEventListener("dragend", () => {
|
|
1133
|
+
draggedSource = undefined;
|
|
1134
|
+
syncPressed();
|
|
1135
|
+
});
|
|
1136
|
+
button.addEventListener("click", () => {
|
|
1137
|
+
selectedSource = source;
|
|
1138
|
+
syncPressed();
|
|
1139
|
+
addSelectedPair();
|
|
1140
|
+
});
|
|
1141
|
+
button.addEventListener("keydown", (event) => {
|
|
1142
|
+
if (event.key !== "Delete" && event.key !== "Backspace")
|
|
1143
|
+
return;
|
|
1144
|
+
event.preventDefault();
|
|
1145
|
+
removePairsForSource(source);
|
|
1146
|
+
clearSelection();
|
|
1147
|
+
syncPressed();
|
|
1148
|
+
renderPairs();
|
|
1149
|
+
commit();
|
|
1150
|
+
});
|
|
1151
|
+
sourceRegion.append(button);
|
|
1092
1152
|
}
|
|
1153
|
+
for (const target of targets) {
|
|
1154
|
+
const button = tokenButton(target);
|
|
1155
|
+
button.classList.add("qti3-match-target");
|
|
1156
|
+
button.addEventListener("dragover", (event) => {
|
|
1157
|
+
event.preventDefault();
|
|
1158
|
+
button.classList.add("qti3-drop-target");
|
|
1159
|
+
});
|
|
1160
|
+
button.addEventListener("dragleave", () => button.classList.remove("qti3-drop-target"));
|
|
1161
|
+
button.addEventListener("drop", (event) => {
|
|
1162
|
+
event.preventDefault();
|
|
1163
|
+
button.classList.remove("qti3-drop-target");
|
|
1164
|
+
addPair(event.dataTransfer?.getData("text/plain") || draggedSource, target.identifier);
|
|
1165
|
+
});
|
|
1166
|
+
button.addEventListener("click", () => {
|
|
1167
|
+
selectedTarget = target;
|
|
1168
|
+
syncPressed();
|
|
1169
|
+
addSelectedPair();
|
|
1170
|
+
});
|
|
1171
|
+
button.addEventListener("keydown", (event) => {
|
|
1172
|
+
if (event.key !== "Delete" && event.key !== "Backspace")
|
|
1173
|
+
return;
|
|
1174
|
+
event.preventDefault();
|
|
1175
|
+
removePairsForTarget(target);
|
|
1176
|
+
clearSelection();
|
|
1177
|
+
syncPressed();
|
|
1178
|
+
renderPairs();
|
|
1179
|
+
commit();
|
|
1180
|
+
});
|
|
1181
|
+
targetRegion.append(button);
|
|
1182
|
+
}
|
|
1183
|
+
selector.append(sourceRegion, targetRegion);
|
|
1093
1184
|
syncPressed();
|
|
1094
1185
|
renderPairs();
|
|
1095
|
-
group.append(
|
|
1186
|
+
group.append(selector, pairList);
|
|
1096
1187
|
return group;
|
|
1097
1188
|
}
|
|
1098
1189
|
function pairRegionLabels(interaction) {
|
|
@@ -1103,10 +1194,7 @@ function pairRegionLabels(interaction) {
|
|
|
1103
1194
|
return { source: "Source", target: "Target" };
|
|
1104
1195
|
}
|
|
1105
1196
|
function renderGraphicOrderResponse(interaction, update, currentValue) {
|
|
1106
|
-
const group =
|
|
1107
|
-
const legend = document.createElement("legend");
|
|
1108
|
-
legend.textContent = readableType(interaction.type);
|
|
1109
|
-
group.append(legend);
|
|
1197
|
+
const group = responseGroup();
|
|
1110
1198
|
const width = objectWidth(interaction);
|
|
1111
1199
|
const height = objectHeight(interaction);
|
|
1112
1200
|
const choices = choicesOrFallback(interaction).filter((choice) => choice.role === "hotspot");
|
|
@@ -1239,12 +1327,14 @@ function renderGraphicOrderResponse(interaction, update, currentValue) {
|
|
|
1239
1327
|
list.replaceChildren(...currentChoices.map((choice, index) => {
|
|
1240
1328
|
const item = document.createElement("li");
|
|
1241
1329
|
item.className = "qti3-graphic-order-item";
|
|
1330
|
+
item.dataset.choiceIdentifier = choice.identifier;
|
|
1331
|
+
const choiceLabel = hotspotDisplayLabel(choice, choices);
|
|
1242
1332
|
const label = document.createElement("button");
|
|
1243
1333
|
label.type = "button";
|
|
1244
1334
|
label.className = "qti3-token";
|
|
1245
1335
|
label.dataset.choiceIdentifier = choice.identifier;
|
|
1246
|
-
label.textContent = `${index + 1}. ${
|
|
1247
|
-
label.setAttribute("aria-label", `${
|
|
1336
|
+
label.textContent = `${index + 1}. ${choiceLabel}`;
|
|
1337
|
+
label.setAttribute("aria-label", `${choiceLabel}, position ${index + 1} of ${currentChoices.length}`);
|
|
1248
1338
|
label.addEventListener("click", () => focusHotspot(choice.identifier));
|
|
1249
1339
|
label.addEventListener("keydown", (event) => {
|
|
1250
1340
|
if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
|
|
@@ -1260,22 +1350,14 @@ function renderGraphicOrderResponse(interaction, update, currentValue) {
|
|
|
1260
1350
|
removeHotspot(choice.identifier);
|
|
1261
1351
|
}
|
|
1262
1352
|
});
|
|
1263
|
-
const up =
|
|
1264
|
-
up.type = "button";
|
|
1265
|
-
up.textContent = "Up";
|
|
1353
|
+
const up = movementButton("up", movementLabel(choiceLabel, "up"), () => moveHotspot(choice.identifier, -1));
|
|
1266
1354
|
up.disabled = index === 0;
|
|
1267
|
-
|
|
1268
|
-
up.addEventListener("click", () => moveHotspot(choice.identifier, -1));
|
|
1269
|
-
const down = document.createElement("button");
|
|
1270
|
-
down.type = "button";
|
|
1271
|
-
down.textContent = "Down";
|
|
1355
|
+
const down = movementButton("down", movementLabel(choiceLabel, "down"), () => moveHotspot(choice.identifier, 1));
|
|
1272
1356
|
down.disabled = index === currentChoices.length - 1;
|
|
1273
|
-
down.setAttribute("aria-label", `Move ${hotspotDisplayLabel(choice, choices)} down`);
|
|
1274
|
-
down.addEventListener("click", () => moveHotspot(choice.identifier, 1));
|
|
1275
1357
|
const remove = document.createElement("button");
|
|
1276
1358
|
remove.type = "button";
|
|
1277
1359
|
remove.textContent = "Remove";
|
|
1278
|
-
remove.setAttribute("aria-label", `Remove ${
|
|
1360
|
+
remove.setAttribute("aria-label", `Remove ${choiceLabel}`);
|
|
1279
1361
|
remove.addEventListener("click", () => removeHotspot(choice.identifier));
|
|
1280
1362
|
item.append(label, up, down, remove);
|
|
1281
1363
|
return item;
|
|
@@ -1320,10 +1402,7 @@ function renderGraphicOrderResponse(interaction, update, currentValue) {
|
|
|
1320
1402
|
return group;
|
|
1321
1403
|
}
|
|
1322
1404
|
function renderGraphicAssociateResponse(interaction, update, currentValue) {
|
|
1323
|
-
const group =
|
|
1324
|
-
const legend = document.createElement("legend");
|
|
1325
|
-
legend.textContent = readableType(interaction.type);
|
|
1326
|
-
group.append(legend);
|
|
1405
|
+
const group = responseGroup();
|
|
1327
1406
|
const width = objectWidth(interaction);
|
|
1328
1407
|
const height = objectHeight(interaction);
|
|
1329
1408
|
const choices = choicesOrFallback(interaction).filter((choice) => choice.role === "hotspot");
|
|
@@ -1515,10 +1594,7 @@ function renderGraphicAssociateResponse(interaction, update, currentValue) {
|
|
|
1515
1594
|
return group;
|
|
1516
1595
|
}
|
|
1517
1596
|
function renderGapMatchResponse(interaction, update, currentValue) {
|
|
1518
|
-
const group =
|
|
1519
|
-
const legend = document.createElement("legend");
|
|
1520
|
-
legend.textContent = readableType(interaction.type);
|
|
1521
|
-
group.append(legend);
|
|
1597
|
+
const group = responseGroup();
|
|
1522
1598
|
appendGraphicContext(group, interaction);
|
|
1523
1599
|
const sources = sourceChoices(interaction);
|
|
1524
1600
|
const gaps = targetChoices(interaction);
|
|
@@ -1573,20 +1649,20 @@ function renderGapMatchResponse(interaction, update, currentValue) {
|
|
|
1573
1649
|
const button = document.createElement("button");
|
|
1574
1650
|
button.type = "button";
|
|
1575
1651
|
button.className = "qti3-gap-button";
|
|
1576
|
-
button.textContent = assigned ? assigned.text : "
|
|
1652
|
+
button.textContent = assigned ? assigned.text : "";
|
|
1577
1653
|
button.setAttribute("aria-label", assigned ? `${gapLabel}, assigned ${assigned.text}` : `${gapLabel}, empty`);
|
|
1578
1654
|
button.addEventListener("click", () => assign(gap, selectedSource?.identifier));
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1655
|
+
button.addEventListener("keydown", (event) => {
|
|
1656
|
+
if (event.key !== "Delete" && event.key !== "Backspace")
|
|
1657
|
+
return;
|
|
1658
|
+
if (!assignments.has(gap.identifier))
|
|
1659
|
+
return;
|
|
1660
|
+
event.preventDefault();
|
|
1585
1661
|
assignments.delete(gap.identifier);
|
|
1586
1662
|
renderGaps();
|
|
1587
1663
|
commit();
|
|
1588
1664
|
});
|
|
1589
|
-
target.append(button
|
|
1665
|
+
target.append(button);
|
|
1590
1666
|
return target;
|
|
1591
1667
|
};
|
|
1592
1668
|
const renderGaps = () => {
|
|
@@ -1635,7 +1711,7 @@ function renderSelect(interaction, update, currentValue) {
|
|
|
1635
1711
|
const [selected] = valueToStrings(currentValue);
|
|
1636
1712
|
if (selected)
|
|
1637
1713
|
select.value = selected;
|
|
1638
|
-
select.addEventListener("change", () => update(select.value));
|
|
1714
|
+
select.addEventListener("change", () => update(select.value === "" ? null : select.value));
|
|
1639
1715
|
return select;
|
|
1640
1716
|
}
|
|
1641
1717
|
function appendInlineControl(content, control, nextSegment) {
|
|
@@ -1737,7 +1813,7 @@ function renderSliderResponse(interaction, update, currentValue) {
|
|
|
1737
1813
|
const sync = () => {
|
|
1738
1814
|
output.value = input.value;
|
|
1739
1815
|
output.textContent = input.value;
|
|
1740
|
-
update(input.value);
|
|
1816
|
+
update(coerceResponseInputValue(input.value, interaction.responseBaseType));
|
|
1741
1817
|
};
|
|
1742
1818
|
input.addEventListener("input", sync);
|
|
1743
1819
|
group.append(input, output);
|
|
@@ -1905,25 +1981,20 @@ function renderSelectPointResponse(interaction, update, currentValue) {
|
|
|
1905
1981
|
syncMarker();
|
|
1906
1982
|
const controls = document.createElement("div");
|
|
1907
1983
|
controls.className = "qti3-point-controls";
|
|
1908
|
-
for (const [
|
|
1909
|
-
["
|
|
1910
|
-
["
|
|
1911
|
-
["
|
|
1912
|
-
["
|
|
1984
|
+
for (const [direction, dx, dy] of [
|
|
1985
|
+
["up", 0, -1],
|
|
1986
|
+
["left", -1, 0],
|
|
1987
|
+
["right", 1, 0],
|
|
1988
|
+
["down", 0, 1],
|
|
1913
1989
|
]) {
|
|
1914
|
-
|
|
1915
|
-
button.type = "button";
|
|
1916
|
-
button.textContent = label;
|
|
1917
|
-
button.setAttribute("aria-label", `Move point ${label.toLowerCase()}`);
|
|
1918
|
-
button.addEventListener("click", () => {
|
|
1990
|
+
controls.append(movementButton(direction, movementLabel("point", direction), () => {
|
|
1919
1991
|
const point = mutableActivePoint();
|
|
1920
1992
|
point.x += dx;
|
|
1921
1993
|
point.y += dy;
|
|
1922
1994
|
clampPoint(point);
|
|
1923
1995
|
syncMarker();
|
|
1924
1996
|
commit();
|
|
1925
|
-
});
|
|
1926
|
-
controls.append(button);
|
|
1997
|
+
}));
|
|
1927
1998
|
}
|
|
1928
1999
|
if (isMultiple) {
|
|
1929
2000
|
const clear = document.createElement("button");
|
|
@@ -2097,18 +2168,13 @@ function renderPositionObjectResponse(interaction, update, currentValue) {
|
|
|
2097
2168
|
marker.addEventListener("keydown", handleKey);
|
|
2098
2169
|
const controls = document.createElement("div");
|
|
2099
2170
|
controls.className = "qti3-point-controls";
|
|
2100
|
-
for (const [
|
|
2101
|
-
["
|
|
2102
|
-
["
|
|
2103
|
-
["
|
|
2104
|
-
["
|
|
2171
|
+
for (const [direction, dx, dy] of [
|
|
2172
|
+
["up", 0, -1],
|
|
2173
|
+
["left", -1, 0],
|
|
2174
|
+
["right", 1, 0],
|
|
2175
|
+
["down", 0, 1],
|
|
2105
2176
|
]) {
|
|
2106
|
-
|
|
2107
|
-
button.type = "button";
|
|
2108
|
-
button.textContent = label;
|
|
2109
|
-
button.setAttribute("aria-label", `Move object ${label.toLowerCase()}`);
|
|
2110
|
-
button.addEventListener("click", () => moveBy(dx, dy));
|
|
2111
|
-
controls.append(button);
|
|
2177
|
+
controls.append(movementButton(direction, movementLabel("object", direction), () => moveBy(dx, dy)));
|
|
2112
2178
|
}
|
|
2113
2179
|
syncMarker();
|
|
2114
2180
|
group.append(stage, coordinate, controls);
|
|
@@ -2265,10 +2331,7 @@ function renderPortableCustomResponse(interaction, update, currentValue) {
|
|
|
2265
2331
|
return group;
|
|
2266
2332
|
}
|
|
2267
2333
|
function renderHotspotResponse(interaction, update, currentValue) {
|
|
2268
|
-
const group =
|
|
2269
|
-
const legend = document.createElement("legend");
|
|
2270
|
-
legend.textContent = `${readableType(interaction.type)} regions`;
|
|
2271
|
-
group.append(legend);
|
|
2334
|
+
const group = responseGroup();
|
|
2272
2335
|
const surface = document.createElement("div");
|
|
2273
2336
|
surface.className = "qti3-hotspot-surface";
|
|
2274
2337
|
const width = objectWidth(interaction);
|
|
@@ -2398,7 +2461,7 @@ function objectIsImage(object) {
|
|
|
2398
2461
|
function appendOptions(select, choices) {
|
|
2399
2462
|
const empty = document.createElement("option");
|
|
2400
2463
|
empty.value = "";
|
|
2401
|
-
empty.textContent = "
|
|
2464
|
+
empty.textContent = "";
|
|
2402
2465
|
select.append(empty);
|
|
2403
2466
|
for (const choice of choices) {
|
|
2404
2467
|
const option = document.createElement("option");
|
|
@@ -2480,6 +2543,19 @@ function scalarString(value) {
|
|
|
2480
2543
|
return "";
|
|
2481
2544
|
return String(value);
|
|
2482
2545
|
}
|
|
2546
|
+
function coerceResponseInputValue(value, baseType) {
|
|
2547
|
+
if (baseType === "integer")
|
|
2548
|
+
return Number.parseInt(value, 10);
|
|
2549
|
+
if (baseType === "float")
|
|
2550
|
+
return Number.parseFloat(value);
|
|
2551
|
+
if (baseType === "boolean") {
|
|
2552
|
+
if (value === "true")
|
|
2553
|
+
return true;
|
|
2554
|
+
if (value === "false")
|
|
2555
|
+
return false;
|
|
2556
|
+
}
|
|
2557
|
+
return value;
|
|
2558
|
+
}
|
|
2483
2559
|
function orderChoicesFromValue(choices, value) {
|
|
2484
2560
|
const identifiers = valueToStrings(value);
|
|
2485
2561
|
if (identifiers.length === 0)
|
|
@@ -2898,11 +2974,6 @@ function readableType(type) {
|
|
|
2898
2974
|
.replace(/[A-Z]/g, (letter) => ` ${letter.toLowerCase()}`)
|
|
2899
2975
|
.replace(/^./, (letter) => letter.toUpperCase());
|
|
2900
2976
|
}
|
|
2901
|
-
function orderedResponseLegend(type) {
|
|
2902
|
-
if (type === "order")
|
|
2903
|
-
return readableType(type);
|
|
2904
|
-
return `${readableType(type)} order`;
|
|
2905
|
-
}
|
|
2906
2977
|
function errorView(message) {
|
|
2907
2978
|
const element = document.createElement("p");
|
|
2908
2979
|
element.role = "alert";
|
|
@@ -2938,12 +3009,24 @@ function playerStyleElement() {
|
|
|
2938
3009
|
const style = document.createElement("style");
|
|
2939
3010
|
style.textContent = `
|
|
2940
3011
|
.qti3-player {
|
|
3012
|
+
--qti3-match-accent: #2f6fca;
|
|
3013
|
+
--qti3-match-target-bg: #f5f6f7;
|
|
3014
|
+
--qti3-match-target-border: #6f7782;
|
|
3015
|
+
|
|
2941
3016
|
display: grid;
|
|
2942
3017
|
gap: 1rem;
|
|
2943
3018
|
max-inline-size: 72rem;
|
|
2944
3019
|
font: 16px/1.45 system-ui, sans-serif;
|
|
2945
3020
|
}
|
|
2946
3021
|
|
|
3022
|
+
@supports (color: light-dark(#000, #fff)) {
|
|
3023
|
+
.qti3-player {
|
|
3024
|
+
--qti3-match-accent: light-dark(#2f6fca, #8ab4f8);
|
|
3025
|
+
--qti3-match-target-bg: light-dark(#f5f6f7, #202124);
|
|
3026
|
+
--qti3-match-target-border: light-dark(#6f7782, #9aa0a6);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
2947
3030
|
.qti3-interaction {
|
|
2948
3031
|
display: grid;
|
|
2949
3032
|
gap: 0.75rem;
|
|
@@ -2984,10 +3067,14 @@ function playerStyleElement() {
|
|
|
2984
3067
|
color: CanvasText;
|
|
2985
3068
|
}
|
|
2986
3069
|
|
|
2987
|
-
.qti3-
|
|
3070
|
+
.qti3-response-group {
|
|
2988
3071
|
min-inline-size: 0;
|
|
2989
3072
|
}
|
|
2990
3073
|
|
|
3074
|
+
.qti3-response-group > * + * {
|
|
3075
|
+
margin-block-start: 0.75rem;
|
|
3076
|
+
}
|
|
3077
|
+
|
|
2991
3078
|
.qti3-actions,
|
|
2992
3079
|
.qti3-reorder-item,
|
|
2993
3080
|
.qti3-token-region,
|
|
@@ -3013,52 +3100,50 @@ function playerStyleElement() {
|
|
|
3013
3100
|
align-items: start;
|
|
3014
3101
|
}
|
|
3015
3102
|
|
|
3016
|
-
.qti3-match-
|
|
3103
|
+
.qti3-match-selector {
|
|
3017
3104
|
display: grid;
|
|
3018
|
-
gap:
|
|
3105
|
+
gap: 1.5rem;
|
|
3019
3106
|
inline-size: 100%;
|
|
3020
3107
|
max-inline-size: 72rem;
|
|
3021
3108
|
box-sizing: border-box;
|
|
3022
3109
|
}
|
|
3023
3110
|
|
|
3024
|
-
.qti3-match-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
gap: 0.75rem;
|
|
3028
|
-
align-items: start;
|
|
3029
|
-
inline-size: 100%;
|
|
3030
|
-
min-inline-size: 0;
|
|
3031
|
-
box-sizing: border-box;
|
|
3032
|
-
padding-block: 0.5rem;
|
|
3033
|
-
border-block-end: 1px solid CanvasText;
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
.qti3-match-source {
|
|
3037
|
-
font-weight: 700;
|
|
3111
|
+
.qti3-match-source-bank,
|
|
3112
|
+
.qti3-match-target-bank {
|
|
3113
|
+
align-items: stretch;
|
|
3038
3114
|
}
|
|
3039
3115
|
|
|
3040
|
-
.qti3-match-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
inline-size: 100%;
|
|
3045
|
-
min-inline-size: 0;
|
|
3046
|
-
box-sizing: border-box;
|
|
3116
|
+
.qti3-token.qti3-match-source {
|
|
3117
|
+
border-color: var(--qti3-match-accent);
|
|
3118
|
+
background: Canvas;
|
|
3119
|
+
color: var(--qti3-match-accent);
|
|
3047
3120
|
}
|
|
3048
3121
|
|
|
3049
|
-
.qti3-match-target {
|
|
3050
|
-
flex: 1 1
|
|
3122
|
+
.qti3-token.qti3-match-target {
|
|
3123
|
+
flex: 1 1 9rem;
|
|
3051
3124
|
min-inline-size: 0;
|
|
3052
3125
|
max-inline-size: 100%;
|
|
3126
|
+
min-block-size: 5rem;
|
|
3053
3127
|
box-sizing: border-box;
|
|
3128
|
+
border-color: var(--qti3-match-target-border);
|
|
3129
|
+
background: var(--qti3-match-target-bg);
|
|
3130
|
+
color: CanvasText;
|
|
3131
|
+
font-weight: 700;
|
|
3054
3132
|
white-space: normal;
|
|
3055
3133
|
overflow-wrap: anywhere;
|
|
3056
|
-
text-align:
|
|
3134
|
+
text-align: center;
|
|
3057
3135
|
}
|
|
3058
3136
|
|
|
3059
|
-
@media (
|
|
3060
|
-
.qti3-match-
|
|
3061
|
-
|
|
3137
|
+
@media (forced-colors: active) {
|
|
3138
|
+
.qti3-token.qti3-match-source {
|
|
3139
|
+
border-color: LinkText;
|
|
3140
|
+
color: LinkText;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
.qti3-token.qti3-match-target {
|
|
3144
|
+
border-color: GrayText;
|
|
3145
|
+
background: ButtonFace;
|
|
3146
|
+
color: ButtonText;
|
|
3062
3147
|
}
|
|
3063
3148
|
}
|
|
3064
3149
|
|
|
@@ -3172,6 +3257,15 @@ function playerStyleElement() {
|
|
|
3172
3257
|
cursor: grab;
|
|
3173
3258
|
}
|
|
3174
3259
|
|
|
3260
|
+
.qti3-icon-button {
|
|
3261
|
+
display: inline-grid;
|
|
3262
|
+
place-items: center;
|
|
3263
|
+
inline-size: 2.25rem;
|
|
3264
|
+
block-size: 2.25rem;
|
|
3265
|
+
padding: 0;
|
|
3266
|
+
line-height: 1;
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3175
3269
|
.qti3-token[aria-pressed="true"],
|
|
3176
3270
|
.qti3-pair-chip {
|
|
3177
3271
|
background: Highlight;
|
|
@@ -3207,6 +3301,8 @@ function playerStyleElement() {
|
|
|
3207
3301
|
|
|
3208
3302
|
.qti3-gap-passage .qti3-gap-target {
|
|
3209
3303
|
display: inline-flex;
|
|
3304
|
+
padding: 0;
|
|
3305
|
+
border: 0;
|
|
3210
3306
|
margin-inline: 0.15rem;
|
|
3211
3307
|
margin-block: 0.2rem;
|
|
3212
3308
|
vertical-align: middle;
|
|
@@ -3214,6 +3310,7 @@ function playerStyleElement() {
|
|
|
3214
3310
|
|
|
3215
3311
|
.qti3-gap-button {
|
|
3216
3312
|
min-inline-size: 8rem;
|
|
3313
|
+
min-block-size: 2.25rem;
|
|
3217
3314
|
text-align: start;
|
|
3218
3315
|
}
|
|
3219
3316
|
|