//Critical: // TODO: Think about what should be done for an essay question. //Accessibility: //Enhancement: // TODO: Have randomize check that new seed is actually producing new HTML. // TODO: Don't offer randomize button unless we know a new version can be produced after trying a few seeds. //Styling: // TODO: Review all styling in all scenarios (staged/not, correct/partly-correct/incorrect/blank, single/multiple) async function handleWW(ww_id, action) { const ww_container = document.getElementById(ww_id); const ww_domain = ww_container.dataset.domain; const ww_processing = 'webwork2'; const ww_origin = ww_container.dataset.origin; const ww_problemSource = ww_container.dataset.problemsource; const ww_sourceFilePath = ww_container.dataset.sourcefilepath; const ww_course_id = ww_container.dataset.courseid; const ww_user_id = ww_container.dataset.userid; const ww_passwd = ww_container.dataset.coursepassword; const localize_submit = ww_container.dataset.localizeSubmit || "Submit"; const localize_check_responses = ww_container.dataset.localizeCheckResponses || "Check Responses"; const localize_reveal = ww_container.dataset.localizeReveal || "Reveal"; const localize_randomize = ww_container.dataset.localizeRandomize || "Randomize"; const localize_reset = ww_container.dataset.localizeReset || "Reset"; const runestone_logged_in = (typeof eBookConfig !== 'undefined' && eBookConfig.username !== ''); // will be null on pages generated prior to late December 2022 const activate_button = document.getElementById(ww_id + '-button') // Set the current seed if (!action) { ww_container.dataset.current_seed = ww_container.dataset.seed; if (runestone_logged_in) { ww_container.dataset.current_seed = webworkSeedHash(eBookConfig.username + ww_container.dataset.current_seed); } } else if (action == 'randomize') ww_container.dataset.current_seed = Number(ww_container.dataset.current_seed) + 100; let loader = document.createElement('div'); loader.style.position = 'absolute'; loader.style.left = 0; loader.style.top = 0; loader.style.backgroundColor = 'rgba(0.2, 0.2, 0.2, 0.4)'; loader.style.color = 'white'; loader.style.width = '100%'; loader.style.height = '100%'; loader.style.display = 'flex'; loader.style.alignItems = 'center'; loader.style.justifyContent = 'center'; loader.style.marginTop = '0'; loader.tabIndex = -1; const loaderText = document.createElement('span'); loaderText.textContent = 'Loading'; loaderText.style.fontSize = '2rem'; loader.appendChild(loaderText); ww_container.appendChild(loader); loader.focus(); if (!action) { // Determine if static version shows hints, solutions, or answers and save that information in the container dataset for later runs. ww_container.dataset.hasHint = ww_container.getElementsByClassName('hint').length > 0; ww_container.dataset.hasSolution = ww_container.getElementsByClassName('solution').length > 0; ww_container.dataset.hasAnswer = ww_container.getElementsByClassName('answer').length > 0; // Get (possibly localized) label text for hints and solutions. ww_container.dataset.hintLabelText = ww_container.dataset.hasHint == 'true' ? ww_container.querySelectorAll('.hint-knowl span.type, details.hint span.type')[0].textContent : 'Hint'; ww_container.dataset.solutionLabelText = ww_container.dataset.hasSolution == 'true' ? ww_container.querySelectorAll('.solution-knowl span.type, details.solution span.type')[0].textContent : 'Solution'; ww_container.tabIndex = -1; } let url; if (ww_processing == 'webwork2') { url = new URL(ww_domain + '/webwork2/render_rpc'); } let formData = new FormData(); let generatedPG = 'generated/webwork/pg/'; if (runestone_logged_in) { generatedPG = `/ns/books/published/${eBookConfig.basecourse}/${generatedPG}`; } if (action == 'check' || action =='reveal') { const iframe = ww_container.querySelector('.problem-iframe'); formData = new FormData(iframe.contentDocument.getElementById(ww_id + "-form")); formData.set("answersSubmitted", '1'); formData.set('WWsubmit', "1"); if (action == 'reveal' && ww_container.dataset.hasAnswer == 'true') { formData.set('WWcorrectAnsOnly', "1"); } if (ww_origin == 'generated') { const rawProblemSource = await fetch(generatedPG + ww_problemSource).then((r) => r.text()); formData.set("rawProblemSource", rawProblemSource); } else if (ww_origin == 'webwork2') formData.set("sourceFilePath", ww_sourceFilePath); } else { formData.set("problemSeed", ww_container.dataset.current_seed); if (ww_origin == 'generated') { const rawProblemSource = await fetch(generatedPG + ww_problemSource).then((r) => r.text()); formData.set("rawProblemSource", rawProblemSource); } else if (ww_origin == 'webwork2') formData.set("sourceFilePath", ww_sourceFilePath); formData.set("answersSubmitted", '0'); formData.set("displayMode", "MathJax"); formData.set("courseID", ww_course_id); formData.set("user", ww_user_id); formData.set("userID", ww_user_id); formData.set("passwd", ww_passwd); formData.set("disableCookies", '1'); formData.set("outputformat", "raw"); // note ww_container.dataset.hasSolution is a string, possibly 'false' which is true formData.set("showSolutions", ww_container.dataset.hasSolution == 'true' ? '1' : '0'); formData.set("showHints", ww_container.dataset.hasHint == 'true' ? '1' : '0'); formData.set("problemUUID",ww_id); } // If in Runestone, check if there are previous answer submissions and get them now to use in the form. let checkboxesString = ''; if (runestone_logged_in && !action) { const answersObject = (wwList[ww_id.replace(/-ww-rs$/,'')].answers ? wwList[ww_id.replace(/-ww-rs$/,'')].answers : {'answers' : [], 'mqAnswers' : []}); const previousAnswers = answersObject.answers; if (previousAnswers !== null) { formData.set('WWsubmit', 1); } for (const answer in previousAnswers) { if (previousAnswers[answer].constructor === Array) { for (const k in previousAnswers[answer]) { checkboxesString += '&'; checkboxesString += answer; checkboxesString += '='; checkboxesString += previousAnswers[answer][k]; } } else { formData.set(answer, previousAnswers[answer]); } } } // Need to get form data as a string, including possible repeated checkbox names // Do not pass post data as an object, or checkbox names will overwrite one another const formString = new URLSearchParams(formData).toString(); $.post(url, formString + checkboxesString, (data) => { // Create the form that will contain the text and input fields of the interactive problem. const form = document.createElement("form"); form.id = ww_id + "-form"; form.dataset.iframeHeight = 1; // Create a div for the problem text. const body_div = document.createElement("div"); body_div.id = ww_id + "-body"; body_div.classList.add("exercise", "exercise-like"); body_div.lang = data.lang; body_div.dir = data.dir; // Dump the problem text, answer blanks, etc. body_div.innerHTML = data.rh_result.text; // If showPartialCorrectAnswers = 0, alter the feedback buttons according to whether or not the score is 100%. if ('showPartialCorrectAnswers' in data.rh_result.flags && data.rh_result.flags.showPartialCorrectAnswers == 0) { if ('score' in data.rh_result.problem_result && data.rh_result.problem_result.score >= 1) { body_div.querySelectorAll('button.ww-feedback-btn').forEach( function(button) { button.classList.remove('btn-info'); button.classList.add('btn-success'); button.setAttribute('aria-label', 'Correct'); button.dataset.bsCustomClass = button.dataset.bsCustomClass + ' correct'; button.dataset.bsTitle = button.dataset.bsTitle.replace('Answer Preview', 'Correct'); button.firstChild.classList.add('correct') } ); } else { body_div.querySelectorAll('button.ww-feedback-btn').forEach( function(button) { button.setAttribute('aria-label', 'One or more answers incorrect'); button.dataset.bsTitle = button.dataset.bsTitle.replace('Answer Preview', 'One or more answers incorrect'); } ); } } // Replace all hn headings with h6 headings. for (const tag_name of ['h6', 'h5', 'h4', 'h3', 'h2', 'h1']) { const headings = body_div.getElementsByTagName(tag_name); for (heading of headings) { const new_heading = document.createElement("h6"); new_heading.innerHTML = heading.innerHTML; cloneAttributes(new_heading, heading); new_heading.classList.add('webwork-part'); heading.replaceWith(new_heading); } } // Hide textarea input and hide associated buttons var textareas = body_div.getElementsByTagName("textarea"); for(var i = 0, max = textareas.length; i < max; i++) { textareas[i].style.display = "none"; textareas[i].className = ''; } var textareabuttons = body_div.querySelectorAll(".latexentry-preview"); for(var i = 0, max = textareabuttons.length; i < max; i++) { textareabuttons[i].remove(); } adjustSrcHrefs(body_div, ww_domain); translateHintSol(ww_id, body_div, ww_domain, ww_container.dataset.hasHint == 'true', ww_container.dataset.hasSolution == 'true', ww_container.dataset.hintLabelText, ww_container.dataset.solutionLabelText) // insert previous answers if (runestone_logged_in) { const answersObject = (wwList[ww_id.replace(/-ww-rs$/,'')].answers ? wwList[ww_id.replace(/-ww-rs$/,'')].answers : {'answers' : [], 'mqAnswers' : []}); const mqAnswers = answersObject.mqAnswers; for (const mqAnswer in mqAnswers) { const mqInput = body_div.querySelector('input[id=' + mqAnswer + ']'); if (mqInput && mqInput.value == '') { mqInput.setAttribute('value', mqAnswers[mqAnswer]); } } const answers = answersObject.answers; for (const answer in answers) { const input = body_div.querySelector('input[id=' + answer + ']'); if (input && input.value == '') { input.setAttribute('value', answers[answer]); } } } // insert our cleaned up problem text form.appendChild(body_div); // Set up hidden input fields that the form uses const wwInputs = { problemSeed: data.inputs_ref.problemSeed, problemUUID: data.inputs_ref.problemUUID, psvn: data.inputs_ref.psvn, courseName: ww_course_id, courseID: ww_course_id, user: ww_user_id, passwd: ww_passwd, displayMode: "MathJax", session_key: data.rh_result.session_key, outputformat: "raw", language: data.formLanguage, showSummary: data.showSummary, disableCookies: '1', // note ww_container.dataset.hasSolution is a string, possibly 'false' which is true showSolutions: ww_container.dataset.hasSolution == 'true' ? '1' : '0', showHints: ww_container.dataset.hasHint == 'true' ? '1' : '0', forcePortNumber: data.forcePortNumber }; if (ww_sourceFilePath) wwInputs.sourceFilePath = ww_sourceFilePath; else if (ww_problemSource && ww_origin == 'webwork2') wwInputs.problemSource = ww_problemSource; for (const wwInputName of Object.keys(wwInputs)) { const input = document.createElement('input'); input.type = 'hidden'; input.name = wwInputName; input.value = wwInputs[wwInputName]; form.appendChild(input); } let buttonContainer = ww_container.querySelector('.problem-buttons.webwork'); // Create the submission buttons if they have not yet been created. if (!buttonContainer) { // Hide the original div that contains the Activate. ww_container.querySelector('.problem-buttons').classList.add('hidden-content', 'hidden'); // Create a new div for the webwork buttons. buttonContainer = document.createElement('div'); buttonContainer.classList.add('problem-buttons', 'webwork'); if (activate_button != null) { // Make sure the button container follows the activate button in the DOM activate_button.after(buttonContainer); } else { ww_container.prepend(buttonContainer); } // Check button const check = document.createElement("button"); check.type = "button"; check.id = ww_id + '-check'; check.style.marginRight = "0.25rem"; check.classList.add('webwork-button'); check.textContent = runestone_logged_in ? localize_submit : localize_check_responses; check.addEventListener('click', () => handleWW(ww_id, "check")); buttonContainer.appendChild(check); // Show correct answers button if original PTX has answer knowl. if (ww_container.dataset.hasAnswer == 'true') { const correct = document.createElement("button"); correct.classList.add("show-correct", 'webwork-button'); correct.type = "button"; correct.style.marginRight = "0.25rem"; correct.textContent = localize_reveal; correct.addEventListener('click', () => handleWW(ww_id, 'reveal')); buttonContainer.appendChild(correct); } // randomize button const randomize = document.createElement("button") randomize.type = "button"; randomize.classList.add('webwork-button'); randomize.style.marginRight = "0.25rem"; randomize.textContent = localize_randomize; randomize.addEventListener('click', () => handleWW(ww_id, 'randomize')); buttonContainer.appendChild(randomize) // reset button const reset = document.createElement("button") reset.type = "button" reset.classList.add('webwork-button'); reset.textContent = localize_reset; reset.addEventListener('click', () => resetWW(ww_id)); buttonContainer.appendChild(reset) } if (runestone_logged_in && action == 'check') { // Runestone trigger $("body").trigger('runestone_ww_check', data) } let courseUrlBase = ''; if (runestone_logged_in){ courseUrlBase = '/ns/books/published/' + eBookConfig.basecourse + '/'; } let iframeContents = '
' + '' + `` + '' + `` + '' + ''; // Determine javascript and css dependencies const extra_css_files = []; const extra_js_files = []; if (data.extra_css_files) data.extra_css_files.unshift(...extra_css_files); else data.extra_css_files = extra_css_files; for (const cssFile of data.extra_css_files) { iframeContents += ''; } if (data.extra_js_files) data.extra_js_files.unshift(...extra_js_files); else data.extra_js_files = extra_js_files; for (const jsFile of data.extra_js_files) { iframeContents += ''; } iframeContents += `` + '' + `` + '' + '