diff --git a/jupyterquiz/dynamic/display.py b/jupyterquiz/dynamic/display.py
index 9c1b9c2..69bd2e2 100644
--- a/jupyterquiz/dynamic/display.py
+++ b/jupyterquiz/dynamic/display.py
@@ -43,6 +43,7 @@ def display_quiz(ref, num=1_000_000, shuffle_questions=False,
'--jq-numeric-input-shadow': '#999999',
'--jq-string-bg': '#4c1a57',
'--jq-incorrect-color': '#c80202',
+ '--jq-select-color': '#6f78ff',
'--jq-correct-color': '#009113',
'--jq-text-color': '#fafafa'
}
diff --git a/jupyterquiz/js/multiple_choice.js b/jupyterquiz/js/multiple_choice.js
index 9143b86..a0dce48 100644
--- a/jupyterquiz/js/multiple_choice.js
+++ b/jupyterquiz/js/multiple_choice.js
@@ -8,6 +8,7 @@ function check_mc() {
//console.log("In check_mc(), id="+id);
//console.log(event.srcElement.id)
//console.log(event.srcElement.dataset.correct)
+ //console.log(event.srcElement.dataset.hide)
//console.log(event.srcElement.dataset.feedback)
var label = event.srcElement;
@@ -48,96 +49,193 @@ function check_mc() {
//console.log(id, ", got numcorrect=",fb.dataset.numcorrect);
var responses=JSON.parse(responsesContainer.dataset.responses);
console.log(responses);
- responses[qnum]= response;
+ if (label.dataset.selected == "true") {
+ // If it's already selected, it will be deselected now
+ responses[qnum] = null;
+ } else {
+ responses[qnum]= response;
+ }
responsesContainer.setAttribute('data-responses', JSON.stringify(responses));
printResponses(responsesContainer);
}
// End code to preserve responses
for (var i = 0; i < answers.length; i++) {
+ // Deselect all other buttons
var child = answers[i];
//console.log(child);
- child.className = "MCButton";
+ if (child.id != label.id) {
+ if (child.classList.contains("selectedButton")) {
+ child.setAttribute('data-selected', "false")
+ child.classList.remove("selectedButton");
+ void child.offsetWidth;
+ child.classList.add("deselectedButton");
+ }
+ if (child.classList.contains("incorrectButton"))
+ child.classList.remove("incorrectButton");
+ if (child.classList.contains("correctButton"))
+ child.classList.remove("correctButton");
+ }
}
-
- if (label.dataset.correct == "true") {
- // console.log("Correct action");
- if ("feedback" in label.dataset) {
- fb.innerHTML = jaxify(label.dataset.feedback);
+ if (label.dataset.hide == "true") {
+ if (label.dataset.selected == "true") {
+ label.setAttribute('data-selected', "false");
+ label.classList.remove("selectedButton");
+ void label.offsetWidth;
+ label.classList.add("deselectedButton");
+ fb.innerHTML = "Deselected.";
+ fb.classList.remove("selected");
+ fb.classList.add("deselected");
} else {
- fb.innerHTML = "Correct!";
+ label.setAttribute('data-selected', "true");
+ if ("feedback" in label.dataset) {
+ fb.innerHTML = jaxify(label.dataset.feedback);
+ } else {
+ fb.innerHTML = "Selected.";
+ }
+ if (label.classList.contains("deselectedButton")) {
+ label.classList.remove("deselectedButton");
+ };
+ void label.offsetWidth;
+ label.classList.add("selectedButton");
+ fb.className = "Feedback";
+ if (fb.classList.contains("deselected")) {
+ fb.classList.remove("deselected");
+ }
+ fb.classList.add("selected");
}
- label.classList.add("correctButton");
+ } else {
+ if (label.dataset.correct == "true") {
+ // console.log("Correct action");
+ if ("feedback" in label.dataset) {
+ fb.innerHTML = jaxify(label.dataset.feedback);
+ } else {
+ fb.innerHTML = "Correct!";
+ }
+ label.classList.add("correctButton");
- fb.className = "Feedback";
- fb.classList.add("correct");
+ fb.className = "Feedback";
+ fb.classList.add("correct");
- } else {
- if ("feedback" in label.dataset) {
- fb.innerHTML = jaxify(label.dataset.feedback);
} else {
- fb.innerHTML = "Incorrect -- try again.";
+ if ("feedback" in label.dataset) {
+ fb.innerHTML = jaxify(label.dataset.feedback);
+ } else {
+ fb.innerHTML = "Incorrect -- try again.";
+ }
+ //console.log("Error action");
+ label.classList.add("incorrectButton");
+ fb.className = "Feedback";
+ fb.classList.add("incorrect");
}
- //console.log("Error action");
- label.classList.add("incorrectButton");
- fb.className = "Feedback";
- fb.classList.add("incorrect");
}
}
else { /* Many choice (more than 1 correct answer) */
var reset = false;
var feedback;
- if (label.dataset.correct == "true") {
- if ("feedback" in label.dataset) {
- feedback = jaxify(label.dataset.feedback);
+
+ if (label.dataset.hide == "true") {
+ if (label.dataset.selected == "true") {
+ label.setAttribute('data-selected', "false");
+ label.classList.remove("selectedButton");
+ void label.offsetWidth;
+ label.classList.add("deselectedButton");
+ feedback = "Deselected.";
+ fb.classList.remove("selected");
+ fb.classList.add("deselected");
} else {
- feedback = "Correct!";
+ label.setAttribute('data-selected', "true");
+ if ("feedback" in label.dataset) {
+ feedback = jaxify(label.dataset.feedback);
+ } else {
+ feedback = "Selected.";
+ }
+ if (label.classList.contains("deselectedButton")) {
+ label.classList.remove("deselectedButton");
+ };
+ void label.offsetWidth;
+ label.classList.add("selectedButton");
+ fb.className = "Feedback";
+ if (fb.classList.contains("deselected")) {
+ fb.classList.remove("deselected");
+ }
+ fb.classList.add("selected");
}
- if (label.dataset.answered <= 0) {
- if (fb.dataset.answeredcorrect < 0) {
- fb.dataset.answeredcorrect = 1;
+ } else {
+ if (label.dataset.correct == "true") {
+ if ("feedback" in label.dataset) {
+ feedback = jaxify(label.dataset.feedback);
+ } else {
+ feedback = "Correct!";
+ }
+ if (label.dataset.answered <= 0) {
+ if (fb.dataset.answeredcorrect < 0) {
+ fb.dataset.answeredcorrect = 1;
+ reset = true;
+ } else {
+ fb.dataset.answeredcorrect++;
+ }
+ if (reset) {
+ for (var i = 0; i < answers.length; i++) {
+ var child = answers[i];
+ if (child.id != label.id) {
+ if (child.dataset.selected == "true") {
+ child.setAttribute('data-selected', "false");
+ child.classList.remove("selectedButton");
+ void child.offsetWidth;
+ child.classList.add("deselectedButton");
+ }
+ if (child.classList.contains("correctButton"))
+ child.classList.remove("correctButton");
+ if (child.classList.contains("incorrectButton"))
+ child.classList.remove("incorrectButton");
+ child.dataset.answered = 0;
+ }
+ }
+ }
+ label.classList.add("correctButton");
+ label.dataset.answered = 1;
+ fb.className = "Feedback";
+ fb.classList.add("correct");
+
+ }
+ } else {
+ if ("feedback" in label.dataset) {
+ feedback = jaxify(label.dataset.feedback);
+ } else {
+ feedback = "Incorrect -- try again.";
+ }
+ if (fb.dataset.answeredcorrect > 0) {
+ fb.dataset.answeredcorrect = -1;
reset = true;
} else {
- fb.dataset.answeredcorrect++;
+ fb.dataset.answeredcorrect--;
}
+
if (reset) {
for (var i = 0; i < answers.length; i++) {
var child = answers[i];
- child.className = "MCButton";
- child.dataset.answered = 0;
+ if (child.id != label.id) {
+ if (child.dataset.selected == "true") {
+ child.setAttribute('data-selected', "false");
+ child.classList.remove("selectedButton");
+ void child.offsetWidth;
+ child.classList.add("deselectedButton");
+ }
+ if (child.classList.contains("correctButton"))
+ child.classList.remove("correctButton");
+ if (child.classList.contains("incorrectButton"))
+ child.classList.remove("incorrectButton");
+ child.dataset.answered = 0;
+ }
}
}
- label.classList.add("correctButton");
- label.dataset.answered = 1;
+ label.classList.add("incorrectButton");
fb.className = "Feedback";
- fb.classList.add("correct");
-
- }
- } else {
- if ("feedback" in label.dataset) {
- feedback = jaxify(label.dataset.feedback);
- } else {
- feedback = "Incorrect -- try again.";
+ fb.classList.add("incorrect");
}
- if (fb.dataset.answeredcorrect > 0) {
- fb.dataset.answeredcorrect = -1;
- reset = true;
- } else {
- fb.dataset.answeredcorrect--;
- }
-
- if (reset) {
- for (var i = 0; i < answers.length; i++) {
- var child = answers[i];
- child.className = "MCButton";
- child.dataset.answered = 0;
- }
- }
- label.classList.add("incorrectButton");
- fb.className = "Feedback";
- fb.classList.add("incorrect");
}
// What follows is for the saved responses stuff
var outerContainer = fb.parentElement.parentElement;
@@ -153,17 +251,19 @@ function check_mc() {
var qnum = document.getElementById("quizWrap"+id).dataset.qnum;
console.log("Question " + qnum);
//console.log(id, ", got numcorrect=",fb.dataset.numcorrect);
- var responses=JSON.parse(responsesContainer.dataset.responses);
- if (label.dataset.correct == "true") {
- if (typeof(responses[qnum]) == "object"){
- if (!responses[qnum].includes(response))
- responses[qnum].push(response);
- } else{
- responses[qnum]= [ response ];
- }
+ var responses = JSON.parse(responsesContainer.dataset.responses);
+ if (typeof(responses[qnum]) == "object") {
+ var these_responses = new Set(responses[qnum]);
} else {
- responses[qnum]= response;
+ var these_responses = new Set();
}
+
+ if (label.dataset.selected == "true") {
+ these_responses.add(response);
+ } else {
+ these_responses.delete(response);
+ }
+ responses[qnum] = Array.from(these_responses);
console.log(responses);
responsesContainer.setAttribute('data-responses', JSON.stringify(responses));
printResponses(responsesContainer);
@@ -257,6 +357,12 @@ function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {
//lab.textContent=item.answer;
// Set the data attributes for the answer
+ lab.setAttribute('data-selected', "false")
+ if ("hide" in item) {
+ lab.setAttribute('data-hide', item.hide);
+ } else {
+ lab.setAttribute('data-hide', "false");
+ }
lab.setAttribute('data-correct', item.correct);
if (item.correct) {
num_correct++;
diff --git a/jupyterquiz/styles.css b/jupyterquiz/styles.css
index c157f8c..e31977c 100644
--- a/jupyterquiz/styles.css
+++ b/jupyterquiz/styles.css
@@ -89,7 +89,7 @@
line-height: 20pt;
border: none;
border-radius: 0.2rem;
- transition: box-shadow 0.1s);
+ transition: box-shadow 0.1s;
}
.Input-text:focus {
@@ -165,6 +165,10 @@
color: var(--jq-incorrect-color);
}
+.select {
+ color: var(--jq-select-color);
+}
+
.correct {
color: var(--jq-correct-color);
}
@@ -180,6 +184,21 @@
/*outline: none;*/
}
+.selectedButton {
+ animation: select-anim 0.8s ease;
+ animation-fill-mode: forwards;
+ box-shadow: inset 0 0 5px var(--jq-mc-button-inset-shadow);
+ color: var(--jq-text-color);
+ /*outline: none;*/
+}
+
+.deselectedButton {
+ animation: select-anim 0.4s ease;
+ animation-direction: reverse;
+ color: inherit;
+ /*outline: none;*/
+}
+
.incorrectButton {
animation: incorrect-anim 0.8s ease;
animation-fill-mode: forwards;
@@ -194,6 +213,12 @@
}
}
+@keyframes select-anim {
+ 100% {
+ background-color: var(--jq-select-color);
+ }
+}
+
@keyframes correct-anim {
100% {
background-color: var(--jq-correct-color);
diff --git a/schema/mc_schema.json b/schema/mc_schema.json
index cd32da6..8127731 100644
--- a/schema/mc_schema.json
+++ b/schema/mc_schema.json
@@ -23,6 +23,9 @@
"correct": {
"type": "boolean"
},
+ "hide": {
+ "type": "boolean"
+ },
"feedback": {
"type": "string"
},
diff --git a/test.ipynb b/test.ipynb
index 0753d7a..a2f6636 100644
--- a/test.ipynb
+++ b/test.ipynb
@@ -20,7 +20,7 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": 1,
"metadata": {
"tags": [
"remove-input"
@@ -44,14 +44,14 @@
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
- "
"
+ ],
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": [
+ "var questionsRpLAsUlXFHVZ=[{\"question\": \"What is c?\", \"type\": \"many_choice\", \"answers\": [{\"answer\": \"A letter\", \"correct\": true, \"hide\": true}, {\"answer\": \"A speed\", \"correct\": false, \"hide\": true}, {\"answer\": \"A programming language\", \"correct\": false, \"hide\": true}]}];\n",
+ "\n",
+ "if (typeof Question === 'undefined') {\n",
+ "// Make a random ID\n",
+ "function makeid(length) {\n",
+ " var result = [];\n",
+ " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n",
+ " var charactersLength = characters.length;\n",
+ " for (var i = 0; i < length; i++) {\n",
+ " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n",
+ " }\n",
+ " return result.join('');\n",
+ "}\n",
+ "// Convert LaTeX delimiters and markdown links to HTML\n",
+ "function jaxify(string) {\n",
+ " let mystring = string;\n",
+ " let count = 0, count2 = 0;\n",
+ " let loc = mystring.search(/([^\\\\]|^)(\\$)/);\n",
+ " let loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n",
+ " while (loc >= 0 || loc2 >= 0) {\n",
+ " if (loc2 >= 0) {\n",
+ " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, count2 % 2 ? '$1\\\\]' : '$1\\\\[');\n",
+ " count2++;\n",
+ " } else {\n",
+ " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, count % 2 ? '$1\\\\)' : '$1\\\\(');\n",
+ " count++;\n",
+ " }\n",
+ " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n",
+ " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n",
+ " }\n",
+ " // Replace markdown links\n",
+ " mystring = mystring.replace(//g, 'http$1');\n",
+ " mystring = mystring.replace(/\\[(.*?)\\]\\((.*?)\\)/g, '$1');\n",
+ " return mystring;\n",
+ "}\n",
+ "\n",
+ "// Base class for question types\n",
+ "class Question {\n",
+ " static registry = {};\n",
+ " static register(type, cls) {\n",
+ " Question.registry[type] = cls;\n",
+ " }\n",
+ " static create(qa, id, index, options, rootDiv) {\n",
+ " const Cls = Question.registry[qa.type];\n",
+ " if (!Cls) {\n",
+ " console.error(`No question class registered for type \"${qa.type}\"`);\n",
+ " return;\n",
+ " }\n",
+ " const q = new Cls(qa, id, index, options, rootDiv);\n",
+ " q.render();\n",
+ " }\n",
+ "\n",
+ " constructor(qa, id, index, options, rootDiv) {\n",
+ " this.qa = qa;\n",
+ " this.id = id;\n",
+ " this.index = index;\n",
+ " this.options = options;\n",
+ " this.rootDiv = rootDiv;\n",
+ " // wrapper\n",
+ " this.wrapper = document.createElement('div');\n",
+ " this.wrapper.id = `quizWrap${id}`;\n",
+ " this.wrapper.className = 'Quiz';\n",
+ " this.wrapper.dataset.qnum = index;\n",
+ " this.wrapper.style.maxWidth = `${options.maxWidth}px`;\n",
+ " rootDiv.appendChild(this.wrapper);\n",
+ " // question container\n",
+ " this.outerqDiv = document.createElement('div');\n",
+ " this.outerqDiv.id = `OuterquizQn${id}${index}`;\n",
+ " this.wrapper.appendChild(this.outerqDiv);\n",
+ " // question text\n",
+ " this.qDiv = document.createElement('div');\n",
+ " this.qDiv.id = `quizQn${id}${index}`;\n",
+ " if (qa.question) {\n",
+ " this.qDiv.innerHTML = jaxify(qa.question);\n",
+ " this.outerqDiv.appendChild(this.qDiv);\n",
+ " }\n",
+ " // code block\n",
+ " if (qa.code) {\n",
+ " const codeDiv = document.createElement('div');\n",
+ " codeDiv.id = `code${id}${index}`;\n",
+ " codeDiv.className = 'QuizCode';\n",
+ " const pre = document.createElement('pre');\n",
+ " const codeEl = document.createElement('code');\n",
+ " codeEl.innerHTML = qa.code;\n",
+ " pre.appendChild(codeEl);\n",
+ " codeDiv.appendChild(pre);\n",
+ " this.outerqDiv.appendChild(codeDiv);\n",
+ " }\n",
+ " // answer container\n",
+ " this.aDiv = document.createElement('div');\n",
+ " this.aDiv.id = `quizAns${id}${index}`;\n",
+ " this.aDiv.className = 'Answer';\n",
+ " this.wrapper.appendChild(this.aDiv);\n",
+ " // feedback container (append after answers)\n",
+ " this.fbDiv = document.createElement('div');\n",
+ " this.fbDiv.id = `fb${id}`;\n",
+ " this.fbDiv.className = 'Feedback';\n",
+ " this.fbDiv.dataset.answeredcorrect = 0;\n",
+ " }\n",
+ "\n",
+ " render() {\n",
+ " throw new Error('render() not implemented');\n",
+ " }\n",
+ "\n",
+ " preserveResponse(val) {\n",
+ " if (!this.options.preserveResponses) return;\n",
+ " const resp = document.getElementById(`responses${this.rootDiv.id}`);\n",
+ " if (!resp) return;\n",
+ " const arr = JSON.parse(resp.dataset.responses);\n",
+ " arr[this.index] = val;\n",
+ " resp.dataset.responses = JSON.stringify(arr);\n",
+ " printResponses(resp);\n",
+ " }\n",
+ "\n",
+ " typeset(container) {\n",
+ " if (typeof MathJax !== 'undefined') {\n",
+ " const v = MathJax.version;\n",
+ " if (v[0] === '2') {\n",
+ " MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n",
+ " } else {\n",
+ " MathJax.typeset([container]);\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// Choose a random subset of an array. Can also be used to shuffle the array\n",
+ "function getRandomSubarray(arr, size) {\n",
+ " var shuffled = arr.slice(0), i = arr.length, temp, index;\n",
+ " while (i--) {\n",
+ " index = Math.floor((i + 1) * Math.random());\n",
+ " temp = shuffled[index];\n",
+ " shuffled[index] = shuffled[i];\n",
+ " shuffled[i] = temp;\n",
+ " }\n",
+ " return shuffled.slice(0, size);\n",
+ "}\n",
+ "\n",
+ "function printResponses(responsesContainer) {\n",
+ " var responses=JSON.parse(responsesContainer.dataset.responses);\n",
+ " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers: - Copy the text in this cell below \"Answer String\"
- Double click on the cell directly below the Answer String, labeled \"Replace Me\"
- Select the whole \"Replace Me\" text
- Paste in your answer string and press shift-Enter.
- Save the notebook using the save icon or File->Save Notebook menu item
Answer String:
';\n",
+ " console.log(responses);\n",
+ " responses.forEach((response, index) => {\n",
+ " if (response) {\n",
+ " console.log(index + ': ' + response);\n",
+ " stringResponses+= index + ': ' + response +\"
\";\n",
+ " }\n",
+ " });\n",
+ " responsesContainer.innerHTML=stringResponses;\n",
+ "}\n",
+ "/* Callback function to determine whether a selected multiple-choice\n",
+ " button corresponded to a correct answer and to provide feedback\n",
+ " based on the answer */\n",
+ "function check_mc() {\n",
+ " var id = this.id.split('-')[0];\n",
+ " //var response = this.id.split('-')[1];\n",
+ " //console.log(response);\n",
+ " //console.log(\"In check_mc(), id=\"+id);\n",
+ " //console.log(event.srcElement.id) \n",
+ " //console.log(event.srcElement.dataset.correct) \n",
+ " //console.log(event.srcElement.dataset.hide)\n",
+ " //console.log(event.srcElement.dataset.feedback)\n",
+ "\n",
+ " var label = event.srcElement;\n",
+ " //console.log(label, label.nodeName);\n",
+ " var depth = 0;\n",
+ " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n",
+ " label = label.parentElement;\n",
+ " console.log(depth, label);\n",
+ " depth++;\n",
+ " }\n",
+ "\n",
+ "\n",
+ "\n",
+ " var answers = label.parentElement.children;\n",
+ " //console.log(answers);\n",
+ "\n",
+ " // Split behavior based on multiple choice vs many choice:\n",
+ " var fb = document.getElementById(\"fb\" + id);\n",
+ "\n",
+ "\n",
+ "\n",
+ " /* Multiple choice (1 answer). Allow for 0 correct\n",
+ " answers as an edge case */\n",
+ " if (fb.dataset.numcorrect <= 1) {\n",
+ " // What follows is for the saved responses stuff\n",
+ " var outerContainer = fb.parentElement.parentElement;\n",
+ " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n",
+ " if (responsesContainer) {\n",
+ " //console.log(responsesContainer);\n",
+ " var response = label.firstChild.innerText;\n",
+ " if (label.querySelector(\".QuizCode\")){\n",
+ " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n",
+ " }\n",
+ " console.log(response);\n",
+ " //console.log(document.getElementById(\"quizWrap\"+id));\n",
+ " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n",
+ " console.log(\"Question \" + qnum);\n",
+ " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n",
+ " var responses=JSON.parse(responsesContainer.dataset.responses);\n",
+ " console.log(responses);\n",
+ " if (label.dataset.selected == \"true\") {\n",
+ " // If it's already selected, it will be deselected now\n",
+ " responses[qnum] = null;\n",
+ " } else {\n",
+ " responses[qnum]= response;\n",
+ " }\n",
+ " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n",
+ " printResponses(responsesContainer);\n",
+ " }\n",
+ " // End code to preserve responses\n",
+ "\n",
+ " for (var i = 0; i < answers.length; i++) {\n",
+ " // Deselect all other buttons\n",
+ " var child = answers[i];\n",
+ " //console.log(child);\n",
+ " if (child.id != label.id) {\n",
+ " if (child.classList.contains(\"selectedButton\")) {\n",
+ " child.setAttribute('data-selected', \"false\")\n",
+ " child.classList.remove(\"selectedButton\");\n",
+ " void child.offsetWidth;\n",
+ " child.classList.add(\"deselectedButton\");\n",
+ " }\n",
+ " if (child.classList.contains(\"incorrectButton\"))\n",
+ " child.classList.remove(\"incorrectButton\");\n",
+ " if (child.classList.contains(\"correctButton\"))\n",
+ " child.classList.remove(\"correctButton\");\n",
+ " }\n",
+ " }\n",
+ "\n",
+ "\n",
+ " if (label.dataset.hide == \"true\") {\n",
+ " if (label.dataset.selected == \"true\") {\n",
+ " label.setAttribute('data-selected', \"false\");\n",
+ " label.classList.remove(\"selectedButton\");\n",
+ " void label.offsetWidth;\n",
+ " label.classList.add(\"deselectedButton\");\n",
+ " fb.innerHTML = \"Deselected.\";\n",
+ " fb.classList.remove(\"selected\");\n",
+ " fb.classList.add(\"deselected\");\n",
+ " } else {\n",
+ " label.setAttribute('data-selected', \"true\");\n",
+ " if (\"feedback\" in label.dataset) {\n",
+ " fb.innerHTML = jaxify(label.dataset.feedback);\n",
+ " } else {\n",
+ " fb.innerHTML = \"Selected.\";\n",
+ " }\n",
+ " if (label.classList.contains(\"deselectedButton\")) {\n",
+ " label.classList.remove(\"deselectedButton\");\n",
+ " };\n",
+ " void label.offsetWidth;\n",
+ " label.classList.add(\"selectedButton\");\n",
+ " fb.className = \"Feedback\";\n",
+ " if (fb.classList.contains(\"deselected\")) {\n",
+ " fb.classList.remove(\"deselected\");\n",
+ " }\n",
+ " fb.classList.add(\"selected\");\n",
+ " }\n",
+ " } else {\n",
+ " if (label.dataset.correct == \"true\") {\n",
+ " // console.log(\"Correct action\");\n",
+ " if (\"feedback\" in label.dataset) {\n",
+ " fb.innerHTML = jaxify(label.dataset.feedback);\n",
+ " } else {\n",
+ " fb.innerHTML = \"Correct!\";\n",
+ " }\n",
+ " label.classList.add(\"correctButton\");\n",
+ "\n",
+ " fb.className = \"Feedback\";\n",
+ " fb.classList.add(\"correct\");\n",
+ "\n",
+ " } else {\n",
+ " if (\"feedback\" in label.dataset) {\n",
+ " fb.innerHTML = jaxify(label.dataset.feedback);\n",
+ " } else {\n",
+ " fb.innerHTML = \"Incorrect -- try again.\";\n",
+ " }\n",
+ " //console.log(\"Error action\");\n",
+ " label.classList.add(\"incorrectButton\");\n",
+ " fb.className = \"Feedback\";\n",
+ " fb.classList.add(\"incorrect\");\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " else { /* Many choice (more than 1 correct answer) */\n",
+ " var reset = false;\n",
+ " var feedback;\n",
+ "\n",
+ " if (label.dataset.hide == \"true\") {\n",
+ " if (label.dataset.selected == \"true\") {\n",
+ " label.setAttribute('data-selected', \"false\");\n",
+ " label.classList.remove(\"selectedButton\");\n",
+ " void label.offsetWidth;\n",
+ " label.classList.add(\"deselectedButton\");\n",
+ " feedback = \"Deselected.\";\n",
+ " fb.classList.remove(\"selected\");\n",
+ " fb.classList.add(\"deselected\");\n",
+ " } else {\n",
+ " label.setAttribute('data-selected', \"true\");\n",
+ " if (\"feedback\" in label.dataset) {\n",
+ " feedback = jaxify(label.dataset.feedback);\n",
+ " } else {\n",
+ " feedback = \"Selected.\";\n",
+ " }\n",
+ " if (label.classList.contains(\"deselectedButton\")) {\n",
+ " label.classList.remove(\"deselectedButton\");\n",
+ " };\n",
+ " void label.offsetWidth;\n",
+ " label.classList.add(\"selectedButton\");\n",
+ " fb.className = \"Feedback\";\n",
+ " if (fb.classList.contains(\"deselected\")) {\n",
+ " fb.classList.remove(\"deselected\");\n",
+ " }\n",
+ " fb.classList.add(\"selected\");\n",
+ " }\n",
+ " } else {\n",
+ " if (label.dataset.correct == \"true\") {\n",
+ " if (\"feedback\" in label.dataset) {\n",
+ " feedback = jaxify(label.dataset.feedback);\n",
+ " } else {\n",
+ " feedback = \"Correct!\";\n",
+ " }\n",
+ " if (label.dataset.answered <= 0) {\n",
+ " if (fb.dataset.answeredcorrect < 0) {\n",
+ " fb.dataset.answeredcorrect = 1;\n",
+ " reset = true;\n",
+ " } else {\n",
+ " fb.dataset.answeredcorrect++;\n",
+ " }\n",
+ " if (reset) {\n",
+ " for (var i = 0; i < answers.length; i++) {\n",
+ " var child = answers[i];\n",
+ " if (child.id != label.id) {\n",
+ " if (child.dataset.selected == \"true\") {\n",
+ " child.setAttribute('data-selected', \"false\");\n",
+ " child.classList.remove(\"selectedButton\");\n",
+ " void child.offsetWidth;\n",
+ " child.classList.add(\"deselectedButton\");\n",
+ " }\n",
+ " if (child.classList.contains(\"correctButton\"))\n",
+ " child.classList.remove(\"correctButton\");\n",
+ " if (child.classList.contains(\"incorrectButton\"))\n",
+ " child.classList.remove(\"incorrectButton\");\n",
+ " child.dataset.answered = 0;\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " label.classList.add(\"correctButton\");\n",
+ " label.dataset.answered = 1;\n",
+ " fb.className = \"Feedback\";\n",
+ " fb.classList.add(\"correct\");\n",
+ "\n",
+ " }\n",
+ " } else {\n",
+ " if (\"feedback\" in label.dataset) {\n",
+ " feedback = jaxify(label.dataset.feedback);\n",
+ " } else {\n",
+ " feedback = \"Incorrect -- try again.\";\n",
+ " }\n",
+ " if (fb.dataset.answeredcorrect > 0) {\n",
+ " fb.dataset.answeredcorrect = -1;\n",
+ " reset = true;\n",
+ " } else {\n",
+ " fb.dataset.answeredcorrect--;\n",
+ " }\n",
+ "\n",
+ " if (reset) {\n",
+ " for (var i = 0; i < answers.length; i++) {\n",
+ " var child = answers[i];\n",
+ " if (child.id != label.id) {\n",
+ " if (child.dataset.selected == \"true\") {\n",
+ " child.setAttribute('data-selected', \"false\");\n",
+ " child.classList.remove(\"selectedButton\");\n",
+ " void child.offsetWidth;\n",
+ " child.classList.add(\"deselectedButton\");\n",
+ " }\n",
+ " if (child.classList.contains(\"correctButton\"))\n",
+ " child.classList.remove(\"correctButton\");\n",
+ " if (child.classList.contains(\"incorrectButton\"))\n",
+ " child.classList.remove(\"incorrectButton\");\n",
+ " child.dataset.answered = 0;\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " label.classList.add(\"incorrectButton\");\n",
+ " fb.className = \"Feedback\";\n",
+ " fb.classList.add(\"incorrect\");\n",
+ " }\n",
+ " }\n",
+ " // What follows is for the saved responses stuff\n",
+ " var outerContainer = fb.parentElement.parentElement;\n",
+ " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n",
+ " if (responsesContainer) {\n",
+ " //console.log(responsesContainer);\n",
+ " var response = label.firstChild.innerText;\n",
+ " if (label.querySelector(\".QuizCode\")){\n",
+ " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n",
+ " }\n",
+ " console.log(response);\n",
+ " //console.log(document.getElementById(\"quizWrap\"+id));\n",
+ " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n",
+ " console.log(\"Question \" + qnum);\n",
+ " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n",
+ " var responses = JSON.parse(responsesContainer.dataset.responses);\n",
+ " if (typeof(responses[qnum]) == \"object\") {\n",
+ " var these_responses = new Set(responses[qnum]);\n",
+ " } else {\n",
+ " var these_responses = new Set();\n",
+ " }\n",
+ "\n",
+ " if (label.dataset.selected == \"true\") {\n",
+ " these_responses.add(response);\n",
+ " } else {\n",
+ " these_responses.delete(response);\n",
+ " }\n",
+ " responses[qnum] = Array.from(these_responses);\n",
+ " console.log(responses);\n",
+ " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n",
+ " printResponses(responsesContainer);\n",
+ " }\n",
+ " // End save responses stuff\n",
+ "\n",
+ "\n",
+ "\n",
+ " var numcorrect = fb.dataset.numcorrect;\n",
+ " var answeredcorrect = fb.dataset.answeredcorrect;\n",
+ " if (answeredcorrect >= 0) {\n",
+ " fb.innerHTML = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n",
+ " } else {\n",
+ " fb.innerHTML = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n",
+ " }\n",
+ "\n",
+ "\n",
+ " }\n",
+ "\n",
+ " if (typeof MathJax != 'undefined') {\n",
+ " var version = MathJax.version;\n",
+ " console.log('MathJax version', version);\n",
+ " if (version[0] == \"2\") {\n",
+ " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n",
+ " } else if (version[0] == \"3\") {\n",
+ " MathJax.typeset([fb]);\n",
+ " }\n",
+ " } else {\n",
+ " console.log('MathJax not detected');\n",
+ " }\n",
+ "\n",
+ "}\n",
+ "\n",
+ "\n",
+ "/* Function to produce the HTML buttons for a multiple choice/\n",
+ " many choice question and to update the CSS tags based on\n",
+ " the question type */\n",
+ "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n",
+ "\n",
+ " var shuffled;\n",
+ " if (shuffle_answers == true) {\n",
+ " //console.log(shuffle_answers+\" read as true\");\n",
+ " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n",
+ " } else {\n",
+ " //console.log(shuffle_answers+\" read as false\");\n",
+ " shuffled = qa.answers;\n",
+ " }\n",
+ "\n",
+ "\n",
+ " var num_correct = 0;\n",
+ "\n",
+ " shuffled.forEach((item, index, ans_array) => {\n",
+ " //console.log(answer);\n",
+ "\n",
+ " // Make input element\n",
+ " var inp = document.createElement(\"input\");\n",
+ " inp.type = \"radio\";\n",
+ " inp.id = \"quizo\" + id + index;\n",
+ " inp.style = \"display:none;\";\n",
+ " aDiv.append(inp);\n",
+ "\n",
+ " //Make label for input element\n",
+ " var lab = document.createElement(\"label\");\n",
+ " lab.className = \"MCButton\";\n",
+ " lab.id = id + '-' + index;\n",
+ " lab.onclick = check_mc;\n",
+ " var aSpan = document.createElement('span');\n",
+ " aSpan.classsName = \"\";\n",
+ " //qDiv.id=\"quizQn\"+id+index;\n",
+ " if (\"answer\" in item) {\n",
+ " aSpan.innerHTML = jaxify(item.answer);\n",
+ " //aSpan.innerHTML=item.answer;\n",
+ " }\n",
+ " lab.append(aSpan);\n",
+ "\n",
+ " // Create div for code inside question\n",
+ " var codeSpan;\n",
+ " if (\"code\" in item) {\n",
+ " codeSpan = document.createElement('span');\n",
+ " codeSpan.id = \"code\" + id + index;\n",
+ " codeSpan.className = \"QuizCode\";\n",
+ " var codePre = document.createElement('pre');\n",
+ " codeSpan.append(codePre);\n",
+ " var codeCode = document.createElement('code');\n",
+ " codePre.append(codeCode);\n",
+ " codeCode.innerHTML = item.code;\n",
+ " lab.append(codeSpan);\n",
+ " //console.log(codeSpan);\n",
+ " }\n",
+ "\n",
+ " //lab.textContent=item.answer;\n",
+ "\n",
+ " // Set the data attributes for the answer\n",
+ " lab.setAttribute('data-selected', \"false\")\n",
+ " if (\"hide\" in item) {\n",
+ " lab.setAttribute('data-hide', item.hide);\n",
+ " } else {\n",
+ " lab.setAttribute('data-hide', \"false\");\n",
+ " }\n",
+ " lab.setAttribute('data-correct', item.correct);\n",
+ " if (item.correct) {\n",
+ " num_correct++;\n",
+ " }\n",
+ " if (\"feedback\" in item) {\n",
+ " lab.setAttribute('data-feedback', item.feedback);\n",
+ " }\n",
+ " lab.setAttribute('data-answered', 0);\n",
+ "\n",
+ " aDiv.append(lab);\n",
+ "\n",
+ " });\n",
+ "\n",
+ " if (num_correct > 1) {\n",
+ " outerqDiv.className = \"ManyChoiceQn\";\n",
+ " } else {\n",
+ " outerqDiv.className = \"MultipleChoiceQn\";\n",
+ " }\n",
+ "\n",
+ " return num_correct;\n",
+ "\n",
+ "}\n",
+ "// Object-oriented wrapper for MC/MANY choice\n",
+ "class MCQuestion extends Question {\n",
+ " constructor(qa, id, idx, opts, rootDiv) { super(qa, id, idx, opts, rootDiv); }\n",
+ " render() {\n",
+ " //console.log(\"options.shuffleAnswers \" + this.options.shuffleAnswers);\n",
+ " const numCorrect = make_mc(\n",
+ " this.qa,\n",
+ " this.options.shuffleAnswers,\n",
+ " this.outerqDiv,\n",
+ " this.qDiv,\n",
+ " this.aDiv,\n",
+ " this.id\n",
+ " );\n",
+ " if ('answer_cols' in this.qa) {\n",
+ " this.aDiv.style.gridTemplateColumns =\n",
+ " 'repeat(' + this.qa.answer_cols + ', 1fr)';\n",
+ " }\n",
+ " this.fbDiv.dataset.numcorrect = numCorrect;\n",
+ " this.wrapper.appendChild(this.fbDiv);\n",
+ " }\n",
+ "}\n",
+ "Question.register('multiple_choice', MCQuestion);\n",
+ "Question.register('many_choice', MCQuestion);\n",
+ "function check_numeric(ths, event) {\n",
+ "\n",
+ " if (event.keyCode === 13) {\n",
+ " ths.blur();\n",
+ "\n",
+ " var id = ths.id.split('-')[0];\n",
+ "\n",
+ " var submission = ths.value;\n",
+ " if (submission.indexOf('/') != -1) {\n",
+ " var sub_parts = submission.split('/');\n",
+ " //console.log(sub_parts);\n",
+ " submission = sub_parts[0] / sub_parts[1];\n",
+ " }\n",
+ " //console.log(\"Reader entered\", submission);\n",
+ "\n",
+ " if (\"precision\" in ths.dataset) {\n",
+ " var precision = ths.dataset.precision;\n",
+ " submission = Number(Number(submission).toPrecision(precision));\n",
+ " }\n",
+ "\n",
+ "\n",
+ " //console.log(\"In check_numeric(), id=\"+id);\n",
+ " //console.log(event.srcElement.id) \n",
+ " //console.log(event.srcElement.dataset.feedback)\n",
+ "\n",
+ " var fb = document.getElementById(\"fb\" + id);\n",
+ " fb.style.display = \"none\";\n",
+ " fb.innerHTML = \"Incorrect -- try again.\";\n",
+ "\n",
+ " var answers = JSON.parse(ths.dataset.answers);\n",
+ " //console.log(answers);\n",
+ "\n",
+ " var defaultFB = \"Incorrect. Try again.\";\n",
+ " var correct;\n",
+ " var done = false;\n",
+ " answers.every(answer => {\n",
+ " //console.log(answer.type);\n",
+ "\n",
+ " correct = false;\n",
+ " // if (answer.type==\"value\"){\n",
+ " if ('value' in answer) {\n",
+ " var value;\n",
+ " if (\"precision\" in ths.dataset) {\n",
+ " value = answer.value.toPrecision(ths.dataset.precision);\n",
+ " } else {\n",
+ " value = answer.value;\n",
+ " }\n",
+ " if (submission == value) {\n",
+ " if (\"feedback\" in answer) {\n",
+ " fb.innerHTML = jaxify(answer.feedback);\n",
+ " } else {\n",
+ " fb.innerHTML = jaxify(\"Correct\");\n",
+ " }\n",
+ " correct = answer.correct;\n",
+ " //console.log(answer.correct);\n",
+ " done = true;\n",
+ " }\n",
+ "\n",
+ " // } else if (answer.type==\"range\") {\n",
+ " } else if ('range' in answer) {\n",
+ " console.log(answer.range);\n",
+ " console.log(submission, submission >=answer.range[0], submission < answer.range[1])\n",
+ " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n",
+ " fb.innerHTML = jaxify(answer.feedback);\n",
+ " correct = answer.correct;\n",
+ " console.log(answer.correct);\n",
+ " done = true;\n",
+ " }\n",
+ " } else if (answer.type == \"default\") {\n",
+ " if (\"feedback\" in answer) {\n",
+ " defaultFB = answer.feedback;\n",
+ " } \n",
+ " }\n",
+ " if (done) {\n",
+ " return false; // Break out of loop if this has been marked correct\n",
+ " } else {\n",
+ " return true; // Keep looking for case that includes this as a correct answer\n",
+ " }\n",
+ " });\n",
+ " console.log(\"done:\", done);\n",
+ "\n",
+ " if ((!done) && (defaultFB != \"\")) {\n",
+ " fb.innerHTML = jaxify(defaultFB);\n",
+ " //console.log(\"Default feedback\", defaultFB);\n",
+ " }\n",
+ "\n",
+ " fb.style.display = \"block\";\n",
+ " if (correct) {\n",
+ " ths.className = \"Input-text\";\n",
+ " ths.classList.add(\"correctButton\");\n",
+ " fb.className = \"Feedback\";\n",
+ " fb.classList.add(\"correct\");\n",
+ " } else {\n",
+ " ths.className = \"Input-text\";\n",
+ " ths.classList.add(\"incorrectButton\");\n",
+ " fb.className = \"Feedback\";\n",
+ " fb.classList.add(\"incorrect\");\n",
+ " }\n",
+ "\n",
+ " // What follows is for the saved responses stuff\n",
+ " var outerContainer = fb.parentElement.parentElement;\n",
+ " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n",
+ " if (responsesContainer) {\n",
+ " console.log(submission);\n",
+ " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n",
+ " //console.log(\"Question \" + qnum);\n",
+ " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n",
+ " var responses=JSON.parse(responsesContainer.dataset.responses);\n",
+ " console.log(responses);\n",
+ " if (submission == ths.value){\n",
+ " responses[qnum]= submission;\n",
+ " } else {\n",
+ " responses[qnum]= ths.value + \"(\" + submission +\")\";\n",
+ " }\n",
+ " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n",
+ " printResponses(responsesContainer);\n",
+ " }\n",
+ " // End code to preserve responses\n",
+ "\n",
+ " if (typeof MathJax != 'undefined') {\n",
+ " var version = MathJax.version;\n",
+ " console.log('MathJax version', version);\n",
+ " if (version[0] == \"2\") {\n",
+ " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n",
+ " } else if (version[0] == \"3\") {\n",
+ " MathJax.typeset([fb]);\n",
+ " }\n",
+ " } else {\n",
+ " console.log('MathJax not detected');\n",
+ " }\n",
+ " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n",
+ " if (correct) {\n",
+ " // find the current question wrapper\n",
+ " var wrapper = ths.closest('.Quiz');\n",
+ " if (wrapper) {\n",
+ " var nextWrapper = wrapper.nextElementSibling;\n",
+ " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n",
+ " var nextInput = nextWrapper.querySelector('input.Input-text');\n",
+ " if (nextInput) {\n",
+ " var height = wrapper.getBoundingClientRect().height;\n",
+ " console.log(height);\n",
+ " nextInput.focus();\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " return false;\n",
+ " }\n",
+ "\n",
+ "}\n",
+ "// Object-oriented wrapper for numeric questions\n",
+ "class NumericQuestion extends Question {\n",
+ " constructor(qa, id, idx, opts, rootDiv) {\n",
+ " super(qa, id, idx, opts, rootDiv);\n",
+ " }\n",
+ " render() {\n",
+ " make_numeric(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n",
+ " this.wrapper.appendChild(this.fbDiv);\n",
+ " }\n",
+ "}\n",
+ "Question.register('numeric', NumericQuestion);\n",
+ "\n",
+ "function isValid(el, charC) {\n",
+ " //console.log(\"Input char: \", charC);\n",
+ " if (charC == 46) {\n",
+ " if (el.value.indexOf('.') === -1) {\n",
+ " return true;\n",
+ " } else if (el.value.indexOf('/') != -1) {\n",
+ " var parts = el.value.split('/');\n",
+ " if (parts[1].indexOf('.') === -1) {\n",
+ " return true;\n",
+ " }\n",
+ " }\n",
+ " else {\n",
+ " return false;\n",
+ " }\n",
+ " } else if (charC == 47) {\n",
+ " if (el.value.indexOf('/') === -1) {\n",
+ " if ((el.value != \"\") && (el.value != \".\")) {\n",
+ " return true;\n",
+ " } else {\n",
+ " return false;\n",
+ " }\n",
+ " } else {\n",
+ " return false;\n",
+ " }\n",
+ " } else if (charC == 45) {\n",
+ " var edex = el.value.indexOf('e');\n",
+ " if (edex == -1) {\n",
+ " edex = el.value.indexOf('E');\n",
+ " }\n",
+ "\n",
+ " if (el.value == \"\") {\n",
+ " return true;\n",
+ " } else if (edex == (el.value.length - 1)) { // If just after e or E\n",
+ " return true;\n",
+ " } else {\n",
+ " return false;\n",
+ " }\n",
+ " } else if (charC == 101) { // \"e\"\n",
+ " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n",
+ " // Prev symbol must be digit or decimal point:\n",
+ " if (el.value.slice(-1).search(/\\d/) >= 0) {\n",
+ " return true;\n",
+ " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n",
+ " return true;\n",
+ " } else {\n",
+ " return false;\n",
+ " }\n",
+ " } else {\n",
+ " return false;\n",
+ " }\n",
+ " } else {\n",
+ " if (charC > 31 && (charC < 48 || charC > 57))\n",
+ " return false;\n",
+ " }\n",
+ " return true;\n",
+ "}\n",
+ "\n",
+ "function numeric_keypress(evnt) {\n",
+ " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n",
+ "\n",
+ " if (charC == 13) {\n",
+ " check_numeric(this, evnt);\n",
+ " } else {\n",
+ " return isValid(this, charC);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n",
+ "\n",
+ "\n",
+ "\n",
+ " //console.log(answer);\n",
+ "\n",
+ "\n",
+ " outerqDiv.className = \"NumericQn\";\n",
+ " aDiv.style.display = 'block';\n",
+ "\n",
+ " var lab = document.createElement(\"label\");\n",
+ " lab.className = \"InpLabel\";\n",
+ " lab.innerHTML = \"Type numeric answer here:\";\n",
+ " aDiv.append(lab);\n",
+ "\n",
+ " var inp = document.createElement(\"input\");\n",
+ " inp.type = \"text\";\n",
+ " //inp.id=\"input-\"+id;\n",
+ " inp.id = id + \"-0\";\n",
+ " inp.className = \"Input-text\";\n",
+ " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n",
+ " if (\"precision\" in qa) {\n",
+ " inp.setAttribute('data-precision', qa.precision);\n",
+ " }\n",
+ " aDiv.append(inp);\n",
+ " //console.log(inp);\n",
+ "\n",
+ " //inp.addEventListener(\"keypress\", check_numeric);\n",
+ " //inp.addEventListener(\"keypress\", numeric_keypress);\n",
+ " /*\n",
+ " inp.addEventListener(\"keypress\", function(event) {\n",
+ " return numeric_keypress(this, event);\n",
+ " }\n",
+ " );\n",
+ " */\n",
+ " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n",
+ " inp.onkeypress = numeric_keypress;\n",
+ " inp.onpaste = event => false;\n",
+ "\n",
+ " inp.addEventListener(\"focus\", function (event) {\n",
+ " this.value = \"\";\n",
+ " return false;\n",
+ " }\n",
+ " );\n",
+ "\n",
+ "\n",
+ "}\n",
+ "// Override show_questions to use object-oriented Question API\n",
+ "function show_questions(json, container) {\n",
+ " // Accept container element or element ID\n",
+ " if (typeof container === 'string') {\n",
+ " container = document.getElementById(container);\n",
+ " }\n",
+ " if (!container) {\n",
+ " console.error('show_questions: invalid container', container);\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " const shuffleQuestions = container.dataset.shufflequestions === 'True';\n",
+ " const shuffleAnswers = container.dataset.shuffleanswers === 'True';\n",
+ " const preserveResponses = container.dataset.preserveresponses === 'true';\n",
+ " const maxWidth = parseInt(container.dataset.maxwidth, 10) || 0;\n",
+ " let numQuestions = parseInt(container.dataset.numquestions, 10) || json.length;\n",
+ " if (numQuestions > json.length) numQuestions = json.length;\n",
+ "\n",
+ " let questions = json;\n",
+ " if (shuffleQuestions || numQuestions < json.length) {\n",
+ " questions = getRandomSubarray(json, numQuestions);\n",
+ " }\n",
+ "\n",
+ " questions.forEach((qa, index) => {\n",
+ " const id = makeid(8);\n",
+ " const options = {\n",
+ " shuffleAnswers: shuffleAnswers,\n",
+ " preserveResponses: preserveResponses,\n",
+ " maxWidth: maxWidth\n",
+ " };\n",
+ " Question.create(qa, id, index, options, container);\n",
+ " });\n",
+ "\n",
+ " if (preserveResponses) {\n",
+ " const respDiv = document.createElement('div');\n",
+ " respDiv.id = 'responses' + container.id;\n",
+ " respDiv.className = 'JCResponses';\n",
+ " respDiv.dataset.responses = JSON.stringify([]);\n",
+ " respDiv.innerHTML = 'Select your answers and then follow the directions that will appear here.';\n",
+ " container.appendChild(respDiv);\n",
+ " }\n",
+ "\n",
+ " // Trigger MathJax typesetting if available\n",
+ " if (typeof MathJax != 'undefined') {\n",
+ " console.log(\"MathJax version\", MathJax.version);\n",
+ " var version = MathJax.version;\n",
+ " setTimeout(function(){\n",
+ " var version = MathJax.version;\n",
+ " console.log('After sleep, MathJax version', version);\n",
+ " if (version[0] == \"2\") {\n",
+ " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n",
+ " } else if (version[0] == \"3\") {\n",
+ " if (MathJax.hasOwnProperty('typeset') ) {\n",
+ " MathJax.typeset([container]);\n",
+ " } else {\n",
+ " console.log('WARNING: Trying to force load MathJax 3');\n",
+ " window.MathJax = {\n",
+ " tex: {\n",
+ " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n",
+ " },\n",
+ " svg: {\n",
+ " fontCache: 'global'\n",
+ " }\n",
+ " };\n",
+ "\n",
+ " (function () {\n",
+ " var script = document.createElement('script');\n",
+ " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n",
+ " script.async = true;\n",
+ " document.head.appendChild(script);\n",
+ " })();\n",
+ " }\n",
+ " }\n",
+ " }, 500);\n",
+ "if (typeof version == 'undefined') {\n",
+ " } else\n",
+ " {\n",
+ " if (version[0] == \"2\") {\n",
+ " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n",
+ " } else if (version[0] == \"3\") {\n",
+ " if (MathJax.hasOwnProperty('typeset') ) {\n",
+ " MathJax.typeset([container]);\n",
+ " } else {\n",
+ " console.log('WARNING: Trying to force load MathJax 3');\n",
+ " window.MathJax = {\n",
+ " tex: {\n",
+ " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n",
+ " },\n",
+ " svg: {\n",
+ " fontCache: 'global'\n",
+ " }\n",
+ " };\n",
+ "\n",
+ " (function () {\n",
+ " var script = document.createElement('script');\n",
+ " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n",
+ " script.async = true;\n",
+ " document.head.appendChild(script);\n",
+ " })();\n",
+ " }\n",
+ " } else {\n",
+ " console.log(\"MathJax not found\");\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " // if (typeof MathJax !== 'undefined') {\n",
+ " // const v = MathJax.version;\n",
+ " // if (v[0] === '2') {\n",
+ " // MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n",
+ " // } else if (v[0] === '3') {\n",
+ " // MathJax.typeset([container]);\n",
+ " // }\n",
+ " // }\n",
+ "\n",
+ " // Prevent link clicks from bubbling up\n",
+ " Array.from(container.getElementsByClassName('Link')).forEach(link => {\n",
+ " link.addEventListener('click', e => e.stopPropagation());\n",
+ " });\n",
+ "}\n",
+ "function levenshteinDistance(a, b) {\n",
+ " if (a.length === 0) return b.length;\n",
+ " if (b.length === 0) return a.length;\n",
+ "\n",
+ " const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));\n",
+ "\n",
+ " for (let i = 0; i <= a.length; i++) {\n",
+ " matrix[0][i] = i;\n",
+ " }\n",
+ "\n",
+ " for (let j = 0; j <= b.length; j++) {\n",
+ " matrix[j][0] = j;\n",
+ " }\n",
+ "\n",
+ " for (let j = 1; j <= b.length; j++) {\n",
+ " for (let i = 1; i <= a.length; i++) {\n",
+ " const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n",
+ " matrix[j][i] = Math.min(\n",
+ " matrix[j - 1][i] + 1, // Deletion\n",
+ " matrix[j][i - 1] + 1, // Insertion\n",
+ " matrix[j - 1][i - 1] + cost // Substitution\n",
+ " );\n",
+ " }\n",
+ " }\n",
+ " return matrix[b.length][a.length];\n",
+ "}\n",
+ "// Object-oriented wrapper for string input questions\n",
+ "class StringQuestion extends Question {\n",
+ " constructor(qa, id, idx, opts, rootDiv) {\n",
+ " super(qa, id, idx, opts, rootDiv);\n",
+ " }\n",
+ " render() {\n",
+ " make_string(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n",
+ " this.wrapper.appendChild(this.fbDiv);\n",
+ " }\n",
+ "}\n",
+ "Question.register('string', StringQuestion);\n",
+ "\n",
+ "function check_string(ths, event) {\n",
+ " if (event.keyCode === 13) {\n",
+ " ths.blur();\n",
+ "\n",
+ " var id = ths.id.split('-')[0];\n",
+ " var submission = ths.value.trim();\n",
+ " var fb = document.getElementById(\"fb\" + id);\n",
+ " fb.style.display = \"none\";\n",
+ " fb.innerHTML = \"Incorrect -- try again.\";\n",
+ "\n",
+ " var answers = JSON.parse(ths.dataset.answers);\n",
+ " var defaultFB = \"Incorrect. Try again.\";\n",
+ " var correct;\n",
+ " var done = false;\n",
+ "\n",
+ " // Handle default answer pattern: filter out and capture default feedback\n",
+ " var filteredAnswers = [];\n",
+ " answers.forEach(answer => {\n",
+ " if (answer.type === \"default\") {\n",
+ " defaultFB = answer.feedback;\n",
+ " } else {\n",
+ " filteredAnswers.push(answer);\n",
+ " }\n",
+ " });\n",
+ " answers = filteredAnswers;\n",
+ "\n",
+ " answers.every(answer => {\n",
+ " correct = false;\n",
+ "\n",
+ " let match = false;\n",
+ " if (answer.match_case) {\n",
+ " match = submission === answer.answer;\n",
+ " } else {\n",
+ " match = submission.toLowerCase() === answer.answer.toLowerCase();\n",
+ " }\n",
+ " console.log(submission);\n",
+ " console.log(answer.answer);\n",
+ " console.log(match);\n",
+ "\n",
+ " if (match) {\n",
+ " if (\"feedback\" in answer) {\n",
+ " fb.innerHTML = jaxify(answer.feedback);\n",
+ " } else {\n",
+ " fb.innerHTML = jaxify(\"Correct\");\n",
+ " }\n",
+ " correct = answer.correct;\n",
+ " done = true;\n",
+ " } else if (answer.fuzzy_threshold) {\n",
+ " var max_length = Math.max(submission.length, answer.answer.length);\n",
+ " var ratio;\n",
+ " if (answer.match_case) {\n",
+ " ratio = 1- (levenshteinDistance(submission, answer.answer) / max_length);\n",
+ " } else {\n",
+ " ratio = 1- (levenshteinDistance(submission.toLowerCase(),\n",
+ " answer.answer.toLowerCase()) / max_length);\n",
+ " }\n",
+ " if (ratio >= answer.fuzzy_threshold) {\n",
+ " if (\"feedback\" in answer) {\n",
+ " fb.innerHTML = jaxify(\"(Fuzzy) \" + answer.feedback);\n",
+ " } else {\n",
+ " fb.innerHTML = jaxify(\"Correct\");\n",
+ " }\n",
+ " correct = answer.correct;\n",
+ " done = true;\n",
+ " }\n",
+ "\n",
+ " }\n",
+ "\n",
+ " if (done) {\n",
+ " return false;\n",
+ " } else {\n",
+ " return true;\n",
+ " }\n",
+ " });\n",
+ "\n",
+ " if ((!done) && (defaultFB != \"\")) {\n",
+ " fb.innerHTML = jaxify(defaultFB);\n",
+ " }\n",
+ "\n",
+ " fb.style.display = \"block\";\n",
+ " if (correct) {\n",
+ " ths.className = \"Input-text\";\n",
+ " ths.classList.add(\"correctButton\");\n",
+ " fb.className = \"Feedback\";\n",
+ " fb.classList.add(\"correct\");\n",
+ " } else {\n",
+ " ths.className = \"Input-text\";\n",
+ " ths.classList.add(\"incorrectButton\");\n",
+ " fb.className = \"Feedback\";\n",
+ " fb.classList.add(\"incorrect\");\n",
+ " }\n",
+ "\n",
+ " var outerContainer = fb.parentElement.parentElement;\n",
+ " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n",
+ " if (responsesContainer) {\n",
+ " var qnum = document.getElementById(\"quizWrap\" + id).dataset.qnum;\n",
+ " var responses = JSON.parse(responsesContainer.dataset.responses);\n",
+ " responses[qnum] = submission;\n",
+ " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n",
+ " printResponses(responsesContainer);\n",
+ " }\n",
+ "\n",
+ " if (typeof MathJax != 'undefined') {\n",
+ " var version = MathJax.version;\n",
+ " if (version[0] == \"2\") {\n",
+ " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n",
+ " } else if (version[0] == \"3\") {\n",
+ " MathJax.typeset([fb]);\n",
+ " }\n",
+ " } else {\n",
+ " console.log('MathJax not detected');\n",
+ " }\n",
+ " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n",
+ " if (correct) {\n",
+ " var wrapper = ths.closest('.Quiz');\n",
+ " if (wrapper) {\n",
+ " var nextWrapper = wrapper.nextElementSibling;\n",
+ " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n",
+ " var nextInput = nextWrapper.querySelector('input.Input-text');\n",
+ " if (nextInput) {\n",
+ " var height = wrapper.getBoundingClientRect().height;\n",
+ " nextInput.focus();\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " return false;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "function string_keypress(evnt) {\n",
+ " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n",
+ "\n",
+ " if (charC == 13) {\n",
+ " check_string(this, evnt);\n",
+ " } \n",
+ "}\n",
+ "\n",
+ "\n",
+ "function make_string(qa, outerqDiv, qDiv, aDiv, id) {\n",
+ " outerqDiv.className = \"StringQn\";\n",
+ " aDiv.style.display = 'block';\n",
+ "\n",
+ " var lab = document.createElement(\"label\");\n",
+ " lab.className = \"InpLabel\";\n",
+ " lab.innerHTML = \"Type your answer here:\";\n",
+ " aDiv.append(lab);\n",
+ "\n",
+ " var inp = document.createElement(\"input\");\n",
+ " inp.type = \"text\";\n",
+ " inp.id = id + \"-0\";\n",
+ " inp.className = \"Input-text\";\n",
+ " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n",
+ " // Apply optional input width (approx. number of characters, in em units)\n",
+ " if (qa.input_width != null) {\n",
+ " inp.style['min-width'] = qa.input_width + 'em';\n",
+ " }\n",
+ " aDiv.append(inp);\n",
+ "\n",
+ " inp.onkeypress = string_keypress;\n",
+ " inp.onpaste = event => false;\n",
+ "\n",
+ " inp.addEventListener(\"focus\", function (event) {\n",
+ " this.value = \"\";\n",
+ " return false;\n",
+ " });\n",
+ "}\n",
+ "/*\n",
+ " * Handle asynchrony issues when re-running quizzes in Jupyter notebooks.\n",
+ " * Ensures show_questions is called after the container div is in the DOM.\n",
+ " */\n",
+ "function try_show() {\n",
+ " if (document.getElementById(\"RpLAsUlXFHVZ\")) {\n",
+ " show_questions(questionsRpLAsUlXFHVZ, RpLAsUlXFHVZ);\n",
+ " } else {\n",
+ " setTimeout(try_show, 200);\n",
+ " }\n",
+ "};\n",
+ "// Invoke immediately\n",
+ "{\n",
+ " try_show();\n",
+ "}\n",
+ "}\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "display_quiz(example_string, preserve_responses = True)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -14234,7 +17696,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.0"
+ "version": "3.13.2"
}
},
"nbformat": 4,