From c2df0d37ac565821b686be5e4f6ecd70f57e6e21 Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Tue, 9 Dec 2025 16:01:30 -0800 Subject: [PATCH 1/3] Shortanswers do not always use mathjax --- bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js b/bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js index 86c74955d..49c2eeb57 100644 --- a/bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js +++ b/bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js @@ -32,7 +32,6 @@ export default class ShortAnswer extends RunestoneBase { if ($(this.origElem).is("[data-mathjax]")) { this.mathjax = true; } - this.mathjax = true; if ($(this.origElem).is("[data-attachment]")) { this.attachment = true; } From de1cb59958b1821041a1aa951aa5ada51499ef60 Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Tue, 9 Dec 2025 16:03:40 -0800 Subject: [PATCH 2/3] Shortanswers - add label, aria info to mathjax rendering --- .../runestone/shortanswer/js/shortanswer.js | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js b/bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js index 49c2eeb57..5c19c9c34 100644 --- a/bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js +++ b/bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js @@ -95,12 +95,7 @@ export default class ShortAnswer extends RunestoneBase { $(this.feedbackDiv).removeClass("alert-success"); $(this.feedbackDiv).addClass("alert alert-danger"); }.bind(this); - this.fieldSet.appendChild(document.createElement("br")); - if (this.mathjax) { - this.renderedAnswer = document.createElement("div"); - $(this.renderedAnswer).addClass("latexoutput"); - this.fieldSet.appendChild(this.renderedAnswer); - } + this.buttonDiv = document.createElement("div"); this.fieldSet.appendChild(this.buttonDiv); this.submitButton = document.createElement("button"); @@ -113,6 +108,21 @@ export default class ShortAnswer extends RunestoneBase { this.renderFeedback(); }.bind(this); this.buttonDiv.appendChild(this.submitButton); + + if (this.mathjax) { + this.renderedAnswerLabel = document.createElement("label"); + this.renderedAnswerLabel.innerHTML = "Rendered Answer:"; + this.renderedAnswerLabel.id = this.divid + "_rendered_answer_label"; + this.renderedAnswerLabel.style.display = "none"; + this.fieldSet.appendChild(this.renderedAnswerLabel); + + this.renderedAnswer = document.createElement("div"); + this.renderedAnswer.classList.add("latexoutput"); + this.renderedAnswer.setAttribute('aria-labelledby', this.renderedAnswerLabel.id); + this.renderedAnswer.setAttribute('aria-live', "polite"); + this.renderedAnswer.style.display = "none"; + this.fieldSet.appendChild(this.renderedAnswer); + } this.randomSpan = document.createElement("span"); this.randomSpan.innerHTML = "Instructor's Feedback"; this.fieldSet.appendChild(this.randomSpan); @@ -166,6 +176,9 @@ export default class ShortAnswer extends RunestoneBase { value = value.replace(/\$(.*?)\$/g, "\\( $1 \\)"); value = value.replace(/\n/g, "
"); // add line breaks $(this.renderedAnswer).html(value); + + this.renderedAnswer.style.display = "block"; + this.renderedAnswerLabel.style.display = "block"; this.queueMathJax(this.renderedAnswer); } } From 3d5777791a4c2af5658b296ee5f6fd9018bdddbb Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Wed, 10 Dec 2025 11:13:08 -0800 Subject: [PATCH 3/3] Remove JQuery from shortanswer --- .../runestone/shortanswer/js/shortanswer.js | 97 ++++++++++--------- .../shortanswer/js/timed_shortanswer.js | 21 ++-- 2 files changed, 63 insertions(+), 55 deletions(-) diff --git a/bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js b/bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js index 5c19c9c34..488341997 100644 --- a/bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js +++ b/bases/rsptx/interactives/runestone/shortanswer/js/shortanswer.js @@ -26,17 +26,17 @@ export default class ShortAnswer extends RunestoneBase { this.question = this.origElem.innerHTML; this.optional = false; this.attachURL = opts.attachURL; - if ($(this.origElem).is("[data-optional]")) { + if (this.origElem.hasAttribute("data-optional")) { this.optional = true; } - if ($(this.origElem).is("[data-mathjax]")) { + if (this.origElem.hasAttribute("data-mathjax")) { this.mathjax = true; } - if ($(this.origElem).is("[data-attachment]")) { + if (this.origElem.hasAttribute("data-attachment")) { this.attachment = true; } this.placeholder = - $(this.origElem).data("placeholder") || + this.origElem.getAttribute("data-placeholder") || "Write your answer here"; this.renderHTML(); this.caption = "shortanswer"; @@ -54,7 +54,7 @@ export default class ShortAnswer extends RunestoneBase { renderHTML() { this.containerDiv = document.createElement("div"); this.containerDiv.id = this.divid; - $(this.containerDiv).addClass(this.origElem.getAttribute("class")); + this.containerDiv.classList.add(...(this.origElem.getAttribute("class") || "").split(" ").filter(Boolean)); this.newForm = document.createElement("form"); this.newForm.id = this.divid + "_journal"; this.newForm.name = this.newForm.id; @@ -64,13 +64,13 @@ export default class ShortAnswer extends RunestoneBase { this.newForm.appendChild(this.fieldSet); this.firstLegendDiv = document.createElement("div"); this.firstLegendDiv.innerHTML = this.question; - $(this.firstLegendDiv).addClass("journal-question"); + this.firstLegendDiv.classList.add("journal-question"); this.fieldSet.appendChild(this.firstLegendDiv); this.jInputDiv = document.createElement("div"); this.jInputDiv.id = this.divid + "_journal_input"; this.fieldSet.appendChild(this.jInputDiv); this.jOptionsDiv = document.createElement("div"); - $(this.jOptionsDiv).addClass("journal-options"); + this.jOptionsDiv.classList.add("journal-options"); this.jInputDiv.appendChild(this.jOptionsDiv); this.jTextArea = document.createElement("textarea"); let self = this; @@ -78,10 +78,11 @@ export default class ShortAnswer extends RunestoneBase { self.isAnswered = true; }; this.jTextArea.id = this.divid + "_solution"; - $(this.jTextArea).attr("aria-label", "textarea"); + this.jTextArea.setAttribute("aria-label", "textarea"); this.jTextArea.placeholder = this.placeholder; - $(this.jTextArea).css("display:inline, width:530px"); - $(this.jTextArea).addClass("form-control"); + this.jTextArea.style.display = "inline"; + this.jTextArea.style.width = "530px"; + this.jTextArea.classList.add("form-control"); this.jTextArea.rows = 4; this.jTextArea.cols = 50; this.jOptionsDiv.appendChild(this.jTextArea); @@ -92,14 +93,14 @@ export default class ShortAnswer extends RunestoneBase { } else { this.feedbackDiv.innerHTML = "Your answer has not been saved yet!"; } - $(this.feedbackDiv).removeClass("alert-success"); - $(this.feedbackDiv).addClass("alert alert-danger"); + this.feedbackDiv.classList.remove("alert-success"); + this.feedbackDiv.classList.add("alert", "alert-danger"); }.bind(this); this.buttonDiv = document.createElement("div"); this.fieldSet.appendChild(this.buttonDiv); this.submitButton = document.createElement("button"); - $(this.submitButton).addClass("btn btn-success"); + this.submitButton.classList.add("btn", "btn-success"); this.submitButton.type = "button"; this.submitButton.textContent = "Save"; this.submitButton.onclick = function () { @@ -127,18 +128,16 @@ export default class ShortAnswer extends RunestoneBase { this.randomSpan.innerHTML = "Instructor's Feedback"; this.fieldSet.appendChild(this.randomSpan); this.otherOptionsDiv = document.createElement("div"); - $(this.otherOptionsDiv).css("padding-left:20px"); - $(this.otherOptionsDiv).addClass("journal-options"); + this.otherOptionsDiv.style.paddingLeft = "20px"; + this.otherOptionsDiv.classList.add("journal-options"); this.fieldSet.appendChild(this.otherOptionsDiv); // add a feedback div to give user feedback this.feedbackDiv = document.createElement("div"); - //$(this.feedbackDiv).addClass("bg-info form-control"); - //$(this.feedbackDiv).css("width:530px, background-color:#eee, font-style:italic"); - $(this.feedbackDiv).css("width:530px, font-style:italic"); + this.feedbackDiv.style.width = "530px"; + this.feedbackDiv.style.fontStyle = "italic"; this.feedbackDiv.id = this.divid + "_feedback"; this.feedbackDiv.innerHTML = "You have not answered this question yet."; - $(this.feedbackDiv).addClass("alert alert-danger"); - //this.otherOptionsDiv.appendChild(this.feedbackDiv); + this.feedbackDiv.classList.add("alert", "alert-danger"); this.fieldSet.appendChild(this.feedbackDiv); if (this.attachment) { let attachDiv = document.createElement("div") @@ -159,8 +158,7 @@ export default class ShortAnswer extends RunestoneBase { } this.containerDiv.appendChild(attachDiv); } - //this.fieldSet.appendChild(document.createElement("br")); - $(this.origElem).replaceWith(this.containerDiv); + this.origElem.replaceWith(this.containerDiv); // This is a stopgap measure for when MathJax is not loaded at all. There is another // more difficult case that when MathJax is loaded asynchronously we will get here // before MathJax is loaded. In that case we will need to implement something @@ -174,8 +172,8 @@ export default class ShortAnswer extends RunestoneBase { if (this.mathjax) { value = value.replace(/\$\$(.*?)\$\$/g, "\\[ $1 \\]"); value = value.replace(/\$(.*?)\$/g, "\\( $1 \\)"); - value = value.replace(/\n/g, "
"); // add line breaks - $(this.renderedAnswer).html(value); + value = value.replace(/\n/g, "
"); + this.renderedAnswer.innerHTML = value; this.renderedAnswer.style.display = "block"; this.renderedAnswerLabel.style.display = "block"; @@ -184,7 +182,7 @@ export default class ShortAnswer extends RunestoneBase { } async checkCurrentAnswer() { - let value = $(document.getElementById(this.divid + "_solution")).val(); + let value = document.getElementById(this.divid + "_solution").value; this.renderMath(value); this.setLocalStorage({ answer: value, @@ -193,7 +191,7 @@ export default class ShortAnswer extends RunestoneBase { } async logCurrentAnswer(sid) { - let value = $(document.getElementById(this.divid + "_solution")).val(); + let value = document.getElementById(this.divid + "_solution").value; this.renderMath(value); this.setLocalStorage({ answer: value, @@ -216,8 +214,8 @@ export default class ShortAnswer extends RunestoneBase { renderFeedback() { this.feedbackDiv.innerHTML = "Your answer has been saved."; - $(this.feedbackDiv).removeClass("alert-danger"); - $(this.feedbackDiv).addClass("alert alert-success"); + this.feedbackDiv.classList.remove("alert-danger"); + this.feedbackDiv.classList.add("alert", "alert-success"); } setLocalStorage(data) { if (!this.graderactive) { @@ -245,13 +243,15 @@ export default class ShortAnswer extends RunestoneBase { localStorage.removeItem(this.localStorageKey()); return; } - let solution = $("#" + this.divid + "_solution"); - solution.text(answer); + let solution = document.getElementById(this.divid + "_solution"); + if (solution) { + solution.value = answer; + } this.renderMath(answer); this.feedbackDiv.innerHTML = "Your current saved answer is shown above."; - $(this.feedbackDiv).removeClass("alert-danger"); - $(this.feedbackDiv).addClass("alert alert-success"); + this.feedbackDiv.classList.remove("alert-danger"); + this.feedbackDiv.classList.add("alert", "alert-success"); } } } @@ -273,16 +273,17 @@ export default class ShortAnswer extends RunestoneBase { } else { tsString = ""; } - $(p).text(tsString); + p.textContent = tsString; if (data.last_answer) { this.current_answer = "ontime"; let toggle_answer_button = document.createElement("button"); toggle_answer_button.type = "button"; - $(toggle_answer_button).text("Show Late Answer"); - $(toggle_answer_button).addClass("btn btn-warning"); - $(toggle_answer_button).css("margin-left", "5px"); + toggle_answer_button.textContent = "Show Late Answer"; + toggle_answer_button.classList.add("btn", "btn-warning"); + toggle_answer_button.style.marginLeft = "5px"; - $(toggle_answer_button).click( + toggle_answer_button.addEventListener( + "click", function () { var display_timestamp, button_text; if (this.current_answer === "ontime") { @@ -301,8 +302,8 @@ export default class ShortAnswer extends RunestoneBase { this.current_answer = "ontime"; } this.renderMath(this.answer); - $(p).text(`Submitted: ${display_timestamp}`); - $(toggle_answer_button).text(button_text); + p.textContent = `Submitted: ${display_timestamp}`; + toggle_answer_button.textContent = button_text; }.bind(this) ); @@ -317,8 +318,8 @@ export default class ShortAnswer extends RunestoneBase { } this.feedbackDiv.innerHTML = feedbackStr; - $(this.feedbackDiv).removeClass("alert-danger"); - $(this.feedbackDiv).addClass("alert alert-success"); + this.feedbackDiv.classList.remove("alert-danger"); + this.feedbackDiv.classList.add("alert", "alert-success"); } disableInteraction() { @@ -415,17 +416,17 @@ export default class ShortAnswer extends RunestoneBase { == Find the custom HTML tags and == == execute our code on them == =================================*/ -$(document).on("runestone:login-complete", function () { - $("[data-component=shortanswer]").each(function () { - if ($(this).closest("[data-component=timedAssessment]").length == 0) { - // If this element exists within a timed component, don't render it here +document.addEventListener("runestone:login-complete", function () { + document.querySelectorAll("[data-component=shortanswer]").forEach(function (el) { + // If this element exists within a timed component, don't render it here + if (!el.closest("[data-component=timedAssessment]")) { try { - window.componentMap[this.id] = new ShortAnswer({ - orig: this, + window.componentMap[el.id] = new ShortAnswer({ + orig: el, useRunestoneServices: eBookConfig.useRunestoneServices, }); } catch (err) { - console.log(`Error rendering ShortAnswer Problem ${this.id} + console.log(`Error rendering ShortAnswer Problem ${el.id} Details: ${err}`); } } diff --git a/bases/rsptx/interactives/runestone/shortanswer/js/timed_shortanswer.js b/bases/rsptx/interactives/runestone/shortanswer/js/timed_shortanswer.js index f1d995746..0c5af853a 100644 --- a/bases/rsptx/interactives/runestone/shortanswer/js/timed_shortanswer.js +++ b/bases/rsptx/interactives/runestone/shortanswer/js/timed_shortanswer.js @@ -8,27 +8,34 @@ export default class TimedShortAnswer extends ShortAnswer { this.hideButtons(); } hideButtons() { - $(this.submitButton).hide(); + if (this.submitButton) { + this.submitButton.style.display = "none"; + } } renderTimedIcon(component) { // renders the clock icon on timed components. The component parameter // is the element that the icon should be appended to. var timeIconDiv = document.createElement("div"); var timeIcon = document.createElement("img"); - $(timeIcon).attr({ - src: "../_static/clock.png", - style: "width:15px;height:15px", - }); + + timeIcon.src = "../_static/clock.png"; + timeIcon.style.width = "15px"; + timeIcon.style.height = "15px"; + timeIconDiv.className = "timeTip"; timeIconDiv.title = ""; timeIconDiv.appendChild(timeIcon); - $(component).prepend(timeIconDiv); + + // If firstChild is null, insertBefore acts like appendChild + component.insertBefore(timeIconDiv, component.firstChild); } checkCorrectTimed() { return "I"; // we ignore this in the grading } hideFeedback() { - $(this.feedbackDiv).hide(); + if (this.feedbackDiv) { + this.feedbackDiv.style.display = "none"; + } } }