diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index f3c0f32428f..53ca96f0d2b 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -164,7 +164,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/adminui/index.php b/adminui/index.php index c8f2c44d0bb..d95d8fb1b10 100644 --- a/adminui/index.php +++ b/adminui/index.php @@ -43,6 +43,8 @@ array('link' => (string) new moodle_url('/question/type/stack/adminui/studentinputs.php'))), get_string('bulktestindexintro_desc', 'qtype_stack', array('link' => (string) new moodle_url('/question/type/stack/adminui/bulktestindex.php'))), + get_string('dependenciesintro_desc', 'qtype_stack', + array('link' => (string) new moodle_url('/question/type/stack/adminui/dependencies.php'))), get_string('stackInstall_replace_dollars_desc', 'qtype_stack', array('link' => (string) new moodle_url('/question/type/stack/adminui/replacedollarsindex.php'))), ); diff --git a/amd/build/input.min.js b/amd/build/input.min.js index 9c8699bf7b6..34aa4116ff7 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 JSON.stringify(values);\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').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","this","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","JSON","stringify","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 a35f639710c..e632a33d9d9 100644 --- a/amd/build/stackjsvle.min.js +++ b/amd/build/stackjsvle.min.js @@ -31,6 +31,6 @@ * @copyright 2023 Aalto University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("qtype_stack/stackjsvle",["core/event"],(function(CustomEvents){let IFRAMES={},INPUTS={},INPUTS_INPUT_EVENT={},DISABLE_CHANGES=!1;function vle_get_element(id){let candidate=document.getElementById(id),iter=candidate;for(;iter&&!iter.classList.contains("formulation");)iter=iter.parentElement;return iter&&iter.classList.contains("formulation")?candidate:null}function vle_get_input_element(name,srciframe){let iter=document.getElementById(srciframe);for(;iter&&!iter.classList.contains("formulation");)iter=iter.parentElement;if(iter&&iter.classList.contains("formulation")){let possible=iter.querySelector('input[id$="_'+name+'"]');if(null!==possible)return possible;if(possible=iter.querySelector('input[id$="_'+name+'_1"][type=radio]'),null!==possible)return possible;if(possible=iter.querySelector('select[id$="_'+name+'"]'),null!==possible)return possible}let possible=document.querySelector('.formulation input[id$="_'+name+'"]');return null!==possible?possible:(possible=document.querySelector('.formulation input[id$="_'+name+'_1"][type=radio]'),null!==possible||(possible=document.querySelector('.formulation select[id$="_'+name+'"]')),possible)}function vle_update_dom(modifiedsubtreerootelement){CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement)}function is_evil_attribute(name,value){const lcname=name.toLowerCase();if(lcname.startsWith("on"))return!0;if("src"===lcname||lcname.endsWith("href")){const lcvalue=value.replace(/\s+/g,"").toLowerCase();if(lcvalue.includes("javascript:")||lcvalue.includes("data:text"))return!0}return!1}return window.addEventListener("message",(e=>{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,"text/html");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,evil){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",evil||(frm.sandbox="allow-scripts allow-downloads"),frm.srcdoc=content,document.getElementById(targetdivid).replaceChildren(frm),IFRAMES[iframeid]=frm}}})); +define("qtype_stack/stackjsvle",["core/event"],(function(CustomEvents){let IFRAMES={},INPUTS={},INPUTS_INPUT_EVENT={},DISABLE_CHANGES=!1;function vle_get_element(id){let candidate=document.getElementById(id),iter=candidate;for(;iter&&!iter.classList.contains("formulation");)iter=iter.parentElement;return iter&&iter.classList.contains("formulation")?candidate:null}function vle_get_input_element(name,srciframe){let iter=document.getElementById(srciframe);for(;iter&&!iter.classList.contains("formulation");)iter=iter.parentElement;if(iter&&iter.classList.contains("formulation")){let possible=iter.querySelector('input[id$="_'+name+'"]');if(null!==possible)return possible;if(possible=iter.querySelector('input[id$="_'+name+'_1"][type=radio]'),null!==possible)return possible;if(possible=iter.querySelector('select[id$="_'+name+'"]'),null!==possible)return possible}let possible=document.querySelector('.formulation input[id$="_'+name+'"]');return null!==possible?possible:(possible=document.querySelector('.formulation input[id$="_'+name+'_1"][type=radio]'),null!==possible||(possible=document.querySelector('.formulation select[id$="_'+name+'"]')),possible)}function vle_update_dom(modifiedsubtreerootelement){CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement)}function is_evil_attribute(name,value){const lcname=name.toLowerCase();if(lcname.startsWith("on"))return!0;if("src"===lcname||lcname.endsWith("href")){const lcvalue=value.replace(/\s+/g,"").toLowerCase();if(lcvalue.includes("javascript:")||lcvalue.includes("data:text"))return!0}return!1}return window.addEventListener("message",(e=>{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.1.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)return response.type="error",response.msg='Failed to find element: "'+msg.target+'"',response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");element.replaceChildren(function(src){let doc=(new DOMParser).parseFromString(src,"text/html");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"get-content":element=vle_get_element(msg.target),response.type="xfer-content",response.tgt=msg.src,response.target=msg.target,response.content=null,null!==element&&(response.content=element.innerHTML),IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");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,evil){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",evil||(frm.sandbox="allow-scripts allow-downloads"),frm.srcdoc=content,document.getElementById(targetdivid).replaceChildren(frm),IFRAMES[iframeid]=frm}}})); //# 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 eeea39db2ea..e6bfdebd42b 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, \"text/html\");\n\n // First remove all