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:
  1. Copy the text in this cell below \"Answer String\"
  2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
  3. Select the whole \"Replace Me\" text
  4. Paste in your answer string and press shift-Enter.
  5. Save the notebook using the save icon or File->Save Notebook menu item



  6. 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,