diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index 09e9d1e6ab6..f54cf24b418 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -166,7 +166,7 @@ jobs: run: moodle-plugin-ci mustache - name: Grunt - if: ${{ matrix.moodle-branch == 'MOODLE_311_STABLE' }} + if: ${{ matrix.moodle-branch == 'MOODLE_401_STABLE' }} run: moodle-plugin-ci grunt - name: PHPUnit tests diff --git a/Readme.md b/Readme.md index ee20bac312d..4a8390e24a9 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,4 @@ -# STACK 4.4.5 +# STACK 4.4.6 STACK is an assessment system for mathematics, science and related disciplines. STACK is a question type for the Moodle learning management system, and also the ILIAS learning management system. diff --git a/amd/build/input.min.js b/amd/build/input.min.js index 9c8699bf7b6..0576ecda4c7 100644 --- a/amd/build/input.min.js +++ b/amd/build/input.min.js @@ -19,6 +19,6 @@ * @copyright 2018 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("qtype_stack/input",["core/ajax","core/event"],(function(Ajax,CustomEvents){function StackInput(validationDiv,prefix,qaid,name,input){var TYPING_DELAY=1e3,delayTimeoutHandle=null,validationResults={},lastValidatedValue=getInputValue();function cancelTypingDelay(){delayTimeoutHandle&&clearTimeout(delayTimeoutHandle),delayTimeoutHandle=null}function valueChanging(){cancelTypingDelay(),showWaiting(),delayTimeoutHandle=setTimeout(valueChanged,TYPING_DELAY),setTimeout((function(){checkNoChange()}),0)}function checkNoChange(){getInputValue()===lastValidatedValue&&(cancelTypingDelay(),validationDiv.classList.remove("waiting"))}function valueChanged(){cancelTypingDelay(),showValidationResults()||validateInput()}function validateInput(){Ajax.call([{methodname:"qtype_stack_validate_input",args:{qaid:qaid,name:name,input:getInputValue()},done:function(response){validationReceived(response)},fail:function(response){showValidationFailure(response)}}]),showLoading()}function getInputValue(){return input.getValue()}function validationReceived(response){"invalid"!==response.status?(validationResults[response.input]=response,showValidationResults()):showValidationFailure(response)}function extractScripts(html,scriptCommands){for(var result,scriptregexp=/]*>([\s\S]*?)<\/script>/g;null!==(result=scriptregexp.exec(html));)scriptCommands.push(result[1]);return html.replace(scriptregexp,"")}function showValidationResults(){var val=getInputValue();if(!validationResults[val])return showWaiting(),!1;var results=validationResults[val];lastValidatedValue=val;var scriptCommands=[];validationDiv.innerHTML=extractScripts(results.message,scriptCommands);for(var i=0;i")}}function StackRadioInput(container){this.addEventHandlers=function(valueChanging){container.addEventListener("input",valueChanging)},this.getValue=function(){var selected=container.querySelector(":checked");return selected?selected.value:""}}function StackCheckboxInput(container){this.addEventHandlers=function(valueChanging){container.addEventListener("input",valueChanging)},this.getValue=function(){for(var selected=container.querySelectorAll(":checked"),result=[],i=0;i0?result.join(","):""}}function StackMatrixInput(idPrefix,container){var numcol=0,numrow=0;container.querySelectorAll("input[type=text]").forEach((function(element){if(element.name.slice(0,idPrefix.length+5)===idPrefix+"_sub_"){var bits=element.name.substring(idPrefix.length+5).split("_");numrow=Math.max(numrow,parseInt(bits[0],10)+1),numcol=Math.max(numcol,parseInt(bits[1],10)+1)}})),this.addEventHandlers=function(valueChanging){container.addEventListener("input",valueChanging)},this.getValue=function(){for(var values=new Array(numrow),i=0;i]*>([\s\S]*?)<\/script>/g;null!==(result=scriptregexp.exec(html));)scriptCommands.push(result[1]);return html.replace(scriptregexp,"")}function showValidationResults(){var val=getInputValue();if(!validationResults[val])return showWaiting(),!1;var results=validationResults[val];lastValidatedValue=val;var scriptCommands=[];validationDiv.innerHTML=extractScripts(results.message,scriptCommands);for(var i=0;i")}}function StackRadioInput(container){this.addEventHandlers=function(valueChanging){container.addEventListener("input",valueChanging)},this.getValue=function(){var selected=container.querySelector(":checked");return selected?selected.value:""}}function StackCheckboxInput(container){this.addEventHandlers=function(valueChanging){container.addEventListener("input",valueChanging)},this.getValue=function(){for(var selected=container.querySelectorAll(":checked"),result=[],i=0;i0?result.join(","):""}}function StackMatrixInput(idPrefix,container){var numcol=0,numrow=0;container.querySelectorAll("input[type=text]").forEach((function(element){if(element.name.slice(0,idPrefix.length+5)===idPrefix+"_sub_"){var bits=element.name.substring(idPrefix.length+5).split("_");numrow=Math.max(numrow,parseInt(bits[0],10)+1),numcol=Math.max(numcol,parseInt(bits[1],10)+1)}})),this.addEventHandlers=function(valueChanging){container.addEventListener("input",valueChanging)},this.getValue=function(){for(var values=new Array(numrow),i=0;i.\n\n/**\n * A javascript module to handle the real-time validation of the input the student types\n * into STACK questions.\n *\n * The overall way this works is as follows:\n *\n * - right at the end of this file are the init methods, which set things up.\n * - The work common to all input types is done by StackInput.\n * - Sending the Ajax request.\n * - Updating the validation display.\n * - The work specific to different input types (getting the content of the inputs) is done by\n * the classes like\n * - StackSimpleInput\n * - StackTextareaInput\n * - StackMatrixInput\n * objects of these types need to implement the two methods addEventHandlers and getValue().\n *\n * @module qtype_stack/input\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'core/ajax',\n 'core/event'\n], function(\n Ajax,\n CustomEvents\n) {\n\n \"use strict\";\n\n /**\n * Class constructor representing an input in a Stack question.\n *\n * @constructor\n * @param {HTMLElement} validationDiv The div to display the validation in.\n * @param {String} prefix prefix added to the input name to get HTML ids.\n * @param {String} qaid id of the question_attempt.\n * @param {String} name the name of the input we are validating.\n * @param {Object} input An object representing the input element for this input.\n */\n function StackInput(validationDiv, prefix, qaid, name, input) {\n /** @type {number} delay between the user stopping typing, and the ajax request being sent. */\n var TYPING_DELAY = 1000;\n\n /** @type {?int} if not null, the id of the timer for the typing delay. */\n var delayTimeoutHandle = null;\n\n /** @type {Object} cache of validation results we have already received. */\n var validationResults = {};\n\n /** @type {String} the last value that we sent to be validated. */\n var lastValidatedValue = getInputValue();\n\n /**\n * Cancel any typing pause timer.\n */\n function cancelTypingDelay() {\n if (delayTimeoutHandle) {\n clearTimeout(delayTimeoutHandle);\n }\n delayTimeoutHandle = null;\n }\n\n input.addEventHandlers(valueChanging);\n\n /**\n * Called when the input contents changes. Will validate after TYPING_DELAY if nothing else happens.\n */\n function valueChanging() {\n cancelTypingDelay();\n showWaiting();\n delayTimeoutHandle = setTimeout(valueChanged, TYPING_DELAY);\n setTimeout(function() {\n checkNoChange();\n }, 0);\n }\n\n /**\n * After a small delay, detect the case where the user has got the input back\n * to where they started, so no validation is necessary.\n */\n function checkNoChange() {\n if (getInputValue() === lastValidatedValue) {\n cancelTypingDelay();\n validationDiv.classList.remove('waiting');\n }\n }\n\n /**\n * Called to actually validate the input now.\n */\n function valueChanged() {\n cancelTypingDelay();\n if (!showValidationResults()) {\n validateInput();\n }\n }\n\n /**\n * Make an ajax call to validate the input.\n */\n function validateInput() {\n Ajax.call([{\n methodname: 'qtype_stack_validate_input',\n args: {qaid: qaid, name: name, input: getInputValue()},\n done: function(response) {\n validationReceived(response);\n },\n fail: function(response) {\n showValidationFailure(response);\n }\n }]);\n showLoading();\n }\n\n /**\n * Returns the current value of the input.\n *\n * @return {String}.\n */\n function getInputValue() {\n return input.getValue();\n }\n\n /**\n * Update the validation div to show the results of the validation.\n *\n * @param {Object} response The data that came back from the ajax validation call.\n */\n function validationReceived(response) {\n if (response.status === 'invalid') {\n showValidationFailure(response);\n return;\n }\n validationResults[response.input] = response;\n showValidationResults();\n }\n\n /**\n * Some browsers cannot execute JavaScript just by inserting script tags.\n * To avoid that problem, remove all script tags from the given content,\n * and run them later.\n *\n * @param {String} html HTML content\n * @param {Array} scriptCommands An array of script tags for later use.\n * @return {String} HTML with JS removed\n */\n function extractScripts(html, scriptCommands) {\n var scriptregexp = /]*>([\\s\\S]*?)<\\/script>/g;\n var result;\n while ((result = scriptregexp.exec(html)) !== null) {\n scriptCommands.push(result[1]);\n }\n return html.replace(scriptregexp, '');\n }\n\n /**\n * Update the validation div to show the results of the validation.\n *\n * @return {boolean} true if we could show the validation. false we we are we don't have it.\n */\n function showValidationResults() {\n /* eslint no-eval: \"off\" */\n var val = getInputValue();\n if (!validationResults[val]) {\n showWaiting();\n return false;\n }\n var results = validationResults[val];\n lastValidatedValue = val;\n var scriptCommands = [];\n validationDiv.innerHTML = extractScripts(results.message, scriptCommands);\n // Run script commands.\n for (var i = 0; i < scriptCommands.length; i++) {\n eval(scriptCommands[i]);\n }\n removeAllClasses();\n if (!results.message) {\n validationDiv.classList.add('empty');\n }\n // This fires the Maths filters for content in the validation div.\n CustomEvents.notifyFilterContentUpdated(validationDiv);\n return true;\n }\n\n /**\n * Update the validation div after an ajax validation call failed.\n *\n * @param {Object} response The data that came back from the ajax validation call.\n */\n function showValidationFailure(response) {\n lastValidatedValue = '';\n // Reponse usually contains backtrace, debuginfo, errorcode, link, message and moreinfourl.\n validationDiv.innerHTML = response.message;\n removeAllClasses();\n validationDiv.classList.add('error');\n // This fires the Maths filters for content in the validation div.\n CustomEvents.notifyFilterContentUpdated(validationDiv);\n }\n\n /**\n * Display the loader icon.\n */\n function showLoading() {\n removeAllClasses();\n validationDiv.classList.add('loading');\n }\n\n /**\n * Update the validation div to show that the input contents have changed,\n * so the validation results are no longer relevant.\n */\n function showWaiting() {\n removeAllClasses();\n validationDiv.classList.add('waiting');\n }\n\n /**\n * Strip all our class names from the validation div.\n */\n function removeAllClasses() {\n validationDiv.classList.remove('empty');\n validationDiv.classList.remove('error');\n validationDiv.classList.remove('loading');\n validationDiv.classList.remove('waiting');\n }\n }\n\n /**\n * Input type for inputs that are a single input or select.\n *\n * @constructor\n * @param {HTMLElement} input the HTML input that is this STACK input.\n */\n function StackSimpleInput(input) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n // The input event fires on any change in value, even if pasted in or added by speech\n // recognition to dictate text. Change only fires after loosing focus.\n // Should also work on mobile.\n input.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n return input.value.replace(/^\\s+|\\s+$/g, '');\n };\n }\n\n /**\n * Input type for textarea inputs.\n *\n * @constructor\n * @param {Object} textarea The input element wrapped in jquery.\n */\n function StackTextareaInput(textarea) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n textarea.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var raw = textarea.value.replace(/^\\s+|\\s+$/g, '');\n // Using
here is weird, but it gets sorted out at the PHP end.\n return raw.split(/\\s*[\\r\\n]\\s*/).join('
');\n };\n }\n\n /**\n * Input type for inputs that are a set of radio buttons.\n *\n * @constructor\n * @param {HTMLElement} container container
of this input.\n */\n function StackRadioInput(container) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n // The input event fires on any change in value, even if pasted in or added by speech\n // recognition to dictate text. Change only fires after loosing focus.\n // Should also work on mobile.\n container.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var selected = container.querySelector(':checked');\n if (selected) {\n return selected.value;\n } else {\n return '';\n }\n };\n }\n\n /**\n * Input type for inputs that are a set of checkboxes.\n *\n * @constructor\n * @param {HTMLElement} container container
of this input.\n */\n function StackCheckboxInput(container) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n // The input event fires on any change in value, even if pasted in or added by speech\n // recognition to dictate text. Change only fires after loosing focus.\n // Should also work on mobile.\n container.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var selected = container.querySelectorAll(':checked');\n var result = [];\n for (var i = 0; i < selected.length; i++) {\n result[i] = selected[i].value;\n }\n if (result.length > 0) {\n return result.join(',');\n } else {\n return '';\n }\n };\n }\n\n /**\n * Class constructor representing matrix inputs (one input).\n *\n * @constructor\n * @param {String} idPrefix input id, which is the start of the id of all the different text boxes.\n * @param {HTMLElement} container
of this input.\n */\n function StackMatrixInput(idPrefix, container) {\n var numcol = 0;\n var numrow = 0;\n container.querySelectorAll('input[type=text]').forEach(function(element) {\n if (element.name.slice(0, idPrefix.length + 5) !== idPrefix + '_sub_') {\n return;\n }\n var bits = element.name.substring(idPrefix.length + 5).split('_');\n numrow = Math.max(numrow, parseInt(bits[0], 10) + 1);\n numcol = Math.max(numcol, parseInt(bits[1], 10) + 1);\n });\n\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n container.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var values = new Array(numrow);\n for (var i = 0; i < numrow; i++) {\n values[i] = new Array(numcol);\n }\n container.querySelectorAll('input[type=text]').forEach(function(element) {\n if (element.name.slice(0, idPrefix.length + 5) !== idPrefix + '_sub_') {\n return;\n }\n var bits = element.name.substring(idPrefix.length + 5).split('_');\n values[bits[0]][bits[1]] = element.value.replace(/^\\s+|\\s+$/g, '');\n });\n return 'matrix([' + values.join('],[') + '])';\n };\n }\n\n /**\n * Initialise all the inputs in a STACK question.\n *\n * @param {String} questionDivId id of the outer dic of the question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} qaid Moodle question_attempt id.\n * @param {String[]} inputs names of all the inputs that should have instant validation.\n */\n function initInputs(questionDivId, prefix, qaid, inputs) {\n var questionDiv = document.getElementById(questionDivId);\n\n // Initialise all inputs.\n var allok = true;\n for (var i = 0; i < inputs.length; i++) {\n allok = initInput(questionDiv, prefix, qaid, inputs[i]) && allok;\n }\n\n // With JS With instant validation, we don't need the Check button, so hide it.\n if (allok && (questionDiv.classList.contains('dfexplicitvaildate') ||\n questionDiv.classList.contains('dfcbmexplicitvaildate'))) {\n questionDiv.querySelector('.im-controls input.submit').hidden = true;\n }\n }\n\n /**\n * Initialise one input.\n *\n * @param {HTMLElement} questionDiv outer
of this question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} qaid Moodle question_attempt id.\n * @param {String} name the input to initialise.\n * @return {boolean} true if this input was successfully initialised, else false.\n */\n function initInput(questionDiv, prefix, qaid, name) {\n var validationDiv = document.getElementById(prefix + name + '_val');\n if (!validationDiv) {\n return false;\n }\n\n var inputTypeHandler = getInputTypeHandler(questionDiv, prefix, name);\n if (inputTypeHandler) {\n new StackInput(validationDiv, prefix, qaid, name, inputTypeHandler);\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * Get the input type handler for a named input.\n *\n * @param {HTMLElement} questionDiv outer
of this question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} name the input to initialise.\n * @return {?Object} the input hander, if we can handle it, else null.\n */\n function getInputTypeHandler(questionDiv, prefix, name) {\n // See if it is an ordinary input.\n var input = questionDiv.querySelector('[name=\"' + prefix + name + '\"]');\n if (input) {\n if (input.nodeName === 'TEXTAREA') {\n return new StackTextareaInput(input);\n } else if (input.type === 'radio') {\n return new StackRadioInput(input.closest('.answer'));\n } else {\n return new StackSimpleInput(input);\n }\n }\n\n // See if it is a checkbox input.\n input = questionDiv.querySelector('[name=\"' + prefix + name + '_1\"]');\n if (input && input.type === 'checkbox') {\n return new StackCheckboxInput(input.closest('.answer'));\n }\n\n // See if it is a matrix input.\n var matrix = document.getElementById(prefix + name + '_container');\n if (matrix) {\n return new StackMatrixInput(prefix + name, matrix);\n }\n\n return null;\n }\n\n /** Export our entry point. */\n return {\n /**\n * Initialise all the inputs in a STACK question.\n *\n * @param {String} questionDivId id of the outer dic of the question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} qaid Moodle question_attempt id.\n * @param {String[]} inputs names of all the inputs that should have instant validation.\n */\n initInputs: initInputs\n };\n});\n"],"names":["define","Ajax","CustomEvents","StackInput","validationDiv","prefix","qaid","name","input","TYPING_DELAY","delayTimeoutHandle","validationResults","lastValidatedValue","getInputValue","cancelTypingDelay","clearTimeout","valueChanging","showWaiting","setTimeout","valueChanged","checkNoChange","classList","remove","showValidationResults","validateInput","call","methodname","args","done","response","validationReceived","fail","showValidationFailure","showLoading","getValue","status","extractScripts","html","scriptCommands","result","scriptregexp","exec","push","replace","val","results","innerHTML","message","i","length","eval","removeAllClasses","add","notifyFilterContentUpdated","addEventHandlers","StackSimpleInput","addEventListener","value","StackTextareaInput","textarea","split","join","StackRadioInput","container","selected","querySelector","StackCheckboxInput","querySelectorAll","StackMatrixInput","idPrefix","numcol","numrow","forEach","element","slice","bits","substring","Math","max","parseInt","values","Array","initInputs","questionDivId","inputs","questionDiv","document","getElementById","allok","initInput","contains","hidden","inputTypeHandler","getInputTypeHandler","nodeName","type","closest","matrix"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"input.min.js","sources":["../src/input.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A javascript module to handle the real-time validation of the input the student types\n * into STACK questions.\n *\n * The overall way this works is as follows:\n *\n * - right at the end of this file are the init methods, which set things up.\n * - The work common to all input types is done by StackInput.\n * - Sending the Ajax request.\n * - Updating the validation display.\n * - The work specific to different input types (getting the content of the inputs) is done by\n * the classes like\n * - StackSimpleInput\n * - StackTextareaInput\n * - StackMatrixInput\n * objects of these types need to implement the two methods addEventHandlers and getValue().\n *\n * @module qtype_stack/input\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'core/ajax',\n 'core/event'\n], function(\n Ajax,\n CustomEvents\n) {\n\n \"use strict\";\n\n /**\n * Class constructor representing an input in a Stack question.\n *\n * @constructor\n * @param {HTMLElement} validationDiv The div to display the validation in.\n * @param {String} prefix prefix added to the input name to get HTML ids.\n * @param {String} qaid id of the question_attempt.\n * @param {String} name the name of the input we are validating.\n * @param {Object} input An object representing the input element for this input.\n */\n function StackInput(validationDiv, prefix, qaid, name, input) {\n /** @type {number} delay between the user stopping typing, and the ajax request being sent. */\n var TYPING_DELAY = 1000;\n\n /** @type {?int} if not null, the id of the timer for the typing delay. */\n var delayTimeoutHandle = null;\n\n /** @type {Object} cache of validation results we have already received. */\n var validationResults = {};\n\n /** @type {String} the last value that we sent to be validated. */\n var lastValidatedValue = getInputValue();\n\n /**\n * Cancel any typing pause timer.\n */\n function cancelTypingDelay() {\n if (delayTimeoutHandle) {\n clearTimeout(delayTimeoutHandle);\n }\n delayTimeoutHandle = null;\n }\n\n input.addEventHandlers(valueChanging);\n\n /**\n * Called when the input contents changes. Will validate after TYPING_DELAY if nothing else happens.\n */\n function valueChanging() {\n cancelTypingDelay();\n showWaiting();\n delayTimeoutHandle = setTimeout(valueChanged, TYPING_DELAY);\n setTimeout(function() {\n checkNoChange();\n }, 0);\n }\n\n /**\n * After a small delay, detect the case where the user has got the input back\n * to where they started, so no validation is necessary.\n */\n function checkNoChange() {\n if (getInputValue() === lastValidatedValue) {\n cancelTypingDelay();\n validationDiv.classList.remove('waiting');\n }\n }\n\n /**\n * Called to actually validate the input now.\n */\n function valueChanged() {\n cancelTypingDelay();\n if (!showValidationResults()) {\n validateInput();\n }\n }\n\n /**\n * Make an ajax call to validate the input.\n */\n function validateInput() {\n Ajax.call([{\n methodname: 'qtype_stack_validate_input',\n args: {qaid: qaid, name: name, input: getInputValue()},\n done: function(response) {\n validationReceived(response);\n },\n fail: function(response) {\n showValidationFailure(response);\n }\n }]);\n showLoading();\n }\n\n /**\n * Returns the current value of the input.\n *\n * @return {String}.\n */\n function getInputValue() {\n return input.getValue();\n }\n\n /**\n * Update the validation div to show the results of the validation.\n *\n * @param {Object} response The data that came back from the ajax validation call.\n */\n function validationReceived(response) {\n if (response.status === 'invalid') {\n showValidationFailure(response);\n return;\n }\n validationResults[response.input] = response;\n showValidationResults();\n }\n\n /**\n * Some browsers cannot execute JavaScript just by inserting script tags.\n * To avoid that problem, remove all script tags from the given content,\n * and run them later.\n *\n * @param {String} html HTML content\n * @param {Array} scriptCommands An array of script tags for later use.\n * @return {String} HTML with JS removed\n */\n function extractScripts(html, scriptCommands) {\n var scriptregexp = /]*>([\\s\\S]*?)<\\/script>/g;\n var result;\n while ((result = scriptregexp.exec(html)) !== null) {\n scriptCommands.push(result[1]);\n }\n return html.replace(scriptregexp, '');\n }\n\n /**\n * Update the validation div to show the results of the validation.\n *\n * @return {boolean} true if we could show the validation. false we we are we don't have it.\n */\n function showValidationResults() {\n /* eslint no-eval: \"off\" */\n var val = getInputValue();\n if (!validationResults[val]) {\n showWaiting();\n return false;\n }\n var results = validationResults[val];\n lastValidatedValue = val;\n var scriptCommands = [];\n validationDiv.innerHTML = extractScripts(results.message, scriptCommands);\n // Run script commands.\n for (var i = 0; i < scriptCommands.length; i++) {\n eval(scriptCommands[i]);\n }\n removeAllClasses();\n if (!results.message) {\n validationDiv.classList.add('empty');\n }\n // This fires the Maths filters for content in the validation div.\n CustomEvents.notifyFilterContentUpdated(validationDiv);\n return true;\n }\n\n /**\n * Update the validation div after an ajax validation call failed.\n *\n * @param {Object} response The data that came back from the ajax validation call.\n */\n function showValidationFailure(response) {\n lastValidatedValue = '';\n // Reponse usually contains backtrace, debuginfo, errorcode, link, message and moreinfourl.\n validationDiv.innerHTML = response.message;\n removeAllClasses();\n validationDiv.classList.add('error');\n // This fires the Maths filters for content in the validation div.\n CustomEvents.notifyFilterContentUpdated(validationDiv);\n }\n\n /**\n * Display the loader icon.\n */\n function showLoading() {\n removeAllClasses();\n validationDiv.classList.add('loading');\n }\n\n /**\n * Update the validation div to show that the input contents have changed,\n * so the validation results are no longer relevant.\n */\n function showWaiting() {\n removeAllClasses();\n validationDiv.classList.add('waiting');\n }\n\n /**\n * Strip all our class names from the validation div.\n */\n function removeAllClasses() {\n validationDiv.classList.remove('empty');\n validationDiv.classList.remove('error');\n validationDiv.classList.remove('loading');\n validationDiv.classList.remove('waiting');\n }\n }\n\n /**\n * Input type for inputs that are a single input or select.\n *\n * @constructor\n * @param {HTMLElement} input the HTML input that is this STACK input.\n */\n function StackSimpleInput(input) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n // The input event fires on any change in value, even if pasted in or added by speech\n // recognition to dictate text. Change only fires after loosing focus.\n // Should also work on mobile.\n input.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n return input.value.replace(/^\\s+|\\s+$/g, '');\n };\n }\n\n /**\n * Input type for textarea inputs.\n *\n * @constructor\n * @param {Object} textarea The input element wrapped in jquery.\n */\n function StackTextareaInput(textarea) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n textarea.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var raw = textarea.value.replace(/^\\s+|\\s+$/g, '');\n // Using
here is weird, but it gets sorted out at the PHP end.\n return raw.split(/\\s*[\\r\\n]\\s*/).join('
');\n };\n }\n\n /**\n * Input type for inputs that are a set of radio buttons.\n *\n * @constructor\n * @param {HTMLElement} container container
of this input.\n */\n function StackRadioInput(container) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n // The input event fires on any change in value, even if pasted in or added by speech\n // recognition to dictate text. Change only fires after loosing focus.\n // Should also work on mobile.\n container.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var selected = container.querySelector(':checked');\n if (selected) {\n return selected.value;\n } else {\n return '';\n }\n };\n }\n\n /**\n * Input type for inputs that are a set of checkboxes.\n *\n * @constructor\n * @param {HTMLElement} container container
of this input.\n */\n function StackCheckboxInput(container) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n // The input event fires on any change in value, even if pasted in or added by speech\n // recognition to dictate text. Change only fires after loosing focus.\n // Should also work on mobile.\n container.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var selected = container.querySelectorAll(':checked');\n var result = [];\n for (var i = 0; i < selected.length; i++) {\n result[i] = selected[i].value;\n }\n if (result.length > 0) {\n return result.join(',');\n } else {\n return '';\n }\n };\n }\n\n /**\n * Class constructor representing matrix inputs (one input).\n *\n * @constructor\n * @param {String} idPrefix input id, which is the start of the id of all the different text boxes.\n * @param {HTMLElement} container
of this input.\n */\n function StackMatrixInput(idPrefix, container) {\n var numcol = 0;\n var numrow = 0;\n container.querySelectorAll('input[type=text]').forEach(function(element) {\n if (element.name.slice(0, idPrefix.length + 5) !== idPrefix + '_sub_') {\n return;\n }\n var bits = element.name.substring(idPrefix.length + 5).split('_');\n numrow = Math.max(numrow, parseInt(bits[0], 10) + 1);\n numcol = Math.max(numcol, parseInt(bits[1], 10) + 1);\n });\n\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n container.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var values = new Array(numrow);\n for (var i = 0; i < numrow; i++) {\n values[i] = new Array(numcol);\n }\n container.querySelectorAll('input[type=text]').forEach(function(element) {\n if (element.name.slice(0, idPrefix.length + 5) !== idPrefix + '_sub_') {\n return;\n }\n var bits = element.name.substring(idPrefix.length + 5).split('_');\n values[bits[0]][bits[1]] = element.value.replace(/^\\s+|\\s+$/g, '');\n });\n return 'matrix([' + values.join('],[') + '])';\n };\n }\n\n /**\n * Initialise all the inputs in a STACK question.\n *\n * @param {String} questionDivId id of the outer div of the question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} qaid Moodle question_attempt id.\n * @param {String[]} inputs names of all the inputs that should have instant validation.\n */\n function initInputs(questionDivId, prefix, qaid, inputs) {\n var questionDiv = document.getElementById(questionDivId);\n\n // Initialise all inputs.\n var allok = true;\n for (var i = 0; i < inputs.length; i++) {\n allok = initInput(questionDiv, prefix, qaid, inputs[i]) && allok;\n }\n\n // With JS With instant validation, we don't need the Check button, so hide it.\n if (allok && (questionDiv.classList.contains('dfexplicitvaildate') ||\n questionDiv.classList.contains('dfcbmexplicitvaildate'))) {\n questionDiv.querySelector('.im-controls input.submit, .im-controls button.submit').hidden = true;\n }\n }\n\n /**\n * Initialise one input.\n *\n * @param {HTMLElement} questionDiv outer
of this question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} qaid Moodle question_attempt id.\n * @param {String} name the input to initialise.\n * @return {boolean} true if this input was successfully initialised, else false.\n */\n function initInput(questionDiv, prefix, qaid, name) {\n var validationDiv = document.getElementById(prefix + name + '_val');\n if (!validationDiv) {\n return false;\n }\n\n var inputTypeHandler = getInputTypeHandler(questionDiv, prefix, name);\n if (inputTypeHandler) {\n new StackInput(validationDiv, prefix, qaid, name, inputTypeHandler);\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * Get the input type handler for a named input.\n *\n * @param {HTMLElement} questionDiv outer
of this question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} name the input to initialise.\n * @return {?Object} the input hander, if we can handle it, else null.\n */\n function getInputTypeHandler(questionDiv, prefix, name) {\n // See if it is an ordinary input.\n var input = questionDiv.querySelector('[name=\"' + prefix + name + '\"]');\n if (input) {\n if (input.nodeName === 'TEXTAREA') {\n return new StackTextareaInput(input);\n } else if (input.type === 'radio') {\n return new StackRadioInput(input.closest('.answer'));\n } else {\n return new StackSimpleInput(input);\n }\n }\n\n // See if it is a checkbox input.\n input = questionDiv.querySelector('[name=\"' + prefix + name + '_1\"]');\n if (input && input.type === 'checkbox') {\n return new StackCheckboxInput(input.closest('.answer'));\n }\n\n // See if it is a matrix input.\n var matrix = document.getElementById(prefix + name + '_container');\n if (matrix) {\n return new StackMatrixInput(prefix + name, matrix);\n }\n\n return null;\n }\n\n /** Export our entry point. */\n return {\n /**\n * Initialise all the inputs in a STACK question.\n *\n * @param {String} questionDivId id of the outer dic of the question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} qaid Moodle question_attempt id.\n * @param {String[]} inputs names of all the inputs that should have instant validation.\n */\n initInputs: initInputs\n };\n});\n"],"names":["define","Ajax","CustomEvents","StackInput","validationDiv","prefix","qaid","name","input","TYPING_DELAY","delayTimeoutHandle","validationResults","lastValidatedValue","getInputValue","cancelTypingDelay","clearTimeout","valueChanging","showWaiting","setTimeout","valueChanged","checkNoChange","classList","remove","showValidationResults","validateInput","call","methodname","args","done","response","validationReceived","fail","showValidationFailure","showLoading","getValue","status","extractScripts","html","scriptCommands","result","scriptregexp","exec","push","replace","val","results","innerHTML","message","i","length","eval","removeAllClasses","add","notifyFilterContentUpdated","addEventHandlers","StackSimpleInput","addEventListener","value","StackTextareaInput","textarea","split","join","StackRadioInput","container","selected","querySelector","StackCheckboxInput","querySelectorAll","StackMatrixInput","idPrefix","numcol","numrow","forEach","element","slice","bits","substring","Math","max","parseInt","values","Array","initInputs","questionDivId","inputs","questionDiv","document","getElementById","allok","initInput","contains","hidden","inputTypeHandler","getInputTypeHandler","nodeName","type","closest","matrix"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/stackjsvle.min.js b/amd/build/stackjsvle.min.js index fe017c90a21..198252ae046 100644 --- a/amd/build/stackjsvle.min.js +++ b/amd/build/stackjsvle.min.js @@ -1,4 +1,3 @@ -function _createForOfIteratorHelper(o,allowArrayLike){var it="undefined"!=typeof Symbol&&o[Symbol.iterator]||o["@@iterator"];if(!it){if(Array.isArray(o)||(it=function(o,minLen){if(!o)return;if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);"Object"===n&&o.constructor&&(n=o.constructor.name);if("Map"===n||"Set"===n)return Array.from(o);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}(o))||allowArrayLike&&o&&"number"==typeof o.length){it&&(o=it);var i=0,F=function(){};return{s:F,n:function(){return i>=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i{if(!("string"==typeof e.data||e.data instanceof String))return;let msg=null;try{msg=JSON.parse(e.data)}catch(e){return}if(!("version"in msg)||!msg.version.startsWith("STACK-JS"))return;if(!("src"in msg&&"type"in msg&&msg.src in IFRAMES))return;let element=null,input=null,response={version:"STACK-JS:1.0.0"};switch(msg.type){case"register-input-listener":if(input=vle_get_input_element(msg.name,msg.src),null===input)return response.type="error",response.msg='Failed to connect to input: "'+msg.name+'"',response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");if(response.type="initial-input",response.name=msg.name,response.tgt=msg.src,"select"===input.nodeName.toLowerCase()?(response.value=input.value,response["input-type"]="select"):"checkbox"===input.type?(response.value=input.checked,response["input-type"]="checkbox"):(response.value=input.value,response["input-type"]=input.type),"radio"===input.type){response.value="";for(let inp of document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]"))inp.checked&&(response.value=inp.value)}if(input.id in INPUTS){if(msg.src in INPUTS[input.id])return;if("radio"!==input.type)INPUTS[input.id].push(msg.src);else{let radgroup=document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]");for(let inp of radgroup)INPUTS[inp.id].push(msg.src)}}else{if("radio"!==input.type)INPUTS[input.id]=[msg.src];else{let radgroup=document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]");for(let inp of radgroup)INPUTS[inp.id]=[msg.src]}if("radio"!==input.type)input.addEventListener("change",(()=>{if(DISABLE_CHANGES)return;let resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};"checkbox"===input.type?resp.value=input.checked:resp.value=input.value;for(let tgt of INPUTS[input.id])resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}));else{document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]").forEach((inp=>{inp.addEventListener("change",(()=>{if(DISABLE_CHANGES)return;let resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};if(inp.checked){resp.value=inp.value;for(let tgt of INPUTS[inp.id])resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}}))}))}}if("track-input"in msg&&msg["track-input"]&&"radio"!==input.type)if(input.id in INPUTS_INPUT_EVENT){if(msg.src in INPUTS_INPUT_EVENT[input.id])return;INPUTS_INPUT_EVENT[input.id].push(msg.src)}else INPUTS_INPUT_EVENT[input.id]=[msg.src],input.addEventListener("input",(()=>{if(DISABLE_CHANGES)return;let resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};"checkbox"===input.type?resp.value=input.checked:resp.value=input.value;for(let tgt of INPUTS_INPUT_EVENT[input.id])resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}));msg.src in INPUTS[input.id]||IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");break;case"changed-input":if(input=vle_get_input_element(msg.name,msg.src),null===input){const ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to modify input: "'+msg.name+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}DISABLE_CHANGES=!0,"checkbox"===input.type?input.checked=msg.value:input.value=msg.value,function(inputelement){const c=new Event("change");inputelement.dispatchEvent(c);const i=new Event("input");inputelement.dispatchEvent(i)}(input),DISABLE_CHANGES=!1,response.type="changed-input",response.name=msg.name,response.value=msg.value;for(let tgt of INPUTS[input.id])tgt!==msg.src&&(response.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(response),"*"));break;case"toggle-visibility":if(element=vle_get_element(msg.target),null===element){const ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to find element: "'+msg.target+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}"show"===msg.set?(element.style.display="block",vle_update_dom(element)):"hide"===msg.set&&(element.style.display="none");break;case"change-content":if(element=vle_get_element(msg.target),null===element){const ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to find element: "'+msg.target+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}element.replaceChildren(function(src){let doc=(new DOMParser).parseFromString(src);for(let el of doc.querySelectorAll("script, style"))el.remove();for(let el of doc.querySelectorAll("*"))for(let{name:name,value:value}of el.attributes)is_evil_attribute(name,value)&&el.removeAttribute(name);return doc.body}(msg.content)),vle_update_dom(element);break;case"resize-frame":element=IFRAMES[msg.src].parentElement,element.style.width=msg.width,element.style.height=msg.height,IFRAMES[msg.src].style.width="100%",IFRAMES[msg.src].style.height="100%",vle_update_dom(element);break;case"ping":return response.type="ping",response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");case"initial-input":case"error":break;default:response.type="error",response.msg='Unknown message-type: "'+msg.type+'"',response.tgt=msg.src,IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*")}})),{create_iframe(iframeid,content,targetdivid,title,scrolling){const frm=document.createElement("iframe");frm.id=iframeid,frm.style.width="100%",frm.style.height="100%",frm.style.border=0,!1===scrolling?(frm.scrolling="no",frm.style.overflow="hidden"):frm.scrolling="yes",frm.title=title,frm.referrerpolicy="no-referrer",frm.sandbox="allow-scripts allow-downloads",frm.csp="script-src: 'unsafe-inline' 'self' '*';",document.getElementById(targetdivid).replaceChildren(frm),IFRAMES[iframeid]=frm;const src=new Blob([content],{type:"text/html; charset=utf-8"});frm.src=URL.createObjectURL(src)}}})); //# sourceMappingURL=stackjsvle.min.js.map \ No newline at end of file diff --git a/amd/build/stackjsvle.min.js.map b/amd/build/stackjsvle.min.js.map index 9aa14aa6ac9..f39fe520a3f 100644 --- a/amd/build/stackjsvle.min.js.map +++ b/amd/build/stackjsvle.min.js.map @@ -1 +1 @@ -{"version":3,"file":"stackjsvle.min.js","sources":["../src/stackjsvle.js"],"sourcesContent":["/**\n * A javascript module to handle separation of author sourced scripts into\n * IFRAMES. All such scripts will have limited access to the actual document\n * on the VLE side and this script represents the VLE side endpoint for\n * message handling needed to give that access. When porting STACK onto VLEs\n * one needs to map this script to do the following:\n *\n * 1. Ensure that searches for target elements/inputs are limited to questions\n * and do not return any elements outside them.\n *\n * 2. Map any identifiers needed to identify inputs by name.\n *\n * 3. Any change handling related to input value modifications through this\n * logic gets connected to any such handling on the VLE side.\n *\n *\n * This script is intenttionally ordered so that the VLE specific bits should\n * be at the top.\n *\n *\n * This script assumes the following:\n *\n * 1. Each relevant IFRAME has an `id`-attribute that will be told to this\n * script.\n *\n * 2. Each such IFRAME exists within the question itself, so that one can\n * traverse up the DOM tree from that IFRAME to find the border of\n * the question.\n *\n * @module qtype_stack/stackjsvle\n * @copyright 2023 Aalto University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\"qtype_stack/stackjsvle\", [\"core/event\"], function(CustomEvents) {\n \"use strict\";\n // Note the VLE specific include of logic.\n\n /* All the IFRAMES have unique identifiers that they give in their\n * messages. But we only work with those that have been created by\n * our logic and are found from this map.\n */\n let IFRAMES = {};\n\n /* For event handling, lists of IFRAMES listening particular inputs.\n */\n let INPUTS = {};\n\n /* For event handling, lists of IFRAMES listening particular inputs\n * and their input events. By default we only listen to changes.\n * We report input events as changes to the other side.\n */\n let INPUTS_INPUT_EVENT = {};\n\n /* A flag to disable certain things. */\n let DISABLE_CHANGES = false;\n\n\n /**\n * Returns an element with a given id, if an only if that element exists\n * inside a portion of DOM that represents a question.\n *\n * If not found or exists outside the restricted area then returns `null`.\n *\n * @param {String} id the identifier of the element we want.\n */\n function vle_get_element(id) {\n /* In the case of Moodle we are happy as long as the element is inside\n something with the `formulation`-class. */\n let candidate = document.getElementById(id);\n let iter = candidate;\n while (iter && !iter.classList.contains('formulation')) {\n iter = iter.parentElement;\n }\n if (iter && iter.classList.contains('formulation')) {\n return candidate;\n }\n\n return null;\n }\n\n /**\n * Returns an input element with a given name, if an only if that element\n * exists inside a portion of DOM that represents a question.\n *\n * Note that, the input element may have a name that multiple questions\n * use and to pick the preferred element one needs to pick the one\n * within the same question as the IFRAME.\n *\n * Note that the input can also be a select. In the case of radio buttons\n * returning one of the possible buttons is enough.\n *\n * If not found or exists outside the restricted area then returns `null`.\n *\n * @param {String} name the name of the input we want\n * @param {String} srciframe the identifier of the iframe wanting it\n */\n function vle_get_input_element(name, srciframe) {\n /* In the case of Moodle we are happy as long as the element is inside\n something with the `formulation`-class. */\n let initialcandidate = document.getElementById(srciframe);\n let iter = initialcandidate;\n while (iter && !iter.classList.contains('formulation')) {\n iter = iter.parentElement;\n }\n if (iter && iter.classList.contains('formulation')) {\n // iter now represents the borders of the question containing\n // this IFRAME.\n let possible = iter.querySelector('input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n // Radios have interesting ids, but the name makes sense\n possible = iter.querySelector('input[id$=\"_' + name + '_1\"][type=radio]');\n if (possible !== null) {\n return possible;\n }\n possible = iter.querySelector('select[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n }\n // If none found within the question itself, search everywhere.\n let possible = document.querySelector('.formulation input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n // Radios have interesting ids, but the name makes sense\n possible = document.querySelector('.formulation input[id$=\"_' + name + '_1\"][type=radio]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.formulation select[id$=\"_' + name + '\"]');\n return possible;\n }\n\n /**\n * Triggers any VLE specific scripting related to updates of the given\n * input element.\n *\n * @param {HTMLElement} inputelement the input element that has changed\n */\n function vle_update_input(inputelement) {\n // Triggering a change event may be necessary.\n const c = new Event('change');\n inputelement.dispatchEvent(c);\n // Also there are those that listen to input events.\n const i = new Event('input');\n inputelement.dispatchEvent(i);\n }\n\n /**\n * Triggers any VLE specific scripting related to DOM updates.\n *\n * @param {HTMLElement} modifiedsubtreerootelement element under which changes may have happened.\n */\n function vle_update_dom(modifiedsubtreerootelement) {\n CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement);\n }\n\n /**\n * Does HTML-string cleaning, i.e., removes any script payload. Returns\n * a DOM version of the given input string.\n *\n * This is used when receiving replacement content for a div.\n *\n * @param {String} src a raw string to sanitise\n */\n function vle_html_sanitize(src) {\n // This can be implemented with many libraries or by custom code\n // however as this is typically a thing that a VLE might already have\n // tools for we have it at this level so that the VLE can use its own\n // tools that do things that the VLE developpers consider safe.\n\n // As Moodle does not currently seem to have such a sanitizer in\n // the core libraries, here is one implementation that shows what we\n // are looking for.\n\n // TODO: look into replacing this with DOMPurify or some such.\n\n let parser = new DOMParser();\n let doc = parser.parseFromString(src);\n\n // First remove all