")}))):loadCustomisationFields(),set_testtype_visibilities(),useace.prop("checked")&&(setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")),loadUiParametersDescription(),customise.on("change",(function(){customise.prop("checked")?setCustomisationVisibility(!0):str.get_string("confirm_proceed","qtype_coderunner").then((function(s){window.confirm(s)?setCustomisationVisibility(!1):customise.prop("checked",!0)}))})),acelang.on("change",check_ace_lang),language.on("change",(function(){useace.prop("checked")&&setUi("id_template","ace"),check_ace_lang()})),typeCombo.on("change",(function(){customise.prop("checked")?str.get_string("question_type_changed","qtype_coderunner").then((function(s){window.confirm(s)&&loadCustomisationFields()})):loadCustomisationFields()})),useace.on("change",(function(){useace.prop("checked")?(setUi("id_template","ace"),setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")):(setUi("id_template",""),setUi("id_templateparams",""),setUi("id_uiparameters",""))})),evaluatePerStudent.on("change",(function(){evaluatePerStudent.is(":checked")&&langStringAlert("templateparamsusingsandbox")})),uiplugin.on("change",(function(){setUis(),loadUiParametersDescription()})),precheck.on("change",set_testtype_visibilities),new MutationObserver((function(){setUis()})).observe(preloadHdr.get(0),{attributes:!0}),$("button.replaceexpectedwithgot").click((function(){var gotPre=$(this).prev('pre[id^="id_got_"]'),testCaseId=gotPre.attr("id").replace("id_got_","");$("#id_expected_"+testCaseId).val(gotPre.text()),$("#id_fail_expected_"+testCaseId).html(gotPre.text()),$(".failrow_"+testCaseId).addClass("fixed"),$(this).prop("disabled",!0)}))}}}));
//# sourceMappingURL=authorform.min.js.map
\ No newline at end of file
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
index 1e193a847..221c8419f 100644
--- a/amd/build/authorform.min.js.map
+++ b/amd/build/authorform.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n typeName = $('#id_typename'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n */\n function setUis() {\n var uiname = uiplugin.val();\n var enableUi = true;\n if (uiname === 'html' && uiparameters.val().trim() !== '') {\n try {\n var uiparams = JSON.parse(uiparameters.val());\n if (uiparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n typeName.prop('value', newType);\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message to be reported.\n */\n function reportError(questionType, error) {\n langStringAlert('prototype_load_failure', error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n var errorMessage = s + \"\\n\";\n errorMessage += error + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n */\n function loadCustomisationFields() {\n var newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n }\n else {\n reportError(newType, outcome.error);\n }\n\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n var newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n */\n function checkForBrokenQuestion() {\n var brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n typeCombo.prop('disabled', true);\n customise.prop('disabled', true);\n }\n\n checkForBrokenQuestion();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n var isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","messagePara","typeCombo","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","typeName","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","enableUi","trim","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","questionType","success","errorMessage","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","empty","append","header","uiparamstable","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n typeName = $('#id_typename'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n */\n function setUis() {\n var uiname = uiplugin.val();\n var enableUi = true;\n if (uiname === 'html' && uiparameters.val().trim() !== '') {\n try {\n var uiparams = JSON.parse(uiparameters.val());\n if (uiparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n typeName.prop('value', newType);\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as a langstring to be reported.\n */\n function reportError(questionType, error) {\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(error, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clear old broken question fields.\n brokenQuestion.prop('value', '');\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n }\n else {\n reportError(newType, outcome.error);\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n var newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n typeCombo.prop('disabled', true);\n customise.prop('disabled', true);\n }\n\n checkForBrokenQuestion();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n var isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","typeName","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","enableUi","trim","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","questionType","success","errorMessage","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","empty","append","header","uiparamstable","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":";;;;;;;AAuBAA,qCAAO,CAAC,SAAU,wCAAyC,aAAa,SAASC,EAAGC,GAAIC,SAShFC,iBAAmB,CACnBC,SAAqB,CAAC,eAAgB,QAAS,IAC/CC,qBAAqB,CAAC,2BAA4B,UAAW,GACrC,SAAUC,aACW,MAAVA,QAEnCC,iBAAqB,CAAC,uBAAwB,QAAS,IACvDC,WAAqB,CAAC,iBAAkB,QAAS,IACjDC,QAAqB,CAAC,cAAe,QAAS,WAC9CC,cAAqB,CAAC,oBAAqB,QAAS,IACpDC,eAAqB,CAAC,qBAAsB,QAAS,GAC7B,SAAUC,iBACCA,SAASC,QAAQ,KAAM,SAE1DC,oBAAqB,CAAC,0BAA2B,UAAW,GACpC,SAAUR,aACW,MAAVA,QAEnCS,OAAqB,CAAC,aAAc,QAAS,kBAC7CC,cAAqB,CAAC,oBAAqB,QAAS,IACpDC,SAAqB,CAAC,eAAgB,QAAS,IAC/CC,QAAqB,CAAC,cAAe,QAAS,IAC9CC,SAAqB,CAAC,eAAgB,QAAS,cA2iB5C,CAACC,4BAjiBAC,UAAYrB,EAAE,sBACdI,SAAWJ,EAAE,gBACbsB,mBAAqBtB,EAAE,gCACvBuB,YAAcvB,EAAE,mBAChBwB,eAAiBxB,EAAE,sBACnByB,OAASzB,EAAE,cACXiB,SAAWjB,EAAE,gBACbkB,QAAUlB,EAAE,eACZ0B,UAAY1B,EAAE,iBACd2B,aAAe3B,EAAE,4BACjB4B,eAAiB5B,EAAE,sBACnB6B,oBAAsB7B,EAAE,2BACxB8B,sBAAwB9B,EAAE,2BAC1B+B,sBAAwB/B,EAAE,mCAC1BgC,aAAeN,UAAUO,KAAK,WAC9BC,cAAgBlC,EAAE,qBAClBmC,WAAanC,EAAE,wBACfoC,SAAWpC,EAAE,gBACbqC,SAAWrC,EAAE,0BAA0BiC,KAAK,SAC5CK,oBAAsBtC,EAAE,eACxBuC,SAAWvC,EAAE,sBACbwC,aAAexC,EAAE,gBACjByC,eAAiBzC,EAAE,uBACnBmB,SAAWnB,EAAE,gBACb0C,aAAe1C,EAAE,6BAWZ2C,MAAMC,KAAMC,YAEbC,KAIAC,UALAC,GAAKhD,EAAEiD,SAASC,eAAeN,OAE/BO,YAAcH,GAAGI,KAAK,aACtBC,WAAaL,GAAGI,KAAK,eACrBE,OAAS,GAKbN,GAAGI,KAAK,sBAAuB5B,eAAe+B,OAC9CP,GAAGI,KAAK,mBAAoB7B,YAAYgC,OACxCP,GAAGI,KAAK,aAAcpD,EAAE,kBAAkBuD,WAEtCD,OAASE,KAAKC,MAAMJ,YACtB,MAAMK,MAEO,UADfb,OAASA,OAAOc,iBAEZd,OAAS,IAGD,qBAARD,MAAuC,mBAARA,KAC/BE,KAAO,IAEPA,KAAO7B,SAASgB,KAAK,SACR,gBAATW,MAA0B1B,QAAQe,KAAK,WACvCa,cAiMc5B,aAClB0C,MAAOC,KACP3C,QAAQ4C,QAAQ,KAAO,SAChB5C,YAEP0C,MAAQ1C,QAAQ6C,MAAM,KACjBF,EAAI,EAAGA,EAAID,MAAMI,OAAQH,OACtBD,MAAMC,GAAGI,SAAS,YACXL,MAAMC,GAAGK,OAAO,EAAGN,MAAMC,GAAGG,OAAS,UAG7CJ,MAAMI,OAAS,EAAIJ,MAAM,GAAK,GA5M1BO,CAAiBjD,QAAQe,KAAK,aAI7Cc,UAAYC,GAAGoB,KAAK,wBAEHrB,UAAUF,SAAWA,QAAUM,aAAeL,OAI/DE,GAAGI,KAAK,YAAaN,MAEhBC,WAIDO,OAAOR,KAAOA,KACdC,UAAUsB,OAAOxB,OAAQS,SAJzBP,UAAY,IAAI9C,GAAGqE,iBAAiBzB,OAAQD,gBAe3C2B,aACD1B,OAAS1B,SAASoC,MAClBiB,UAAW,KACA,SAAX3B,QAAmD,KAA9BH,aAAaa,MAAMkB,YAGF,IADnBjB,KAAKC,MAAMf,aAAaa,OAC1BmB,mBACTF,UAAW,GAEjB,MAAOG,OACLC,MAAM,0BAGVJ,WACA7B,MAAM,YAAaE,QACnBF,MAAM,mBAAoBE,kBAQzBgC,2BAA2BC,eAC5BC,QAAUD,UAAY,QAAU,OACpChD,sBAAsBkD,IAAI,UAAWD,SACrChD,sBAAsBiD,IAAI,UAAWD,SACjCD,WAAarD,OAAOQ,KAAK,YACzBU,MAAM,cAAe,gBA+CpBsC,2BAA2BC,QAASC,cACrCC,cAAeC,QAZfC,wBAeC,IAAIC,gBAxCwBC,aAE7BzC,UADA0C,MAAQ,CAAC,cAAe,sBAExBhE,OAAOQ,KAAK,eACR,IAAI4B,EAAI,EAAGA,EAAI4B,MAAMzB,OAAQH,KAE7Bd,UADK/C,EAAEiD,SAASC,eAAeuC,MAAM5B,KACtBO,KAAK,wBACHoB,QACbzC,UAAU2C,UACH3C,YAAcyC,SACrBzC,UAAU4C,OA6BtBC,EAA4B,GACZzF,iBACZiF,cAAgBjF,iBAAiBoF,KACjCF,QAAUF,SAASI,KAAOJ,SAASI,KAAOH,cAAc,GACpDA,cAAcpB,OAAS,IAEvBqB,SADAQ,EAAST,cAAc,IACNC,UAErBrF,EAAEoF,cAAc,IAAInD,KAAKmD,cAAc,GAAIC,SAG/CjD,SAASH,KAAK,QAASiD,SACvBxD,UAAUO,KAAK,WAAW,GAC1B/B,IAAI4F,WAAW,2BAA4B,oBAAoBC,MAAK,SAAUC,OAsC7DC,MAAOC,iBAAkBC,KAEtCC,WAvCA9D,oBAAoB6D,MAqCPF,MArCwBf,QAqCjBgB,iBArC0BF,EAqCRG,KArCWhB,SAASkB,aAuC1DD,WAAa,2CACjBA,YAAcF,iBACdE,YAAcH,MAAQ,SAAWE,UAtCjCtB,4BAA2B,GA/BvBS,oBAAsB3D,aAAaM,KAAK,WAE5CL,eAAeK,KAAK,YAAaqD,qBACjCzD,oBAAoBI,KAAK,YAAaqD,8BA6EjCgB,gBAAgBf,IAAKgB,OACtBC,OAAOC,eAAe,iBAAmBD,OAAOE,cAGpDxG,IAAI4F,WAAWP,IAAK,oBAAoBQ,MAAK,SAASC,OAC9CW,QAAUX,EAAEnF,QAAQ,MAAO,KAC3B0F,QACAI,SAAW,KAAOJ,OAEtB3B,MAAM+B,qBA+BLC,8BACD1B,QAAU7D,UAAUwF,SAAS,mBAAmBC,OAEpC,KAAZ5B,SAA8B,cAAZA,UAElB7D,UAAUwF,SAAS,sBAAsB5E,KAAK,WAAY,YAG1DjC,EAAE+G,QAAQC,EAAEC,IAAIC,QAAU,qCACtB,CACIC,MAAOjC,QACPkC,SAAU/E,SACVgF,QAASL,EAAEC,IAAII,UAEnB,SAAUC,aA3FDC,aAAc5C,MA6FnBlC,eAAeR,KAAK,QAAS,IACzBqF,QAAQE,SACRvC,2BAA2BC,QAASoC,SACpC/C,WAhGCgD,aAmGWrC,QAnGGP,MAmGM2C,QAAQ3C,MAlG7CzE,IAAI4F,WAAW,kBAAmB,oBAAoBC,MAAK,SAASC,GAChE9F,IAAI4F,WAAWnB,MAAO,mBAAoB4C,cAAcxB,MAAK,SAAS7F,KAClEoG,gBAAgB,yBAA0BpG,SACtCuH,aAAezB,EAAI,KACvByB,cAAgBvH,IAAM,KACtBuH,cAAgB,aAAepF,SAAW,YAAckF,aACxDnH,SAAS6B,KAAK,QAASwF,wBA+FzBC,MAAK,WAIHpB,gBAAgB,2BAChBlG,SAAS6B,KAAK,QAAS,wCACvB/B,IAAI4F,WAAW,aAAc,oBAAoBC,MAAK,SAASC,GAC3D5F,SAAS6B,KAAK,QAAS+D,mBA4B9B2B,kCACDC,MAAQzG,SAAS0F,SAAS,mBAAmBC,OACjD9G,EAAE+G,QAAQC,EAAEC,IAAIC,QAAU,qCACtB,CACI/F,SAAUyG,MACVR,SAAU/E,SACVgF,QAASL,EAAEC,IAAII,UAEnB,SAAUQ,YAIFC,MAHAC,oBAAsBrF,aAAaa,MACnCyE,oBAAsBhI,EAAE,wBACxBiI,eAAiBjI,EAAE,iDAAmD6H,OAAOK,YAAc,aAE/FF,oBAAoBG,QACpBH,oBAAoBI,OAAOP,OAAOQ,QACC,GAA/BR,OAAOS,cAActE,QAA8C,KAA/B+D,oBAAoBtD,QACxD/B,aAAaa,IAAI,IACjBvD,EAAE,+BAA+BuI,SAEE,GAA/BV,OAAOS,cAActE,SACrBgE,oBAAoBI,OAAOH,gBAC3BH,MAAQ9H,WArCSwI,iBAEKC,MAAO5E,EADzCsC,KAAO,8DACPuC,KAAOF,YAAYG,kBACvBxC,MAAQ,WAAauC,KAAK,GAAK,YAAcA,KAAK,GAAK,YAAcA,KAAK,GAAK,eAC1E7E,EAAI,EAAGA,EAAI2E,YAAYF,cAActE,OAAQH,IAE9CsC,MAAQ,YADRsC,MAAQD,YAAYF,cAAczE,IACP,GAAK,YAAc4E,MAAM,GAAK,YAAcA,MAAM,GAAK,sBAEtFtC,KAAQ,mBA6BkByC,CAA4Bf,SACtCG,oBAAoBI,OAAON,OAC3BA,MAAMS,OACNN,eAAeY,OAAM,WACbZ,eAAe9B,QAAU0B,OAAOK,aAChCJ,MAAMgB,OACNb,eAAe9B,KAAK0B,OAAOkB,eAE3BjB,MAAMS,OACNN,eAAe9B,KAAK0B,OAAOK,kBAIvClI,EAAE,+BAA+B8I,OAC7BrH,OAAOQ,KAAK,YACZU,MAAM,kBAAmB,WAIvC+E,MAAK,WAEHpB,gBAAgB,sCAQf0C,4BACkB,MAAnBzG,SAASgB,MACTf,aAAasG,OAEbtG,aAAa+F,gBAQZU,iBACkB,QAAnB9H,SAASoC,OACTgB,SAiC2B,GAA/BrC,cAAcD,KAAK,WAEnB/B,IAAI4F,WAAW,sBAAuB,oBAAoBC,MAAK,SAASC,GACpEpB,MAAMoB,MAEV9D,cAAcD,KAAK,YAAY,GAC/BZ,UAAUY,KAAK,YAAY,GAC3BP,UAAUO,KAAK,YAAY,mBApBvBiH,YAAc,KACY,KAFFzG,eAAeR,KAAK,WAG5CiH,YAAclJ,EAAE,MAAQyC,eAAeR,KAAK,SAAW,QACvDjC,EAAE,kCAAkCoI,OAAOc,cAoBnDC,GAEAtE,2BAA2B7C,cACtBA,cAIDuC,SACArE,IAAI4F,WAAW,mBAAoB,oBAAoBC,MAAK,SAASC,GACjE1D,oBAAoB6D,KAAK,MAAQH,EAAI,YAJzCY,0BAQJoC,4BAEIvH,OAAOQ,KAAK,aACZU,MAAM,oBAAqB,OAC3BA,MAAM,kBAAmB,QAG7BgF,8BAIAjG,UAAU0H,GAAG,UAAU,WACA1H,UAAUO,KAAK,WAG9B4C,4BAA2B,GAE3B3E,IAAI4F,WAAW,kBAAmB,oBAAoBC,MAAK,SAASC,GAC5DQ,OAAO6C,QAAQrD,GACfnB,4BAA2B,GAE3BnD,UAAUO,KAAK,WAAW,SAM1Cf,QAAQkI,GAAG,SAAUH,gBACrBhI,SAASmI,GAAG,UAAU,WA3Ed3H,OAAOQ,KAAK,YACZU,MAAM,cAAe,OA4EzBsG,oBAGJ5H,UAAU+H,GAAG,UAAU,WACf1H,UAAUO,KAAK,WAEf/B,IAAI4F,WAAW,wBAAyB,oBAAoBC,MAAK,SAAUC,GACnEQ,OAAO6C,QAAQrD,IACfY,6BAIRA,6BAIRnF,OAAO2H,GAAG,UAAU,WACE3H,OAAOQ,KAAK,YAE1BU,MAAM,cAAe,OACrBA,MAAM,oBAAqB,OAC3BA,MAAM,kBAAmB,SAEzBA,MAAM,cAAe,IACrBA,MAAM,oBAAqB,IAC3BA,MAAM,kBAAmB,QAIjCrB,mBAAmB8H,GAAG,UAAU,WACxB9H,mBAAmBgI,GAAG,aACtBhD,gBAAgB,iCAIxBnF,SAASiI,GAAG,UAAU,WAClB7E,SACAoD,iCAGJpF,SAAS6G,GAAG,SAAUJ,2BAKP,IAAIO,kBAAkB,WACjChF,YAEKiF,QAAQrH,WAAWsH,IAAI,GAAI,aAAe,IAInDzJ,EAAE,iCAAiC6I,OAAM,eACjCa,OAAS1J,EAAE2J,MAAMC,KAAK,sBACtBC,WAAaH,OAAOtG,KAAK,MAAMvC,QAAQ,UAAW,IACtDb,EAAE,gBAAkB6J,YAAYtG,IAAImG,OAAO5C,QAC3C9G,EAAE,qBAAuB6J,YAAY1D,KAAKuD,OAAO5C,QACjD9G,EAAE,YAAc6J,YAAYC,SAAS,SACrC9J,EAAE2J,MAAM1H,KAAK,YAAY"}
\ No newline at end of file
diff --git a/amd/src/authorform.js b/amd/src/authorform.js
index 4b91ab125..10cf66469 100644
--- a/amd/src/authorform.js
+++ b/amd/src/authorform.js
@@ -256,15 +256,17 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
* the template contents with an error message in case the user
* saves the question and later wonders why it breaks.
* @param {string} questionType The CodeRunner (sub) question type.
- * @param {string} error The error message to be reported.
+ * @param {string} error The error message as a langstring to be reported.
*/
function reportError(questionType, error) {
- langStringAlert('prototype_load_failure', error);
str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {
- var errorMessage = s + "\n";
- errorMessage += error + '\n';
- errorMessage += "CourseId: " + courseId + ", qtype: " + questionType;
- template.prop('value', errorMessage);
+ str.get_string(error, 'qtype_coderunner', questionType).then(function(str) {
+ langStringAlert('prototype_load_failure', str);
+ let errorMessage = s + "\n";
+ errorMessage += str + '\n';
+ errorMessage += "CourseId: " + courseId + ", qtype: " + questionType;
+ template.prop('value', errorMessage);
+ });
});
}
@@ -334,7 +336,7 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
* CodeRunner question type currently selected by the combobox.
*/
function loadCustomisationFields() {
- var newType = typeCombo.children('option:selected').text();
+ let newType = typeCombo.children('option:selected').text();
if (newType !== '' && newType !== 'Undefined') {
// Prevent 'Undefined' ever being reselected.
@@ -348,6 +350,8 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
sesskey: M.cfg.sesskey
},
function (outcome) {
+ // Clear old broken question fields.
+ brokenQuestion.prop('value', '');
if (outcome.success) {
copyFieldsFromQuestionType(newType, outcome);
setUis();
@@ -355,7 +359,6 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
else {
reportError(newType, outcome.error);
}
-
}
).fail(function () {
// AJAX failed. We're dead, Fred. The attempt to get the
@@ -475,7 +478,7 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
* given message as an error at the top of the question.
*/
function checkForBrokenQuestion() {
- var brokenQuestionMessage = brokenQuestion.prop('value'),
+ let brokenQuestionMessage = brokenQuestion.prop('value'),
messagePara = null;
if (brokenQuestionMessage !== '') {
messagePara = $('
' + brokenQuestion.prop('value') + '
');
diff --git a/edit_coderunner_form.php b/edit_coderunner_form.php
index b19d6e6e1..842eb5b51 100644
--- a/edit_coderunner_form.php
+++ b/edit_coderunner_form.php
@@ -355,18 +355,10 @@ public function data_preprocessing($question) {
// calling question_bank::loadquestion($question->id).
global $COURSE;
- if (!isset($question->brokenquestionmessage)) {
- $question->brokenquestionmessage = '';
- }
if (isset($question->options->testcases)) { // Reloading a saved question?
-
- // Firstly check if we're editing a question with a missing prototype.
- // Set the broken_question message if so.
$q = $this->make_question_from_form_data($question);
- if ($q->prototype === null) {
- $question->brokenquestionmessage = get_string(
- 'missingprototype', 'qtype_coderunner', array('crtype' => $question->coderunnertype));
- }
+ // Loads the error messages into the brokenquestionmessage.
+ $question->brokenquestionmessage = $this->load_error_messages($question, $q);
// Record the prototype for subsequent use.
$question->prototype = $q->prototype;
@@ -459,7 +451,33 @@ private function newline_hack($s) {
return "\n" . $s;
}
-
+ /**
+ * Loads error messages to be put into brokenquestionmessage of the question if needed.
+ * Returns a string of the message to be inserted.
+ *
+ * @param type $question Object with all the question data within.
+ * @param type $q Object with the new question data within.
+ */
+ private function load_error_messages($question, $q) {
+ $errorstring = "";
+ // Firstly check if we're editing a question with a missing prototype or duplicates.
+ // Set the broken_question message if so.
+ if ($q->prototype === null) {
+ $errorstring = get_string(
+ 'missingprototype', 'qtype_coderunner', array('crtype' => $question->coderunnertype));
+ } else if (is_array($q->prototype)) {
+ $outputstring = "
";
+ // Output every duplicate Question id, name and category.
+ foreach ($q->prototype as $component) {
+ $outputstring .= "Question ID: {$component->id}
Name: {$component->name}
"
+ . "
Category: {$component->category}
";
+ }
+ $errorstring = get_string(
+ 'duplicateprototype', 'qtype_coderunner', ['crtype' => $question->coderunnertype,
+ 'outputstring' => $outputstring]);
+ }
+ return $errorstring;
+ }
// FUNCTIONS TO BUILD PARTS OF THE MAIN FORM
@@ -812,9 +830,11 @@ private function make_advanced_customisation_panel($mform) {
public function validation($data, $files) {
$errors = parent::validation($data, $files);
$this->formquestion = $this->make_question_from_form_data($data);
- if ($data['coderunnertype'] == 'Undefined') {
+ $this->formquestion->brokenquestionmessage = $this->load_error_messages($data, $this->formquestion);
+ if ($data['coderunnertype'] == 'Undefined' || $this->formquestion->brokenquestionmessage !== '') {
$errors['coderunner_type_group'] = get_string('questiontype_required', 'qtype_coderunner');
- return $errors; // Don't continue checking in this case. Template param validation breaks.
+ return $errors; // Don't continue checking in this case, including missing or extra prototypes.
+ // Else template param validation breaks.
}
if ($data['cputimelimitsecs'] != '' &&
(!ctype_digit($data['cputimelimitsecs']) || intval($data['cputimelimitsecs']) <= 0)) {
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index d803a777d..dc720b9ff 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -136,6 +136,7 @@
after courses are the number of quizzes in the course with at least
one submission. The numbers in parentheses after the quiz name
are the numbers of submissions.';
+$string['duplicateprototype'] = 'This question was defined to be of type \'{$a->crtype}\' but the prototype is non-unique in the following questions: {$a->outputstring}';
$string['editingcoderunner'] = 'Editing a CodeRunner Question';
$string['empty_new_prototype_name'] = 'New question type name cannot be empty';
$string['emptypenaltyregime'] = 'Penalty regime must be defined (since version 3.1)';
@@ -412,7 +413,7 @@
$string['missinganswers'] = 'missing answers';
$string['missingorbadfraction'] = 'Bad or missing fraction in output from template grader. Output was: {$a->output}';
$string['missingoutput'] = 'You must supply the expected output from this test case.';
-$string['missingprototype'] = 'This question was defined to be of type \'{$a->crtype}\' but the prototype does not exist, or is non-unique, or is unavailable in this context. You should Cancel and try to (re)install the prototype.
+$string['missingprototype'] = 'This question was defined to be of type \'{$a->crtype}\' but the prototype does not exist, or is unavailable in this context. You should Cancel and try to (re)install the prototype.
Proceed to edit only if you know what you are doing!';
$string['missingprototypes'] = 'Missing prototypes';
$string['missingprototypewhenrunning'] = 'Broken question (missing or duplicate prototype \'{$a->crtype}\'). Cannot be run.';
@@ -507,6 +508,8 @@
to make subsequentmaintenance easier.';
$string['prototype_error'] = '*** PROTOTYPE LOAD FAILURE. DON\'T SAVE THIS! ***';
$string['prototype_load_failure'] = 'Error loading prototype: ';
+$string['prototype_missing_alert'] = 'Missing prototype: Check if {$a} prototype exists in this context.';
+$string['prototype_duplicate_alert'] = 'Duplicate prototype: Duplicate {$a} prototypes exist. Can only load one.';
$string['prototypeQ'] = 'Is prototype?';
$string['qtype_c_function'] = '
A question type for C write-a-function questions.
diff --git a/question.php b/question.php
index e54c64bbd..80d5fcdff 100644
--- a/question.php
+++ b/question.php
@@ -159,8 +159,8 @@ public function evaluate_merged_parameters($seed, $step=null) {
assert(isset($this->templateparams));
$paramsjson = $this->template_params_json($seed, $step, '_template_params');
$prototype = $this->prototype;
- if ($prototype !== null && $this->prototypetype == 0) {
- // Merge with prototype parameters (unless this is a prototype or prototype is missing).
+ if ($prototype !== null && !is_array($prototype) && $this->prototypetype == 0) {
+ // Merge with prototype parameters (unless this is a prototype or prototype is missing/multiple).
$prototype->student = $this->student; // Supply this missing attribute.
$prototypeparamsjson = $prototype->template_params_json($seed, $step, '_prototype__template_params');
$paramsjson = qtype_coderunner_util::merge_json($prototypeparamsjson, $paramsjson);
diff --git a/questiontype.php b/questiontype.php
index 94eec6359..efdbeeecd 100644
--- a/questiontype.php
+++ b/questiontype.php
@@ -414,12 +414,12 @@ public function get_question_options($question) {
* This is used only to display the customisation panel during authoring.
* @param object $target the target object whose fields are being set. It should
* be either a qtype_coderunner_question object or its options field ($question->options).
- * @param string $prototype the prototype question. Null if non-existent (a broken question).
+ * @param string $prototype the prototype question. Null if non-existent or more than one (a broken question).
*/
public function set_inherited_fields($target, $prototype) {
$target->customise = false; // Starting assumption.
- if ($prototype === null) {
+ if ($prototype === null || is_array($prototype)) {
return;
}
@@ -501,7 +501,7 @@ public static function get_prototype($coderunnertype, $context, $checkexistenceo
list($contextcondition, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true));
$params[] = $coderunnertype;
- $sql = "SELECT q.id
+ $sql = "SELECT q.id, q.name, qc.name as category
FROM {question_coderunner_options} qco
JOIN {question} q ON qco.questionid = q.id
JOIN {question_versions} qv ON qv.questionid = q.id
@@ -518,7 +518,7 @@ public static function get_prototype($coderunnertype, $context, $checkexistenceo
$validprotoids = $DB->get_records_sql($sql, $params);
if (count($validprotoids) !== 1) {
- return null; // Exactly one prototype should be found.
+ return count($validprotoids) === 0 ? null : $validprotoids; // If either no or too many prototypes are found.
} else if ($checkexistenceonly) {
return true;
} else {
@@ -840,7 +840,7 @@ public function export_to_xml($question, qformat_xml $format, $extra=null) {
// Clear all inherited fields equal in value to the corresponding Prototype field
// (but only if we found a prototype and this is not a prototype question itself).
- if ($row && $questiontoexport->options->prototypetype == 0) {
+ if ($row && $questiontoexport->options->prototypetype == 0 && is_array($row)) {
$noninheritedfields = $this->noninherited_fields();
$extrafields = $this->extra_question_fields();
foreach ($row as $field => $value) {
From 27395098dd662a0d02255d54517dd24609456d64 Mon Sep 17 00:00:00 2001
From: Michelle Hsieh
Date: Wed, 21 Dec 2022 23:38:26 +1300
Subject: [PATCH 028/188] Done some more major error handling, but incomplete.
---
ajax.php | 15 +++++--
amd/build/authorform.min.js | 2 +-
amd/build/authorform.min.js.map | 2 +-
amd/src/authorform.js | 76 ++++++++++++++++++++++++++-------
edit_coderunner_form.php | 48 ++++++++++++++-------
lang/en/qtype_coderunner.php | 5 ++-
questiontype.php | 8 ++--
styles.css | 13 +++++-
8 files changed, 127 insertions(+), 42 deletions(-)
diff --git a/ajax.php b/ajax.php
index e243c6abe..eb6d9a698 100644
--- a/ajax.php
+++ b/ajax.php
@@ -51,13 +51,22 @@
if ($qtype) {
$questiontype = qtype_coderunner::get_prototype($qtype, $coursecontext);
if ($questiontype === null || is_array($questiontype)) {
+ $questionprototype = $questiontype;
$questiontype = new stdClass();
$questiontype->success = false;
if ($questiontype === null) {
- $questiontype->error = "prototype_missing_alert";
+ $questiontype->error = json_encode(["error" => "missingprototype",
+ "alert" => "prototype_missing_alert", "extras" => ""]);
} else {
- $questiontype->error = "prototype_duplicate_alert";
- }
+ $extras = "";
+ foreach ($questionprototype as $component) {
+ $extras .= get_string('listprototypeduplicates', 'qtype_coderunner',
+ ['id' => $component->id, 'name' => $component->name,
+ 'category' => $component->category]);
+ }
+ $questiontype->error = json_encode(["error" => "duplicateprototype",
+ "alert" => "prototype_duplicate_alert", "extras" => $extras]);
+ }
} else {
$questiontype->success = true;
$questiontype->error = '';
diff --git a/amd/build/authorform.min.js b/amd/build/authorform.min.js
index 04289879e..88fd003f6 100644
--- a/amd/build/authorform.min.js
+++ b/amd/build/authorform.min.js
@@ -5,6 +5,6 @@
* @copyright Richard Lobb, 2015, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/authorform",["jquery","qtype_coderunner/userinterfacewrapper","core/str"],(function($,ui,str){var JSON_TO_FORM_MAP={template:["#id_template","value",""],iscombinatortemplate:["#id_iscombinatortemplate","checked","",function(value){return"1"===value}],cputimelimitsecs:["#id_cputimelimitsecs","value",""],memlimitmb:["#id_memlimitmb","value",""],sandbox:["#id_sandbox","value","DEFAULT"],sandboxparams:["#id_sandboxparams","value",""],testsplitterre:["#id_testsplitterre","value","",function(splitter){return splitter.replace("\n","\\n")}],allowmultiplestdins:["#id_allowmultiplestdins","checked","",function(value){return"1"===value}],grader:["#id_grader","value","EqualityGrader"],resultcolumns:["#id_resultcolumns","value",""],language:["#id_language","value",""],acelang:["#id_acelang","value",""],uiplugin:["#id_uiplugin","value","ace"]};return{initEditForm:function(){var typeCombo=$("#id_coderunnertype"),template=$("#id_template"),evaluatePerStudent=$("#id_templateparamsevalpertry"),globalextra=$("#id_globalextra"),prototypeextra=$("#id_prototypeextra"),useace=$("#id_useace"),language=$("#id_language"),acelang=$("#id_acelang"),customise=$("#id_customise"),isCombinator=$("#id_iscombinatortemplate"),testSplitterRe=$("#id_testsplitterre"),allowMultipleStdins=$("#id_allowmultiplestdins"),customisationFieldSet=$("#id_customisationheader"),advancedCustomisation=$("#id_advancedcustomisationheader"),isCustomised=customise.prop("checked"),prototypeType=$("#id_prototypetype"),preloadHdr=$("#id_answerpreloadhdr"),typeName=$("#id_typename"),courseId=$('input[name="courseid"]').prop("value"),questiontypeHelpDiv=$("#qtype-help"),precheck=$("select#id_precheck"),testtypedivs=$("div.testtype"),brokenQuestion=$("#id_broken_question"),uiplugin=$("#id_uiplugin"),uiparameters=$("#id_uiparameters");function setUi(taId,uiname){var lang,uiWrapper,ta=$(document.getElementById(taId)),currentLang=ta.attr("data-lang"),paramsJson=ta.attr("data-params"),params={};ta.attr("data-prototypeextra",prototypeextra.val()),ta.attr("data-globalextra",globalextra.val()),ta.attr("data-test0",$("#id_testcode_0").val());try{params=JSON.parse(paramsJson)}catch(err){}"none"===(uiname=uiname.toLowerCase())&&(uiname=""),"id_templateparams"==taId||"id_uiparameters"==taId?lang="":(lang=language.prop("value"),"id_template"!==taId&&acelang.prop("value")&&(lang=function(acelang){var langs,i;if(acelang.indexOf(",")<0)return acelang;for(langs=acelang.split(","),i=0;i0?langs[0]:""}(acelang.prop("value")))),(uiWrapper=ta.data("current-ui-wrapper"))&&uiWrapper.uiname===uiname&¤tLang==lang||(ta.attr("data-lang",lang),uiWrapper?(params.lang=lang,uiWrapper.loadUi(uiname,params)):uiWrapper=new ui.InterfaceWrapper(uiname,taId))}function setUis(){var uiname=uiplugin.val(),enableUi=!0;if("html"===uiname&&""!==uiparameters.val().trim())try{!1===JSON.parse(uiparameters.val()).enable_in_editor&&(enableUi=!1)}catch(error){alert("Invalid UI parameters.")}enableUi&&(setUi("id_answer",uiname),setUi("id_answerpreload",uiname))}function setCustomisationVisibility(isVisible){var display=isVisible?"block":"none";customisationFieldSet.css("display",display),advancedCustomisation.css("display",display),isVisible&&useace.prop("checked")&&setUi("id_template","ace")}function copyFieldsFromQuestionType(newType,response){var formspecifier,attrval,isCombinatorEnabled;for(var key in function(stateOn){var uiWrapper,taIds=["id_template","id_uiparameters"];if(useace.prop("checked"))for(var i=0;i3&&(attrval=(0,formspecifier[3])(attrval)),$(formspecifier[0]).prop(formspecifier[1],attrval);typeName.prop("value",newType),customise.prop("checked",!1),str.get_string("coderunner_question_type","qtype_coderunner").then((function(s){var title,coderunner_descr,html,resultHtml;questiontypeHelpDiv.html((title=newType,coderunner_descr=s,html=response.questiontext,resultHtml='
")}))):loadCustomisationFields(),set_testtype_visibilities(),useace.prop("checked")&&(setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")),loadUiParametersDescription(),customise.on("change",(function(){customise.prop("checked")?setCustomisationVisibility(!0):str.get_string("confirm_proceed","qtype_coderunner").then((function(s){window.confirm(s)?setCustomisationVisibility(!1):customise.prop("checked",!0)}))})),acelang.on("change",check_ace_lang),language.on("change",(function(){useace.prop("checked")&&setUi("id_template","ace"),check_ace_lang()})),typeCombo.on("change",(function(){customise.prop("checked")?str.get_string("question_type_changed","qtype_coderunner").then((function(s){window.confirm(s)&&loadCustomisationFields()})):loadCustomisationFields()})),useace.on("change",(function(){useace.prop("checked")?(setUi("id_template","ace"),setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")):(setUi("id_template",""),setUi("id_templateparams",""),setUi("id_uiparameters",""))})),evaluatePerStudent.on("change",(function(){evaluatePerStudent.is(":checked")&&langStringAlert("templateparamsusingsandbox")})),uiplugin.on("change",(function(){setUis(),loadUiParametersDescription()})),precheck.on("change",set_testtype_visibilities),new MutationObserver((function(){setUis()})).observe(preloadHdr.get(0),{attributes:!0}),$("button.replaceexpectedwithgot").click((function(){var gotPre=$(this).prev('pre[id^="id_got_"]'),testCaseId=gotPre.attr("id").replace("id_got_","");$("#id_expected_"+testCaseId).val(gotPre.text()),$("#id_fail_expected_"+testCaseId).html(gotPre.text()),$(".failrow_"+testCaseId).addClass("fixed"),$(this).prop("disabled",!0)}))}}}));
//# sourceMappingURL=authorform.min.js.map
\ No newline at end of file
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
index 221c8419f..709e7dfc0 100644
--- a/amd/build/authorform.min.js.map
+++ b/amd/build/authorform.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n typeName = $('#id_typename'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n */\n function setUis() {\n var uiname = uiplugin.val();\n var enableUi = true;\n if (uiname === 'html' && uiparameters.val().trim() !== '') {\n try {\n var uiparams = JSON.parse(uiparameters.val());\n if (uiparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n typeName.prop('value', newType);\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as a langstring to be reported.\n */\n function reportError(questionType, error) {\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(error, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clear old broken question fields.\n brokenQuestion.prop('value', '');\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n }\n else {\n reportError(newType, outcome.error);\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n var newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n typeCombo.prop('disabled', true);\n customise.prop('disabled', true);\n }\n\n checkForBrokenQuestion();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n var isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","typeName","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","enableUi","trim","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","questionType","success","errorMessage","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","empty","append","header","uiparamstable","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":";;;;;;;AAuBAA,qCAAO,CAAC,SAAU,wCAAyC,aAAa,SAASC,EAAGC,GAAIC,SAShFC,iBAAmB,CACnBC,SAAqB,CAAC,eAAgB,QAAS,IAC/CC,qBAAqB,CAAC,2BAA4B,UAAW,GACrC,SAAUC,aACW,MAAVA,QAEnCC,iBAAqB,CAAC,uBAAwB,QAAS,IACvDC,WAAqB,CAAC,iBAAkB,QAAS,IACjDC,QAAqB,CAAC,cAAe,QAAS,WAC9CC,cAAqB,CAAC,oBAAqB,QAAS,IACpDC,eAAqB,CAAC,qBAAsB,QAAS,GAC7B,SAAUC,iBACCA,SAASC,QAAQ,KAAM,SAE1DC,oBAAqB,CAAC,0BAA2B,UAAW,GACpC,SAAUR,aACW,MAAVA,QAEnCS,OAAqB,CAAC,aAAc,QAAS,kBAC7CC,cAAqB,CAAC,oBAAqB,QAAS,IACpDC,SAAqB,CAAC,eAAgB,QAAS,IAC/CC,QAAqB,CAAC,cAAe,QAAS,IAC9CC,SAAqB,CAAC,eAAgB,QAAS,cA2iB5C,CAACC,4BAjiBAC,UAAYrB,EAAE,sBACdI,SAAWJ,EAAE,gBACbsB,mBAAqBtB,EAAE,gCACvBuB,YAAcvB,EAAE,mBAChBwB,eAAiBxB,EAAE,sBACnByB,OAASzB,EAAE,cACXiB,SAAWjB,EAAE,gBACbkB,QAAUlB,EAAE,eACZ0B,UAAY1B,EAAE,iBACd2B,aAAe3B,EAAE,4BACjB4B,eAAiB5B,EAAE,sBACnB6B,oBAAsB7B,EAAE,2BACxB8B,sBAAwB9B,EAAE,2BAC1B+B,sBAAwB/B,EAAE,mCAC1BgC,aAAeN,UAAUO,KAAK,WAC9BC,cAAgBlC,EAAE,qBAClBmC,WAAanC,EAAE,wBACfoC,SAAWpC,EAAE,gBACbqC,SAAWrC,EAAE,0BAA0BiC,KAAK,SAC5CK,oBAAsBtC,EAAE,eACxBuC,SAAWvC,EAAE,sBACbwC,aAAexC,EAAE,gBACjByC,eAAiBzC,EAAE,uBACnBmB,SAAWnB,EAAE,gBACb0C,aAAe1C,EAAE,6BAWZ2C,MAAMC,KAAMC,YAEbC,KAIAC,UALAC,GAAKhD,EAAEiD,SAASC,eAAeN,OAE/BO,YAAcH,GAAGI,KAAK,aACtBC,WAAaL,GAAGI,KAAK,eACrBE,OAAS,GAKbN,GAAGI,KAAK,sBAAuB5B,eAAe+B,OAC9CP,GAAGI,KAAK,mBAAoB7B,YAAYgC,OACxCP,GAAGI,KAAK,aAAcpD,EAAE,kBAAkBuD,WAEtCD,OAASE,KAAKC,MAAMJ,YACtB,MAAMK,MAEO,UADfb,OAASA,OAAOc,iBAEZd,OAAS,IAGD,qBAARD,MAAuC,mBAARA,KAC/BE,KAAO,IAEPA,KAAO7B,SAASgB,KAAK,SACR,gBAATW,MAA0B1B,QAAQe,KAAK,WACvCa,cAiMc5B,aAClB0C,MAAOC,KACP3C,QAAQ4C,QAAQ,KAAO,SAChB5C,YAEP0C,MAAQ1C,QAAQ6C,MAAM,KACjBF,EAAI,EAAGA,EAAID,MAAMI,OAAQH,OACtBD,MAAMC,GAAGI,SAAS,YACXL,MAAMC,GAAGK,OAAO,EAAGN,MAAMC,GAAGG,OAAS,UAG7CJ,MAAMI,OAAS,EAAIJ,MAAM,GAAK,GA5M1BO,CAAiBjD,QAAQe,KAAK,aAI7Cc,UAAYC,GAAGoB,KAAK,wBAEHrB,UAAUF,SAAWA,QAAUM,aAAeL,OAI/DE,GAAGI,KAAK,YAAaN,MAEhBC,WAIDO,OAAOR,KAAOA,KACdC,UAAUsB,OAAOxB,OAAQS,SAJzBP,UAAY,IAAI9C,GAAGqE,iBAAiBzB,OAAQD,gBAe3C2B,aACD1B,OAAS1B,SAASoC,MAClBiB,UAAW,KACA,SAAX3B,QAAmD,KAA9BH,aAAaa,MAAMkB,YAGF,IADnBjB,KAAKC,MAAMf,aAAaa,OAC1BmB,mBACTF,UAAW,GAEjB,MAAOG,OACLC,MAAM,0BAGVJ,WACA7B,MAAM,YAAaE,QACnBF,MAAM,mBAAoBE,kBAQzBgC,2BAA2BC,eAC5BC,QAAUD,UAAY,QAAU,OACpChD,sBAAsBkD,IAAI,UAAWD,SACrChD,sBAAsBiD,IAAI,UAAWD,SACjCD,WAAarD,OAAOQ,KAAK,YACzBU,MAAM,cAAe,gBA+CpBsC,2BAA2BC,QAASC,cACrCC,cAAeC,QAZfC,wBAeC,IAAIC,gBAxCwBC,aAE7BzC,UADA0C,MAAQ,CAAC,cAAe,sBAExBhE,OAAOQ,KAAK,eACR,IAAI4B,EAAI,EAAGA,EAAI4B,MAAMzB,OAAQH,KAE7Bd,UADK/C,EAAEiD,SAASC,eAAeuC,MAAM5B,KACtBO,KAAK,wBACHoB,QACbzC,UAAU2C,UACH3C,YAAcyC,SACrBzC,UAAU4C,OA6BtBC,EAA4B,GACZzF,iBACZiF,cAAgBjF,iBAAiBoF,KACjCF,QAAUF,SAASI,KAAOJ,SAASI,KAAOH,cAAc,GACpDA,cAAcpB,OAAS,IAEvBqB,SADAQ,EAAST,cAAc,IACNC,UAErBrF,EAAEoF,cAAc,IAAInD,KAAKmD,cAAc,GAAIC,SAG/CjD,SAASH,KAAK,QAASiD,SACvBxD,UAAUO,KAAK,WAAW,GAC1B/B,IAAI4F,WAAW,2BAA4B,oBAAoBC,MAAK,SAAUC,OAsC7DC,MAAOC,iBAAkBC,KAEtCC,WAvCA9D,oBAAoB6D,MAqCPF,MArCwBf,QAqCjBgB,iBArC0BF,EAqCRG,KArCWhB,SAASkB,aAuC1DD,WAAa,2CACjBA,YAAcF,iBACdE,YAAcH,MAAQ,SAAWE,UAtCjCtB,4BAA2B,GA/BvBS,oBAAsB3D,aAAaM,KAAK,WAE5CL,eAAeK,KAAK,YAAaqD,qBACjCzD,oBAAoBI,KAAK,YAAaqD,8BA6EjCgB,gBAAgBf,IAAKgB,OACtBC,OAAOC,eAAe,iBAAmBD,OAAOE,cAGpDxG,IAAI4F,WAAWP,IAAK,oBAAoBQ,MAAK,SAASC,OAC9CW,QAAUX,EAAEnF,QAAQ,MAAO,KAC3B0F,QACAI,SAAW,KAAOJ,OAEtB3B,MAAM+B,qBA+BLC,8BACD1B,QAAU7D,UAAUwF,SAAS,mBAAmBC,OAEpC,KAAZ5B,SAA8B,cAAZA,UAElB7D,UAAUwF,SAAS,sBAAsB5E,KAAK,WAAY,YAG1DjC,EAAE+G,QAAQC,EAAEC,IAAIC,QAAU,qCACtB,CACIC,MAAOjC,QACPkC,SAAU/E,SACVgF,QAASL,EAAEC,IAAII,UAEnB,SAAUC,aA3FDC,aAAc5C,MA6FnBlC,eAAeR,KAAK,QAAS,IACzBqF,QAAQE,SACRvC,2BAA2BC,QAASoC,SACpC/C,WAhGCgD,aAmGWrC,QAnGGP,MAmGM2C,QAAQ3C,MAlG7CzE,IAAI4F,WAAW,kBAAmB,oBAAoBC,MAAK,SAASC,GAChE9F,IAAI4F,WAAWnB,MAAO,mBAAoB4C,cAAcxB,MAAK,SAAS7F,KAClEoG,gBAAgB,yBAA0BpG,SACtCuH,aAAezB,EAAI,KACvByB,cAAgBvH,IAAM,KACtBuH,cAAgB,aAAepF,SAAW,YAAckF,aACxDnH,SAAS6B,KAAK,QAASwF,wBA+FzBC,MAAK,WAIHpB,gBAAgB,2BAChBlG,SAAS6B,KAAK,QAAS,wCACvB/B,IAAI4F,WAAW,aAAc,oBAAoBC,MAAK,SAASC,GAC3D5F,SAAS6B,KAAK,QAAS+D,mBA4B9B2B,kCACDC,MAAQzG,SAAS0F,SAAS,mBAAmBC,OACjD9G,EAAE+G,QAAQC,EAAEC,IAAIC,QAAU,qCACtB,CACI/F,SAAUyG,MACVR,SAAU/E,SACVgF,QAASL,EAAEC,IAAII,UAEnB,SAAUQ,YAIFC,MAHAC,oBAAsBrF,aAAaa,MACnCyE,oBAAsBhI,EAAE,wBACxBiI,eAAiBjI,EAAE,iDAAmD6H,OAAOK,YAAc,aAE/FF,oBAAoBG,QACpBH,oBAAoBI,OAAOP,OAAOQ,QACC,GAA/BR,OAAOS,cAActE,QAA8C,KAA/B+D,oBAAoBtD,QACxD/B,aAAaa,IAAI,IACjBvD,EAAE,+BAA+BuI,SAEE,GAA/BV,OAAOS,cAActE,SACrBgE,oBAAoBI,OAAOH,gBAC3BH,MAAQ9H,WArCSwI,iBAEKC,MAAO5E,EADzCsC,KAAO,8DACPuC,KAAOF,YAAYG,kBACvBxC,MAAQ,WAAauC,KAAK,GAAK,YAAcA,KAAK,GAAK,YAAcA,KAAK,GAAK,eAC1E7E,EAAI,EAAGA,EAAI2E,YAAYF,cAActE,OAAQH,IAE9CsC,MAAQ,YADRsC,MAAQD,YAAYF,cAAczE,IACP,GAAK,YAAc4E,MAAM,GAAK,YAAcA,MAAM,GAAK,sBAEtFtC,KAAQ,mBA6BkByC,CAA4Bf,SACtCG,oBAAoBI,OAAON,OAC3BA,MAAMS,OACNN,eAAeY,OAAM,WACbZ,eAAe9B,QAAU0B,OAAOK,aAChCJ,MAAMgB,OACNb,eAAe9B,KAAK0B,OAAOkB,eAE3BjB,MAAMS,OACNN,eAAe9B,KAAK0B,OAAOK,kBAIvClI,EAAE,+BAA+B8I,OAC7BrH,OAAOQ,KAAK,YACZU,MAAM,kBAAmB,WAIvC+E,MAAK,WAEHpB,gBAAgB,sCAQf0C,4BACkB,MAAnBzG,SAASgB,MACTf,aAAasG,OAEbtG,aAAa+F,gBAQZU,iBACkB,QAAnB9H,SAASoC,OACTgB,SAiC2B,GAA/BrC,cAAcD,KAAK,WAEnB/B,IAAI4F,WAAW,sBAAuB,oBAAoBC,MAAK,SAASC,GACpEpB,MAAMoB,MAEV9D,cAAcD,KAAK,YAAY,GAC/BZ,UAAUY,KAAK,YAAY,GAC3BP,UAAUO,KAAK,YAAY,mBApBvBiH,YAAc,KACY,KAFFzG,eAAeR,KAAK,WAG5CiH,YAAclJ,EAAE,MAAQyC,eAAeR,KAAK,SAAW,QACvDjC,EAAE,kCAAkCoI,OAAOc,cAoBnDC,GAEAtE,2BAA2B7C,cACtBA,cAIDuC,SACArE,IAAI4F,WAAW,mBAAoB,oBAAoBC,MAAK,SAASC,GACjE1D,oBAAoB6D,KAAK,MAAQH,EAAI,YAJzCY,0BAQJoC,4BAEIvH,OAAOQ,KAAK,aACZU,MAAM,oBAAqB,OAC3BA,MAAM,kBAAmB,QAG7BgF,8BAIAjG,UAAU0H,GAAG,UAAU,WACA1H,UAAUO,KAAK,WAG9B4C,4BAA2B,GAE3B3E,IAAI4F,WAAW,kBAAmB,oBAAoBC,MAAK,SAASC,GAC5DQ,OAAO6C,QAAQrD,GACfnB,4BAA2B,GAE3BnD,UAAUO,KAAK,WAAW,SAM1Cf,QAAQkI,GAAG,SAAUH,gBACrBhI,SAASmI,GAAG,UAAU,WA3Ed3H,OAAOQ,KAAK,YACZU,MAAM,cAAe,OA4EzBsG,oBAGJ5H,UAAU+H,GAAG,UAAU,WACf1H,UAAUO,KAAK,WAEf/B,IAAI4F,WAAW,wBAAyB,oBAAoBC,MAAK,SAAUC,GACnEQ,OAAO6C,QAAQrD,IACfY,6BAIRA,6BAIRnF,OAAO2H,GAAG,UAAU,WACE3H,OAAOQ,KAAK,YAE1BU,MAAM,cAAe,OACrBA,MAAM,oBAAqB,OAC3BA,MAAM,kBAAmB,SAEzBA,MAAM,cAAe,IACrBA,MAAM,oBAAqB,IAC3BA,MAAM,kBAAmB,QAIjCrB,mBAAmB8H,GAAG,UAAU,WACxB9H,mBAAmBgI,GAAG,aACtBhD,gBAAgB,iCAIxBnF,SAASiI,GAAG,UAAU,WAClB7E,SACAoD,iCAGJpF,SAAS6G,GAAG,SAAUJ,2BAKP,IAAIO,kBAAkB,WACjChF,YAEKiF,QAAQrH,WAAWsH,IAAI,GAAI,aAAe,IAInDzJ,EAAE,iCAAiC6I,OAAM,eACjCa,OAAS1J,EAAE2J,MAAMC,KAAK,sBACtBC,WAAaH,OAAOtG,KAAK,MAAMvC,QAAQ,UAAW,IACtDb,EAAE,gBAAkB6J,YAAYtG,IAAImG,OAAO5C,QAC3C9G,EAAE,qBAAuB6J,YAAY1D,KAAKuD,OAAO5C,QACjD9G,EAAE,YAAc6J,YAAYC,SAAS,SACrC9J,EAAE2J,MAAM1H,KAAK,YAAY"}
\ No newline at end of file
+{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_userprototypename'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n typeName = $('#id_typename'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n */\n function setUis() {\n var uiname = uiplugin.val();\n var enableUi = true;\n if (uiname === 'html' && uiparameters.val().trim() !== '') {\n try {\n var uiparams = JSON.parse(uiparameters.val());\n if (uiparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n prototypeDisplay.prop('value', typeName.prop('value'));\n prototypeDisplay.removeAttr('hidden');\n\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n typeCombo.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","typeName","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","enableUi","trim","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/authorform.js b/amd/src/authorform.js
index 10cf66469..b2732d14b 100644
--- a/amd/src/authorform.js
+++ b/amd/src/authorform.js
@@ -23,6 +23,9 @@
define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {
+ // We need this to keep track of the current question type.
+ let currentQtype = "";
+
// Define a mapping from the fields of the JSON object returned by an AJAX
// 'get question type' request to the form elements. Only fields that
// belong to the question type should appear here. Keys are JSON field
@@ -63,6 +66,7 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
*/
function initEditForm() {
var typeCombo = $('#id_coderunnertype'),
+ prototypeDisplay = $('#id_userprototypename'),
template = $('#id_template'),
evaluatePerStudent = $('#id_templateparamsevalpertry'),
globalextra = $('#id_globalextra'),
@@ -85,6 +89,7 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
precheck = $('select#id_precheck'),
testtypedivs = $('div.testtype'),
brokenQuestion = $('#id_broken_question'),
+ badQuestionLoad = $('#id_bad_question_load'),
uiplugin = $('#id_uiplugin'),
uiparameters = $('#id_uiparameters');
@@ -240,7 +245,6 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
$(formspecifier[0]).prop(formspecifier[1], attrval);
}
- typeName.prop('value', newType);
customise.prop('checked', false);
str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {
questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));
@@ -255,12 +259,16 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
* missing question type. Report the error with an alert, and replace
* the template contents with an error message in case the user
* saves the question and later wonders why it breaks.
+ * Returns the JSON error object for further use.
* @param {string} questionType The CodeRunner (sub) question type.
- * @param {string} error The error message as a langstring to be reported.
+ * @param {string} error The error message as JSON encoded error => langstring,
+ * extra => components string.
+ * @return {JSON object} The JSON error object for further parsing.
*/
function reportError(questionType, error) {
+ const errorObject = JSON.parse(error);
str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {
- str.get_string(error, 'qtype_coderunner', questionType).then(function(str) {
+ str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {
langStringAlert('prototype_load_failure', str);
let errorMessage = s + "\n";
errorMessage += str + '\n';
@@ -268,6 +276,7 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
template.prop('value', errorMessage);
});
});
+ return errorObject;
}
/**
@@ -334,6 +343,7 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
/**
* Load the various customisation fields into the form from the
* CodeRunner question type currently selected by the combobox.
+ * Looks at the preexisting type of the selected field.
*/
function loadCustomisationFields() {
let newType = typeCombo.children('option:selected').text();
@@ -350,14 +360,23 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
sesskey: M.cfg.sesskey
},
function (outcome) {
- // Clear old broken question fields.
- brokenQuestion.prop('value', '');
+ // Clean all warnings regardless.
+ $('#id_qtype_coderunner_warning_div').empty();
if (outcome.success) {
copyFieldsFromQuestionType(newType, outcome);
setUis();
+ // Success, so remove the errors and change the current Qtype.
+ currentQtype = newType;
+ $('#id_qtype_coderunner_error_div').empty();
}
else {
- reportError(newType, outcome.error);
+ const errorObject = reportError(newType, outcome.error);
+ // Checks to see if there has been a change in type from last saved.
+ // If so, put up a load error and keep type unchanged.
+ if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {
+ showLoadTypeError(currentQtype, errorObject, newType);
+ $("#id_coderunnertype").val(currentQtype);
+ }
}
}
).fail(function () {
@@ -396,7 +415,7 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
* is changed.
*/
function loadUiParametersDescription() {
- var newUi = uiplugin.children('option:selected').text();
+ let newUi = uiplugin.children('option:selected').text();
$.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',
{
uiplugin: newUi,
@@ -476,6 +495,7 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
/**
* If the brokenquestionmessage hidden element is not empty, insert the
* given message as an error at the top of the question.
+ * itself to go back to the last valid value.
*/
function checkForBrokenQuestion() {
let brokenQuestionMessage = brokenQuestion.prop('value'),
@@ -486,23 +506,47 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
}
}
+ /**
+ * Shows the load type error of the selected type if the selected type is
+ * faulty.
+ * @param {string} currentType The current type with its errors.
+ * @param {JSON Object} errorObject The JSON object containing a list of all the errors.
+ * @param {string} newType The new type string which it failed to load.
+ */
+ function showLoadTypeError(currentType, errorObject, newType) {
+ str.get_string('loadprototypeerror', 'qtype_coderunner',
+ { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })
+ .then(function(str) {
+ $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));
+ });
+ }
+
/*************************************************************
*
* Body of initEditFormWhenReady starts here.
*
*************************************************************/
- if (prototypeType.prop('value') == 1) {
- // Editing a built-in question type: Dangerous!
- str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {
- alert(s);
- });
- prototypeType.prop('disabled', true);
- typeCombo.prop('disabled', true);
- customise.prop('disabled', true);
+ if (prototypeType.prop('value') != 0) {
+ prototypeDisplay.prop('value', typeName.prop('value'));
+ prototypeDisplay.removeAttr('hidden');
+
+ if (prototypeType.prop('value') == 1) {
+ // Editing a built-in question type: Dangerous!
+ str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {
+ alert(s);
+ });
+ prototypeType.prop('disabled', true);
+ typeCombo.prop('disabled', true);
+ customise.prop('disabled', true);
+ }
}
+
checkForBrokenQuestion();
+ badQuestionLoad.prop('hidden'); // Until we check it once.
+ // Keep track of the current prototype loaded.
+ currentQtype = typeCombo.children('option:selected').text();
setCustomisationVisibility(isCustomised);
if (!isCustomised) {
@@ -527,7 +571,7 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
// Set up event Handlers.
customise.on('change', function() {
- var isCustomised = customise.prop('checked');
+ let isCustomised = customise.prop('checked');
if (isCustomised) {
// Customisation is being turned on.
setCustomisationVisibility(true);
diff --git a/edit_coderunner_form.php b/edit_coderunner_form.php
index 842eb5b51..b2aa3e98d 100644
--- a/edit_coderunner_form.php
+++ b/edit_coderunner_form.php
@@ -387,7 +387,7 @@ public function data_preprocessing($question) {
// needs to be copied down from the options here.
$question->customise = $question->options->customise;
- // Save the prototypetype so can see if it changed on post-back.
+ // Save the prototypetype and value so can see if it changed on post-back.
$question->saved_prototype_type = $question->prototypetype;
$question->courseid = $COURSE->id;
@@ -469,8 +469,8 @@ private function load_error_messages($question, $q) {
$outputstring = "
";
// Output every duplicate Question id, name and category.
foreach ($q->prototype as $component) {
- $outputstring .= "Question ID: {$component->id}
Name: {$component->name}
"
- . "
Category: {$component->category}
";
+ $outputstring .= get_string('listprototypeduplicates', 'qtype_coderunner',
+ ['id' => $component->id, 'name' => $component->name, 'category' => $component->category]);
}
$errorstring = get_string(
'duplicateprototype', 'qtype_coderunner', ['crtype' => $question->coderunnertype,
@@ -483,9 +483,10 @@ private function load_error_messages($question, $q) {
// FUNCTIONS TO BUILD PARTS OF THE MAIN FORM
// =========================================.
- // Create an empty div with id id_qtype_coderunner_error_div for use by
+ // Create 2 empty divs with id id__qtype_coderunner_warning_div, id_qtype_coderunner_error_div for use by
// JavaScript error handling code.
private function make_error_div($mform) {
+ $mform->addElement('html', "");
$mform->addElement('html', "");
}
@@ -495,17 +496,27 @@ private function make_questiontype_panel($mform) {
$hidemethod = method_exists($mform, 'hideIf') ? 'hideIf' : 'disabledIf';
$mform->addElement('header', 'questiontypeheader', get_string('type_header', 'qtype_coderunner'));
+
+ // Insert the (possible) bad question load message as a hidden field before broken question. JavaScript
+ // will be used to show it if non-empty.
+ $mform->addElement('hidden', 'badquestionload', '',
+ array('id' => 'id_bad_question_load', 'class' => 'badquestionload'));
+ $mform->setType('badquestionload', PARAM_RAW);
+
// Insert the (possible) missing prototype message as a hidden field. JavaScript
// will be used to show it if non-empty.
$mform->addElement('hidden', 'brokenquestionmessage', '',
array('id' => 'id_broken_question', 'class' => 'brokenquestionerror'));
$mform->setType('brokenquestionmessage', PARAM_RAW);
- // The Question Type controls (a group with just a single member).
+ // The Question Type controls (a group with the question type and the custom prototype, if it is one).
$typeselectorelements = array();
$expandedtypes = array_merge(array('Undefined' => 'Undefined'), $types);
$typeselectorelements[] = $mform->createElement('select', 'coderunnertype',
null, $expandedtypes);
+ $typeselectorelements[] = $mform->createElement('text', 'userprototypename',
+ null, ['readonly' => 1, 'hidden' => 1]);
+ $mform->setType('userprototypename', PARAM_RAW);
$mform->addElement('group', 'coderunner_type_group',
get_string('coderunnertype', 'qtype_coderunner'), $typeselectorelements, null, false);
$mform->addHelpButton('coderunner_type_group', 'coderunnertype', 'qtype_coderunner');
@@ -888,7 +899,7 @@ public function validation($data, $files) {
$typename = trim($data['typename']);
if ($typename === '') {
$errors['prototypecontrols'] = get_string('empty_new_prototype_name', 'qtype_coderunner');
- } else if (!$this->is_valid_new_type($typename)) {
+ } else if (!$this->is_valid_new_type($typename) && $data['typename'] != $data['userprototypename']) {
$errors['prototypecontrols'] = get_string('bad_new_prototype_name', 'qtype_coderunner');
}
}
@@ -1265,20 +1276,25 @@ private function num_examples($data) {
return isset($data['useasexample']) ? count($data['useasexample']) : 0;
}
+ /**
+ * Return two arrays (language => language_upper_case) and (type => subtype) of
+ * all the coderunner question types available in the current course
+ * context. [If needing to filter duplicates out in future, see here! (row->count)]
+ * The subtype is the suffix of the type in the database,
+ * e.g. for java_method it is 'method'. The language is the bit before
+ * the underscore, and language_upper_case is a capitalised version,
+ * e.g. Java for java. For question types without a
+ * subtype the word 'Default' is used.
+ *
+ * @global type $COURSE The Course in which this query contex will lie.
+ * @return array Language and type arrays as specified.
+ */
private function get_languages_and_types() {
- // Return two arrays (language => language_upper_case) and (type => subtype) of
- // all the coderunner question types available in the current course
- // context.
- // The subtype is the suffix of the type in the database,
- // e.g. for java_method it is 'method'. The language is the bit before
- // the underscore, and language_upper_case is a capitalised version,
- // e.g. Java for java. For question types without a
- // subtype the word 'Default' is used.
-
global $COURSE;
$courseid = $COURSE->id;
$records = qtype_coderunner::get_all_prototypes($courseid);
- $types = array();
+ $types = [];
+ $languages = [];
foreach ($records as $row) {
if (($pos = strpos($row->coderunnertype, '_')) !== false) {
$language = substr($row->coderunnertype, 0, $pos);
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index dc720b9ff..6ff7cfbb2 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -136,7 +136,7 @@
after courses are the number of quizzes in the course with at least
one submission. The numbers in parentheses after the quiz name
are the numbers of submissions.';
-$string['duplicateprototype'] = 'This question was defined to be of type \'{$a->crtype}\' but the prototype is non-unique in the following questions: {$a->outputstring}';
+$string['duplicateprototype'] = 'This question was defined to be of type \'{$a->crtype}\' but the prototype is non-unique in the following questions: {$a->outputstring} Please remove all but one instance, or select another question type.';
$string['editingcoderunner'] = 'Editing a CodeRunner Question';
$string['empty_new_prototype_name'] = 'New question type name cannot be empty';
$string['emptypenaltyregime'] = 'Penalty regime must be defined (since version 3.1)';
@@ -381,6 +381,8 @@
$string['languageselectlabel'] = 'Language';
$string['legacyuiparams'] = 'UI parameters can no longer be defined within the template parameters field. Please move the following to the UI parameters field instead: ';
$string['legacyuiparams2'] = 'UI parameters can no longer be defined within the template parameters field. Please move the following to the UI parameters field instead, removing the \'{$a->uiname}_\' prefix: ';
+$string['listprototypeduplicates'] = 'Question ID: {$a->id}
Name: {$a->name}
Category: {$a->category}
';
+$string['loadprototypeerror'] = 'Reverted to question type: \'{$a->oldtype}\' Could not load question type \'{$a->crtype}\' as the prototype is non-unique in the following questions: {$a->outputstring}';
$string['mark'] = 'Mark';
$string['marking'] = 'Mark allocation';
$string['markinggroup'] = 'Marking';
@@ -1220,6 +1222,7 @@ function should be applied, e.g. {{STUDENT_ANSWER | e(\'py\')}} is
$string['unserializefailed'] = 'Stored test results could not be deserialised. Perhaps try regrading?';
$string['useasexample'] = 'Use as example';
$string['useace'] = 'Template uses ace';
+$string['userprototypename'] = 'Prototype name:';
$string['validateonsave'] = 'Validate on save';
diff --git a/questiontype.php b/questiontype.php
index efdbeeecd..91e1e4622 100644
--- a/questiontype.php
+++ b/questiontype.php
@@ -455,7 +455,8 @@ public function set_inherited_fields($target, $prototype) {
* Get all available prototypes for the given course.
* Only the most recent version of each prototype question is returned.
* @param int $courseid the ID of the course whose prototypes are required.
- * @return stdClass[] prototype rows from question_coderunner_options.
+ * @return stdClass[] prototype rows from question_coderunner_options,
+ * including count number of occurrences.
*/
public static function get_all_prototypes($courseid) {
global $DB;
@@ -463,7 +464,7 @@ public static function get_all_prototypes($courseid) {
list($contextcondition, $params) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true));
$rows = $DB->get_records_sql("
- SELECT qco.*
+ SELECT qco.coderunnertype, count(qco.coderunnertype) as count
FROM {question_coderunner_options} qco
JOIN {question} q ON q.id = qco.questionid
JOIN {question_versions} qv ON qv.questionid = q.id
@@ -475,7 +476,8 @@ public static function get_all_prototypes($courseid) {
WHERE be.id = qbe.id)
)
AND prototypetype != 0
- AND qc.contextid $contextcondition", $params);
+ AND qc.contextid $contextcondition
+ GROUP BY qco.coderunnertype", $params);
return $rows;
}
diff --git a/styles.css b/styles.css
index e87032d66..6ca41534b 100644
--- a/styles.css
+++ b/styles.css
@@ -269,7 +269,8 @@ body#page-question-type-coderunner pre.templateparamserror {
color: #ca3120;
}
-body#page-question-type-coderunner div#id_qtype_coderunner_error_div:empty {
+body#page-question-type-coderunner div#id_qtype_coderunner_error_div:empty,
+body#page-question-type-coderunner div#id_qtype_coderunner_warning_div:empty {
display: none;
}
@@ -279,6 +280,16 @@ body#page-question-type-coderunner div#id_qtype_coderunner_error_div {
color: red;
border: 2px solid red;
padding: 4px;
+ margin-bottom: 6px;
+}
+
+body#page-question-type-coderunner div#id_qtype_coderunner_warning_div {
+ font-size: 120%;
+ font-weight: bold;
+ color: blue;
+ border: 2px solid blue;
+ padding: 4px;
+ margin-bottom: 6px;
}
body#page-question-type-coderunner textarea#id_templateparams {
From 340109ae6ef83103804ea09fe046a3b8f10cf556 Mon Sep 17 00:00:00 2001
From: Michelle Hsieh
Date: Thu, 22 Dec 2022 21:48:25 +1300
Subject: [PATCH 029/188] Now have modified behaviour for handling drop-downs,
fixed some tests
---
amd/build/authorform.min.js | 2 +-
amd/build/authorform.min.js.map | 2 +-
amd/src/authorform.js | 22 ++++++++++++++++------
edit_coderunner_form.php | 15 +++++++++------
lang/en/qtype_coderunner.php | 2 +-
questiontype.php | 2 +-
styles.css | 4 ++++
tests/behat/missing_prototype.feature | 2 +-
8 files changed, 34 insertions(+), 17 deletions(-)
diff --git a/amd/build/authorform.min.js b/amd/build/authorform.min.js
index 88fd003f6..9ef278644 100644
--- a/amd/build/authorform.min.js
+++ b/amd/build/authorform.min.js
@@ -5,6 +5,6 @@
* @copyright Richard Lobb, 2015, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/authorform",["jquery","qtype_coderunner/userinterfacewrapper","core/str"],(function($,ui,str){let currentQtype="";var JSON_TO_FORM_MAP={template:["#id_template","value",""],iscombinatortemplate:["#id_iscombinatortemplate","checked","",function(value){return"1"===value}],cputimelimitsecs:["#id_cputimelimitsecs","value",""],memlimitmb:["#id_memlimitmb","value",""],sandbox:["#id_sandbox","value","DEFAULT"],sandboxparams:["#id_sandboxparams","value",""],testsplitterre:["#id_testsplitterre","value","",function(splitter){return splitter.replace("\n","\\n")}],allowmultiplestdins:["#id_allowmultiplestdins","checked","",function(value){return"1"===value}],grader:["#id_grader","value","EqualityGrader"],resultcolumns:["#id_resultcolumns","value",""],language:["#id_language","value",""],acelang:["#id_acelang","value",""],uiplugin:["#id_uiplugin","value","ace"]};return{initEditForm:function(){var typeCombo=$("#id_coderunnertype"),prototypeDisplay=$("#id_userprototypename"),template=$("#id_template"),evaluatePerStudent=$("#id_templateparamsevalpertry"),globalextra=$("#id_globalextra"),prototypeextra=$("#id_prototypeextra"),useace=$("#id_useace"),language=$("#id_language"),acelang=$("#id_acelang"),customise=$("#id_customise"),isCombinator=$("#id_iscombinatortemplate"),testSplitterRe=$("#id_testsplitterre"),allowMultipleStdins=$("#id_allowmultiplestdins"),customisationFieldSet=$("#id_customisationheader"),advancedCustomisation=$("#id_advancedcustomisationheader"),isCustomised=customise.prop("checked"),prototypeType=$("#id_prototypetype"),preloadHdr=$("#id_answerpreloadhdr"),typeName=$("#id_typename"),courseId=$('input[name="courseid"]').prop("value"),questiontypeHelpDiv=$("#qtype-help"),precheck=$("select#id_precheck"),testtypedivs=$("div.testtype"),brokenQuestion=$("#id_broken_question"),badQuestionLoad=$("#id_bad_question_load"),uiplugin=$("#id_uiplugin"),uiparameters=$("#id_uiparameters");function setUi(taId,uiname){var lang,uiWrapper,ta=$(document.getElementById(taId)),currentLang=ta.attr("data-lang"),paramsJson=ta.attr("data-params"),params={};ta.attr("data-prototypeextra",prototypeextra.val()),ta.attr("data-globalextra",globalextra.val()),ta.attr("data-test0",$("#id_testcode_0").val());try{params=JSON.parse(paramsJson)}catch(err){}"none"===(uiname=uiname.toLowerCase())&&(uiname=""),"id_templateparams"==taId||"id_uiparameters"==taId?lang="":(lang=language.prop("value"),"id_template"!==taId&&acelang.prop("value")&&(lang=function(acelang){var langs,i;if(acelang.indexOf(",")<0)return acelang;for(langs=acelang.split(","),i=0;i0?langs[0]:""}(acelang.prop("value")))),(uiWrapper=ta.data("current-ui-wrapper"))&&uiWrapper.uiname===uiname&¤tLang==lang||(ta.attr("data-lang",lang),uiWrapper?(params.lang=lang,uiWrapper.loadUi(uiname,params)):uiWrapper=new ui.InterfaceWrapper(uiname,taId))}function setUis(){var uiname=uiplugin.val(),enableUi=!0;if("html"===uiname&&""!==uiparameters.val().trim())try{!1===JSON.parse(uiparameters.val()).enable_in_editor&&(enableUi=!1)}catch(error){alert("Invalid UI parameters.")}enableUi&&(setUi("id_answer",uiname),setUi("id_answerpreload",uiname))}function setCustomisationVisibility(isVisible){var display=isVisible?"block":"none";customisationFieldSet.css("display",display),advancedCustomisation.css("display",display),isVisible&&useace.prop("checked")&&setUi("id_template","ace")}function copyFieldsFromQuestionType(newType,response){var formspecifier,attrval,isCombinatorEnabled;for(var key in function(stateOn){var uiWrapper,taIds=["id_template","id_uiparameters"];if(useace.prop("checked"))for(var i=0;i3&&(attrval=(0,formspecifier[3])(attrval)),$(formspecifier[0]).prop(formspecifier[1],attrval);customise.prop("checked",!1),str.get_string("coderunner_question_type","qtype_coderunner").then((function(s){var title,coderunner_descr,html,resultHtml;questiontypeHelpDiv.html((title=newType,coderunner_descr=s,html=response.questiontext,resultHtml='
")}))):loadCustomisationFields(),set_testtype_visibilities(),useace.prop("checked")&&(setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")),loadUiParametersDescription(),customise.on("change",(function(){customise.prop("checked")?setCustomisationVisibility(!0):str.get_string("confirm_proceed","qtype_coderunner").then((function(s){window.confirm(s)?setCustomisationVisibility(!1):customise.prop("checked",!0)}))})),acelang.on("change",check_ace_lang),language.on("change",(function(){useace.prop("checked")&&setUi("id_template","ace"),check_ace_lang()})),typeCombo.on("change",(function(){customise.prop("checked")?str.get_string("question_type_changed","qtype_coderunner").then((function(s){window.confirm(s)&&loadCustomisationFields()})):loadCustomisationFields()})),useace.on("change",(function(){useace.prop("checked")?(setUi("id_template","ace"),setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")):(setUi("id_template",""),setUi("id_templateparams",""),setUi("id_uiparameters",""))})),evaluatePerStudent.on("change",(function(){evaluatePerStudent.is(":checked")&&langStringAlert("templateparamsusingsandbox")})),uiplugin.on("change",(function(){setUis(),loadUiParametersDescription()})),precheck.on("change",set_testtype_visibilities),prototypeType.on("change",(function(){"0"==prototypeType.prop("value")?prototypeDisplay.attr("hidden","1"):prototypeDisplay.removeAttr("hidden")})),new MutationObserver((function(){setUis()})).observe(preloadHdr.get(0),{attributes:!0}),$("button.replaceexpectedwithgot").click((function(){var gotPre=$(this).prev('pre[id^="id_got_"]'),testCaseId=gotPre.attr("id").replace("id_got_","");$("#id_expected_"+testCaseId).val(gotPre.text()),$("#id_fail_expected_"+testCaseId).html(gotPre.text()),$(".failrow_"+testCaseId).addClass("fixed"),$(this).prop("disabled",!0)})),$(".btn-primary").click((function(){typeCombo.prop("disabled",!1)}))}}}));
//# sourceMappingURL=authorform.min.js.map
\ No newline at end of file
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
index 709e7dfc0..a44df6300 100644
--- a/amd/build/authorform.min.js.map
+++ b/amd/build/authorform.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_userprototypename'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n typeName = $('#id_typename'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n */\n function setUis() {\n var uiname = uiplugin.val();\n var enableUi = true;\n if (uiname === 'html' && uiparameters.val().trim() !== '') {\n try {\n var uiparams = JSON.parse(uiparameters.val());\n if (uiparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n prototypeDisplay.prop('value', typeName.prop('value'));\n prototypeDisplay.removeAttr('hidden');\n\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n typeCombo.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","typeName","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","enableUi","trim","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n */\n function setUis() {\n var uiname = uiplugin.val();\n var enableUi = true;\n if (uiname === 'html' && uiparameters.val().trim() !== '') {\n try {\n var uiparams = JSON.parse(uiparameters.val());\n if (uiparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","enableUi","trim","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/authorform.js b/amd/src/authorform.js
index b2732d14b..a64a48c04 100644
--- a/amd/src/authorform.js
+++ b/amd/src/authorform.js
@@ -66,7 +66,7 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
*/
function initEditForm() {
var typeCombo = $('#id_coderunnertype'),
- prototypeDisplay = $('#id_userprototypename'),
+ prototypeDisplay = $('#id_isprototype'),
template = $('#id_template'),
evaluatePerStudent = $('#id_templateparamsevalpertry'),
globalextra = $('#id_globalextra'),
@@ -83,7 +83,6 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
isCustomised = customise.prop('checked'),
prototypeType = $('#id_prototypetype'),
preloadHdr = $('#id_answerpreloadhdr'),
- typeName = $('#id_typename'),
courseId = $('input[name="courseid"]').prop('value'),
questiontypeHelpDiv = $('#qtype-help'),
precheck = $('select#id_precheck'),
@@ -528,21 +527,18 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
*************************************************************/
if (prototypeType.prop('value') != 0) {
- prototypeDisplay.prop('value', typeName.prop('value'));
+ // Display the prototype warning if it's a prototype.
prototypeDisplay.removeAttr('hidden');
-
if (prototypeType.prop('value') == 1) {
// Editing a built-in question type: Dangerous!
str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {
alert(s);
});
prototypeType.prop('disabled', true);
- typeCombo.prop('disabled', true);
customise.prop('disabled', true);
}
}
-
checkForBrokenQuestion();
badQuestionLoad.prop('hidden'); // Until we check it once.
// Keep track of the current prototype loaded.
@@ -631,6 +627,15 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
precheck.on('change', set_testtype_visibilities);
+ // Displays and hides the reason for the question type to be disabled.
+ prototypeType.on('change', function () {
+ if (prototypeType.prop('value') == '0') {
+ prototypeDisplay.attr('hidden', '1');
+ } else {
+ prototypeDisplay.removeAttr('hidden');
+ }
+ });
+
// In order to initialise the Ui plugin when the answer preload section is
// expanded, we monitor attribute mutations in the Answer Preload
// header.
@@ -649,6 +654,11 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
$('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.
$(this).prop('disabled', true);
});
+
+ // On reloading the page, enable the typeCombo so that its value is POSTed.
+ $('.btn-primary').click(function() {
+ typeCombo.prop('disabled', false);
+ });
}
return {initEditForm: initEditForm};
diff --git a/edit_coderunner_form.php b/edit_coderunner_form.php
index b2aa3e98d..489db4687 100644
--- a/edit_coderunner_form.php
+++ b/edit_coderunner_form.php
@@ -509,14 +509,15 @@ private function make_questiontype_panel($mform) {
array('id' => 'id_broken_question', 'class' => 'brokenquestionerror'));
$mform->setType('brokenquestionmessage', PARAM_RAW);
- // The Question Type controls (a group with the question type and the custom prototype, if it is one).
+ // The Question Type controls (a group with the question type and the warning, if it is one).
$typeselectorelements = array();
$expandedtypes = array_merge(array('Undefined' => 'Undefined'), $types);
$typeselectorelements[] = $mform->createElement('select', 'coderunnertype',
null, $expandedtypes);
- $typeselectorelements[] = $mform->createElement('text', 'userprototypename',
- null, ['readonly' => 1, 'hidden' => 1]);
- $mform->setType('userprototypename', PARAM_RAW);
+ $prototypelangstring = get_string('prototypeexists', 'qtype_coderunner');
+ $typeselectorelements[] = $mform->createElement('html',
+ "
"
+ . "{$prototypelangstring}
");
$mform->addElement('group', 'coderunner_type_group',
get_string('coderunnertype', 'qtype_coderunner'), $typeselectorelements, null, false);
$mform->addHelpButton('coderunner_type_group', 'coderunnertype', 'qtype_coderunner');
@@ -824,10 +825,12 @@ private function make_advanced_customisation_panel($mform) {
// status of the testsplitterre and allowmultiplestdins elements
// after loading a new question type as the following code apparently
// sets up event handlers only for clicks on the iscombinatortemplate
- // checkbox.
+ // checkbox. Note, if disabled, the value doesn't exist, so check
+ // properties exist!
$mform->disabledIf('typename', 'prototypetype', 'neq', '2');
$mform->disabledIf('testsplitterre', 'iscombinatortemplate', 'eq', 0);
$mform->disabledIf('allowmultiplestdins', 'iscombinatortemplate', 'eq', 0);
+ $mform->disabledIf('coderunnertype', 'prototypetype', 'eq', '2');
}
@@ -899,7 +902,7 @@ public function validation($data, $files) {
$typename = trim($data['typename']);
if ($typename === '') {
$errors['prototypecontrols'] = get_string('empty_new_prototype_name', 'qtype_coderunner');
- } else if (!$this->is_valid_new_type($typename) && $data['typename'] != $data['userprototypename']) {
+ } else if (!$this->is_valid_new_type($typename)) {
$errors['prototypecontrols'] = get_string('bad_new_prototype_name', 'qtype_coderunner');
}
}
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index 6ff7cfbb2..2a61bf38b 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -487,6 +487,7 @@
$string['privacy:metadata'] = 'The CodeRunner question type plugin does not store any personal data.';
$string['proceed_at_own_risk'] = 'Editing a built-in question prototype?! Proceed at your own risk!';
$string['prototypecontrols'] = 'Prototyping';
+$string['prototypeexists'] = 'This is a prototype; cannot change question type.';
$string['prototypeextra'] = 'Prototype extra';
$string['prototypeextra_help'] = 'A field of text for general-purpose use by question type authors, like global extra, but part of the prototype state. Available to the template author as {{ QUESTION.prototypeextra }}.';
@@ -1222,7 +1223,6 @@ function should be applied, e.g. {{STUDENT_ANSWER | e(\'py\')}} is
$string['unserializefailed'] = 'Stored test results could not be deserialised. Perhaps try regrading?';
$string['useasexample'] = 'Use as example';
$string['useace'] = 'Template uses ace';
-$string['userprototypename'] = 'Prototype name:';
$string['validateonsave'] = 'Validate on save';
diff --git a/questiontype.php b/questiontype.php
index 91e1e4622..6fef0c82b 100644
--- a/questiontype.php
+++ b/questiontype.php
@@ -842,7 +842,7 @@ public function export_to_xml($question, qformat_xml $format, $extra=null) {
// Clear all inherited fields equal in value to the corresponding Prototype field
// (but only if we found a prototype and this is not a prototype question itself).
- if ($row && $questiontoexport->options->prototypetype == 0 && is_array($row)) {
+ if ($row && $questiontoexport->options->prototypetype == 0) {
$noninheritedfields = $this->noninherited_fields();
$extrafields = $this->extra_question_fields();
foreach ($row as $field => $value) {
diff --git a/styles.css b/styles.css
index 6ca41534b..7c4f14278 100644
--- a/styles.css
+++ b/styles.css
@@ -269,6 +269,10 @@ body#page-question-type-coderunner pre.templateparamserror {
color: #ca3120;
}
+body#page-question-type-coderunner .qtype_coderunner_prototype_message {
+ color: #ca3120;
+}
+
body#page-question-type-coderunner div#id_qtype_coderunner_error_div:empty,
body#page-question-type-coderunner div#id_qtype_coderunner_warning_div:empty {
display: none;
diff --git a/tests/behat/missing_prototype.feature b/tests/behat/missing_prototype.feature
index c57cf156b..7260429bd 100644
--- a/tests/behat/missing_prototype.feature
+++ b/tests/behat/missing_prototype.feature
@@ -67,7 +67,7 @@ Feature: missing_prototype
Scenario: As a teacher, I should be able to re-parent the question and have it work correctly
And I am on the "Prototype tester" "core_question > edit" page
- Then I should see "This question was defined to be of type 'python3_test_prototype' but the prototype does not exist, or is non-unique, or is unavailable in this context"
+ Then I should see "This question was defined to be of type 'python3_test_prototype' but the prototype does not exist, or is unavailable in this context"
And I set the field "id_coderunnertype" to "python3"
And I set the field "id_customise" to "1"
And I set the field "id_uiplugin" to "None"
From 7e81e88b367900bdff18d0770bf7d5b9aac22e10 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Mon, 16 Jan 2023 09:15:03 +1300
Subject: [PATCH 030/188] Prevent grading of the unchanged answer preload.
---
lang/en/qtype_coderunner.php | 1 +
question.php | 3 +++
tests/walkthrough_test.php | 11 +++++++++++
3 files changed, 15 insertions(+)
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index d803a777d..7b7d39672 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -44,6 +44,7 @@
$string['answer'] = 'Sample answer';
$string['answerprompt'] = 'Answer:';
$string['answer_help'] = 'A sample answer can be entered here and used for checking by the question author and optionally shown to students during review. It is also used by the bulk tester script. The correctness of a non-empty answer is checked when saving unless \'Validate on save\' is unchecked';
+$string['answerunchanged'] = 'You must complete or edit the preloaded answer.';
$string['answerrequired'] = 'Please provide a non-empty answer';
$string['answertooshort'] = 'Answer too short. Must be at least {$a} characters.';
$string['atleastonetest'] = 'You must provide at least one test case for this question.';
diff --git a/question.php b/question.php
index beffa9440..f5d003855 100644
--- a/question.php
+++ b/question.php
@@ -379,6 +379,8 @@ public function validate_response(array $response) {
return get_string('answerrequired', 'qtype_coderunner');
} else if (strlen($response['answer']) < constants::FUNC_MIN_LENGTH) {
return get_string('answertooshort', 'qtype_coderunner', constants::FUNC_MIN_LENGTH);
+ } else if (trim($response['answer']) == trim($this->answerpreload)) {
+ return get_string('answerunchanged', 'qtype_coderunner');
}
}
return ''; // All good.
@@ -642,6 +644,7 @@ private function twig_all() {
$this->answer = $this->twig_expand($this->answer);
$this->answerpreload = $this->twig_expand($this->answerpreload);
$this->globalextra = $this->twig_expand($this->globalextra);
+ $this->prototypeextra = $this->twig_expand($this->prototypeextra);
if (!empty($this->uiparameters)) {
$this->uiparameters = $this->twig_expand($this->uiparameters);
}
diff --git a/tests/walkthrough_test.php b/tests/walkthrough_test.php
index dcc274700..3d2c8dd5a 100644
--- a/tests/walkthrough_test.php
+++ b/tests/walkthrough_test.php
@@ -359,6 +359,17 @@ public function test_hide_check() {
$this->check_output_does_not_contain('Check');
}
+ // Check that a question with an answer preload is not gradable if answer not changed
+ public function test_preload_not_graded() {
+ $q = \test_question_maker::make_question('coderunner', 'sqr');
+ $q->answerpreload = 'def sqr(n):';
+ $this->start_attempt_at_question($q, 'adaptive', 1, 1);
+ $this->check_output_contains('def sqr(n):');
+ $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n):'));
+ $this->check_current_state(\question_state::$invalid);
+ $this->check_current_mark(null);
+ }
+
public function test_stop_button_always() {
$q = \test_question_maker::make_question('coderunner', 'sqr');
$q->giveupallowed = constants::GIVEUP_ALWAYS;
From 21d7a0a1a44da2faf3867e2f29c359cd76ee9097 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Mon, 16 Jan 2023 09:18:09 +1300
Subject: [PATCH 031/188] Add a few more queries.
---
miscsqlqueries | 79 +++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 78 insertions(+), 1 deletion(-)
diff --git a/miscsqlqueries b/miscsqlqueries
index 83bf4b1cd..b8b6c63d9 100644
--- a/miscsqlqueries
+++ b/miscsqlqueries
@@ -1,3 +1,5 @@
+# ************ Moodle 3.n queries ***************
+
List all courses:
SELECT crs.id as courseid, fullname, path, depth, ctx.contextlevel, ctx.id as ctxid
FROM `mdl_context` as ctx
@@ -281,4 +283,79 @@ WHERE slt.id is null
AND catid is null
AND contextlevel=50
AND crs.id=31
-ORDER BY cat.name;
\ No newline at end of file
+ORDER BY cat.name;
+
+Turn on Stop button for all CodeRunner questions in a given category + course.
+Also fill in the General Feedback to display the answer for the various
+different question types.
+
+# ========================================================
+SET @question_category = _utf8mb4'LM6%' COLLATE 'utf8mb4_unicode_ci';
+
+UPDATE mdl_question_coderunner_options cro
+JOIN mdl_question q ON cro.questionid = q.id
+JOIN mdl_question_categories cat ON q.category = cat.id
+JOIN `mdl_context` ctx ON cat.contextid = ctx.id
+JOIN mdl_course crs ON ctx.instanceid = crs.id
+SET giveupallowed=2
+WHERE contextlevel=50
+AND cat.name like @question_category
+AND (cro.coderunnertype = 'python3_scratchpad' OR cro.coderunnertype = 'python3' OR cro.coderunnertype = 'python3_stage1' OR cro.coderunnertype = 'python3_stage1_gapfiller')
+AND shortname like 'COSC131Headstart';
+
+UPDATE mdl_question q
+JOIN mdl_question_coderunner_options cro ON cro.questionid = q.id
+JOIN mdl_question_categories cat ON q.category = cat.id
+JOIN `mdl_context` ctx ON cat.contextid = ctx.id
+JOIN mdl_course crs ON ctx.instanceid = crs.id
+SET generalfeedback = CONCAT('
')
+WHERE contextlevel=50
+AND cro.coderunnertype = 'python3_stage1_gapfiller'
+AND cat.name like @question_category
+AND shortname like 'COSC131Headstart';
+
+# =========================================
+Find questions in a given category that aren't of the given coderunner type
+SELECT q.name
+FROM mdl_question_coderunner_options cro
+JOIN mdl_question q ON cro.questionid = q.id
+JOIN mdl_question_categories cat ON q.category = cat.id
+JOIN `mdl_context` ctx ON cat.contextid = ctx.id
+JOIN mdl_course crs ON ctx.instanceid = crs.id
+WHERE contextlevel=50
+AND cat.name like @question_category
+AND shortname like 'COSC131Headstart'
+AND cro.coderunnertype <> 'python3_stage1'
+AND cro.coderunnertype <> 'python3_scratchpad'
+AND cro.coderunnertype <> 'python3';
\ No newline at end of file
From 2862a2ce7a7efb851d1cfa2357d282cfada19c41 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Mon, 16 Jan 2023 09:18:57 +1300
Subject: [PATCH 032/188] Commit grunt rebuilds of JavaScript modules.
---
amd/build/graphutil.min.js | 2 +-
amd/build/graphutil.min.js.map | 2 +-
amd/build/textareas.min.js | 2 +-
amd/build/textareas.min.js.map | 2 +-
amd/build/ui_ace_gapfiller.min.js | 2 +-
amd/build/ui_ace_gapfiller.min.js.map | 2 +-
amd/build/userinterfacewrapper.min.js | 2 +-
amd/build/userinterfacewrapper.min.js.map | 2 +-
8 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/amd/build/graphutil.min.js b/amd/build/graphutil.min.js
index 93b742731..feb02fc20 100644
--- a/amd/build/graphutil.min.js
+++ b/amd/build/graphutil.min.js
@@ -1,3 +1,3 @@
-define("qtype_coderunner/graphutil",(function(){function Util(){this.greekLetterNames=["Alpha","Beta","Gamma","Delta","Epsilon","Zeta","Eta","Theta","Iota","Kappa","Lambda","Mu","Nu","Xi","Omicron","Pi","Rho","Sigma","Tau","Upsilon","Phi","Chi","Psi","Omega"]}return Util.prototype.convertLatexShortcuts=function(text){for(var i=0;i16)))).replace(new RegExp("\\\\"+name.toLowerCase(),"g"),String.fromCharCode(945+i+(i>16)))}for(i=0;i<10;i++)text=text.replace(new RegExp("_"+i,"g"),String.fromCharCode(8320+i));return text=text.replace(new RegExp("_a","g"),String.fromCharCode(8336))},Util.prototype.drawArrow=function(c,x,y,angle){var dx=Math.cos(angle),dy=Math.sin(angle);c.beginPath(),c.moveTo(x,y),c.lineTo(x-8*dx+5*dy,y-8*dy-5*dx),c.lineTo(x-8*dx-5*dy,y-8*dy+5*dx),c.fill()},Util.prototype.det=function(a,b,c,d,e,f,g,h,i){return a*e*i+b*f*g+c*d*h-a*f*h-b*d*i-c*e*g},Util.prototype.vectorMagnitude=function(v){return Math.sqrt(v.x*v.x+v.y*v.y)},Util.prototype.scalarProjection=function(a,b){return(a.x*b.x+a.y*b.y)/this.vectorMagnitude(b)},Util.prototype.isCCW=function(a,b){return a.x*b.y-b.x*a.y>0},Util.prototype.circleFromThreePoints=function(x1,y1,x2,y2,x3,y3){var a=this.det(x1,y1,1,x2,y2,1,x3,y3,1),bx=-this.det(x1*x1+y1*y1,y1,1,x2*x2+y2*y2,y2,1,x3*x3+y3*y3,y3,1),by=this.det(x1*x1+y1*y1,x1,1,x2*x2+y2*y2,x2,1,x3*x3+y3*y3,x3,1),c=-this.det(x1*x1+y1*y1,x1,y1,x2*x2+y2*y2,x2,y2,x3*x3+y3*y3,x3,y3);return{x:-bx/(2*a),y:-by/(2*a),radius:Math.sqrt(bx*bx+by*by-4*a*c)/(2*Math.abs(a))}},Util.prototype.isInside=function(pos,rect){return pos.x>rect.x&&pos.xrect.y},Util.prototype.crossBrowserKey=function(e){return(e=e||window.event).which||e.keyCode},Util.prototype.crossBrowserRelativeMousePos=function(e){const rect=e.target.getBoundingClientRect();return{x:e.clientX-rect.left,y:e.clientY-rect.top}},new Util}));
+define("qtype_coderunner/graphutil",(function(){function Util(){this.greekLetterNames=["Alpha","Beta","Gamma","Delta","Epsilon","Zeta","Eta","Theta","Iota","Kappa","Lambda","Mu","Nu","Xi","Omicron","Pi","Rho","Sigma","Tau","Upsilon","Phi","Chi","Psi","Omega"]}return Util.prototype.convertLatexShortcuts=function(text){for(var i=0;i16)))).replace(new RegExp("\\\\"+name.toLowerCase(),"g"),String.fromCharCode(945+i+(i>16)))}for(i=0;i<10;i++)text=text.replace(new RegExp("_"+i,"g"),String.fromCharCode(8320+i));return text=text.replace(new RegExp("_a","g"),String.fromCharCode(8336))},Util.prototype.drawArrow=function(c,x,y,angle){var dx=Math.cos(angle),dy=Math.sin(angle);c.beginPath(),c.moveTo(x,y),c.lineTo(x-8*dx+5*dy,y-8*dy-5*dx),c.lineTo(x-8*dx-5*dy,y-8*dy+5*dx),c.fill()},Util.prototype.det=function(a,b,c,d,e,f,g,h,i){return a*e*i+b*f*g+c*d*h-a*f*h-b*d*i-c*e*g},Util.prototype.vectorMagnitude=function(v){return Math.sqrt(v.x*v.x+v.y*v.y)},Util.prototype.scalarProjection=function(a,b){return(a.x*b.x+a.y*b.y)/this.vectorMagnitude(b)},Util.prototype.isCCW=function(a,b){return a.x*b.y-b.x*a.y>0},Util.prototype.circleFromThreePoints=function(x1,y1,x2,y2,x3,y3){var a=this.det(x1,y1,1,x2,y2,1,x3,y3,1),bx=-this.det(x1*x1+y1*y1,y1,1,x2*x2+y2*y2,y2,1,x3*x3+y3*y3,y3,1),by=this.det(x1*x1+y1*y1,x1,1,x2*x2+y2*y2,x2,1,x3*x3+y3*y3,x3,1),c=-this.det(x1*x1+y1*y1,x1,y1,x2*x2+y2*y2,x2,y2,x3*x3+y3*y3,x3,y3);return{x:-bx/(2*a),y:-by/(2*a),radius:Math.sqrt(bx*bx+by*by-4*a*c)/(2*Math.abs(a))}},Util.prototype.isInside=function(pos,rect){return pos.x>rect.x&&pos.xrect.y},Util.prototype.crossBrowserKey=function(e){return(e=e||window.event).which||e.keyCode},Util.prototype.crossBrowserRelativeMousePos=function(e){var rect=e.target.getBoundingClientRect();return{x:e.clientX-rect.left,y:e.clientY-rect.top}},new Util}));
//# sourceMappingURL=graphutil.min.js.map
\ No newline at end of file
diff --git a/amd/build/graphutil.min.js.map b/amd/build/graphutil.min.js.map
index 23ac65002..5f11864b8 100644
--- a/amd/build/graphutil.min.js.map
+++ b/amd/build/graphutil.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"graphutil.min.js","sources":["../src/graphutil.js"],"sourcesContent":["/***********************************************************************\n *\n * Utility functions/data/constants for the ui_graph module.\n *\n ***********************************************************************/\n// Most of this code is taken from Finite State Machine Designer:\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(function() {\n /**\n * Contstructor for the Util class.\n */\n function Util() {\n\n this.greekLetterNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',\n 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda',\n 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',\n 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega' ];\n }\n\n Util.prototype.convertLatexShortcuts = function(text) {\n // Html greek characters.\n for(var i = 0; i < this.greekLetterNames.length; i++) {\n var name = this.greekLetterNames[i];\n text = text.replace(new RegExp('\\\\\\\\' + name, 'g'), String.fromCharCode(913 + i + (i > 16)));\n text = text.replace(new RegExp('\\\\\\\\' + name.toLowerCase(), 'g'), String.fromCharCode(945 + i + (i > 16)));\n }\n\n // Subscripts.\n for(var i = 0; i < 10; i++) {\n text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));\n }\n text = text.replace(new RegExp('_a', 'g'), String.fromCharCode(8336));\n return text;\n };\n\n Util.prototype.drawArrow = function(c, x, y, angle) {\n // Draw an arrow head on the graphics context c at (x, y) with given angle.\n\n var dx = Math.cos(angle);\n var dy = Math.sin(angle);\n c.beginPath();\n c.moveTo(x, y);\n c.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);\n c.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);\n c.fill();\n };\n\n Util.prototype.det = function(a, b, c, d, e, f, g, h, i) {\n // Determinant of given matrix elements.\n return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g;\n };\n\n Util.prototype.vectorMagnitude = function(v){\n // Returns magnitude (length) of a vector v\n return Math.sqrt(v.x * v.x + v.y * v.y);\n };\n\n Util.prototype.scalarProjection = function(a, b) {\n // Returns scalar projection of vector a onto vector b\n return (a.x * b.x + a.y * b.y) / this.vectorMagnitude(b);\n };\n\n Util.prototype.isCCW = function(a, b) {\n // Returns true iff vector b is in a counter-clockwise orientation relative to a\n return (a.x * b.y) - (b.x * a.y) > 0;\n };\n\n Util.prototype.circleFromThreePoints = function(x1, y1, x2, y2, x3, y3) {\n // Return {x, y, radius} of circle through (x1, y1), (x2, y2), (x3, y3).\n var a = this.det(x1, y1, 1, x2, y2, 1, x3, y3, 1);\n var bx = -this.det(x1 * x1 + y1 * y1, y1, 1, x2 * x2 + y2 * y2, y2, 1, x3 * x3 + y3 * y3, y3, 1);\n var by = this.det(x1 * x1 + y1 * y1, x1, 1, x2 * x2 + y2 * y2, x2, 1, x3 * x3 + y3 * y3, x3, 1);\n var c = -this.det(x1 * x1 + y1 * y1, x1, y1, x2 * x2 + y2 * y2, x2, y2, x3 * x3 + y3 * y3, x3, y3);\n return {\n 'x': -bx / (2 * a),\n 'y': -by / (2 * a),\n 'radius': Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))\n };\n };\n\n Util.prototype.isInside = function(pos, rect) {\n // True iff given point pos is inside rectangle.\n return pos.x > rect.x && pos.x < rect.x + rect.width && pos.y < rect.y + rect.height && pos.y > rect.y;\n };\n\n Util.prototype.crossBrowserKey = function(e) {\n // Return which key was pressed, given the event, in a browser-independent way.\n e = e || window.event;\n return e.which || e.keyCode;\n };\n\n\n Util.prototype.crossBrowserRelativeMousePos = function(e) {\n // Earlier complex version was breaking in Moodle 4, so replaced\n // with this much simpler version that should work with all modern\n // browsers.\n const rect = e.target.getBoundingClientRect();\n const x = e.clientX - rect.left; //x position within the element.\n const y = e.clientY - rect.top; //y position within the element.\n return {'x': x, 'y': y};\n };\n\n return new Util();\n});\n"],"names":["define","Util","greekLetterNames","prototype","convertLatexShortcuts","text","i","this","length","name","replace","RegExp","String","fromCharCode","toLowerCase","drawArrow","c","x","y","angle","dx","Math","cos","dy","sin","beginPath","moveTo","lineTo","fill","det","a","b","d","e","f","g","h","vectorMagnitude","v","sqrt","scalarProjection","isCCW","circleFromThreePoints","x1","y1","x2","y2","x3","y3","bx","by","abs","isInside","pos","rect","width","height","crossBrowserKey","window","event","which","keyCode","crossBrowserRelativeMousePos","target","getBoundingClientRect","clientX","left","clientY","top"],"mappings":"AA8CAA,qCAAO,oBAIMC,YAEAC,iBAAmB,CAAC,QAAS,OAAQ,QAAS,QAAS,UACpC,OAAQ,MAAO,QAAS,OAAQ,QAAS,SACzC,KAAM,KAAM,KAAM,UAAW,KAAM,MAAO,QAC1C,MAAO,UAAW,MAAO,MAAO,MAAO,gBAGnED,KAAKE,UAAUC,sBAAwB,SAASC,UAExC,IAAIC,EAAI,EAAGA,EAAIC,KAAKL,iBAAiBM,OAAQF,IAAK,KAC9CG,KAAOF,KAAKL,iBAAiBI,GAEjCD,MADAA,KAAOA,KAAKK,QAAQ,IAAIC,OAAO,OAASF,KAAM,KAAMG,OAAOC,aAAa,IAAMP,GAAKA,EAAI,OAC3EI,QAAQ,IAAIC,OAAO,OAASF,KAAKK,cAAe,KAAMF,OAAOC,aAAa,IAAMP,GAAKA,EAAI,UAIjGA,EAAI,EAAGA,EAAI,GAAIA,IACnBD,KAAOA,KAAKK,QAAQ,IAAIC,OAAO,IAAML,EAAG,KAAMM,OAAOC,aAAa,KAAOP,WAE7ED,KAAOA,KAAKK,QAAQ,IAAIC,OAAO,KAAM,KAAMC,OAAOC,aAAa,QAInEZ,KAAKE,UAAUY,UAAY,SAASC,EAAGC,EAAGC,EAAGC,WAGrCC,GAAKC,KAAKC,IAAIH,OACdI,GAAKF,KAAKG,IAAIL,OAClBH,EAAES,YACFT,EAAEU,OAAOT,EAAGC,GACZF,EAAEW,OAAOV,EAAI,EAAIG,GAAK,EAAIG,GAAIL,EAAI,EAAIK,GAAK,EAAIH,IAC/CJ,EAAEW,OAAOV,EAAI,EAAIG,GAAK,EAAIG,GAAIL,EAAI,EAAIK,GAAK,EAAIH,IAC/CJ,EAAEY,QAGN3B,KAAKE,UAAU0B,IAAM,SAASC,EAAGC,EAAGf,EAAGgB,EAAGC,EAAGC,EAAGC,EAAGC,EAAG9B,UAE3CwB,EAAIG,EAAI3B,EAAIyB,EAAIG,EAAIC,EAAInB,EAAIgB,EAAII,EAAIN,EAAII,EAAIE,EAAIL,EAAIC,EAAI1B,EAAIU,EAAIiB,EAAIE,GAG/ElC,KAAKE,UAAUkC,gBAAkB,SAASC,UAE/BjB,KAAKkB,KAAKD,EAAErB,EAAIqB,EAAErB,EAAIqB,EAAEpB,EAAIoB,EAAEpB,IAGzCjB,KAAKE,UAAUqC,iBAAmB,SAASV,EAAGC,UAElCD,EAAEb,EAAIc,EAAEd,EAAIa,EAAEZ,EAAIa,EAAEb,GAAKX,KAAK8B,gBAAgBN,IAG1D9B,KAAKE,UAAUsC,MAAQ,SAASX,EAAGC,UAEvBD,EAAEb,EAAIc,EAAEb,EAAMa,EAAEd,EAAIa,EAAEZ,EAAK,GAGvCjB,KAAKE,UAAUuC,sBAAwB,SAASC,GAAIC,GAAIC,GAAIC,GAAIC,GAAIC,QAE5DlB,EAAIvB,KAAKsB,IAAIc,GAAIC,GAAI,EAAGC,GAAIC,GAAI,EAAGC,GAAIC,GAAI,GAC3CC,IAAM1C,KAAKsB,IAAIc,GAAKA,GAAKC,GAAKA,GAAIA,GAAI,EAAGC,GAAKA,GAAKC,GAAKA,GAAIA,GAAI,EAAGC,GAAKA,GAAKC,GAAKA,GAAIA,GAAI,GAC1FE,GAAK3C,KAAKsB,IAAIc,GAAKA,GAAKC,GAAKA,GAAID,GAAI,EAAGE,GAAKA,GAAKC,GAAKA,GAAID,GAAI,EAAGE,GAAKA,GAAKC,GAAKA,GAAID,GAAI,GACzF/B,GAAKT,KAAKsB,IAAIc,GAAKA,GAAKC,GAAKA,GAAID,GAAIC,GAAIC,GAAKA,GAAKC,GAAKA,GAAID,GAAIC,GAAIC,GAAKA,GAAKC,GAAKA,GAAID,GAAIC,UACxF,IACGC,IAAM,EAAInB,MACVoB,IAAM,EAAIpB,UACNT,KAAKkB,KAAKU,GAAKA,GAAKC,GAAKA,GAAK,EAAIpB,EAAId,IAAM,EAAIK,KAAK8B,IAAIrB,MAI3E7B,KAAKE,UAAUiD,SAAW,SAASC,IAAKC,aAE7BD,IAAIpC,EAAIqC,KAAKrC,GAAKoC,IAAIpC,EAAIqC,KAAKrC,EAAIqC,KAAKC,OAASF,IAAInC,EAAIoC,KAAKpC,EAAIoC,KAAKE,QAAUH,IAAInC,EAAIoC,KAAKpC,GAGzGjB,KAAKE,UAAUsD,gBAAkB,SAASxB,UAEtCA,EAAIA,GAAKyB,OAAOC,OACPC,OAAS3B,EAAE4B,SAIxB5D,KAAKE,UAAU2D,6BAA+B,SAAS7B,SAI7CqB,KAAOrB,EAAE8B,OAAOC,8BAGf,GAFG/B,EAAEgC,QAAUX,KAAKY,OACjBjC,EAAEkC,QAAUb,KAAKc,MAIxB,IAAInE"}
\ No newline at end of file
+{"version":3,"file":"graphutil.min.js","sources":["../src/graphutil.js"],"sourcesContent":["/***********************************************************************\n *\n * Utility functions/data/constants for the ui_graph module.\n *\n ***********************************************************************/\n// Most of this code is taken from Finite State Machine Designer:\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(function() {\n /**\n * Contstructor for the Util class.\n */\n function Util() {\n\n this.greekLetterNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',\n 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda',\n 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',\n 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega' ];\n }\n\n Util.prototype.convertLatexShortcuts = function(text) {\n // Html greek characters.\n for(var i = 0; i < this.greekLetterNames.length; i++) {\n var name = this.greekLetterNames[i];\n text = text.replace(new RegExp('\\\\\\\\' + name, 'g'), String.fromCharCode(913 + i + (i > 16)));\n text = text.replace(new RegExp('\\\\\\\\' + name.toLowerCase(), 'g'), String.fromCharCode(945 + i + (i > 16)));\n }\n\n // Subscripts.\n for(var i = 0; i < 10; i++) {\n text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));\n }\n text = text.replace(new RegExp('_a', 'g'), String.fromCharCode(8336));\n return text;\n };\n\n Util.prototype.drawArrow = function(c, x, y, angle) {\n // Draw an arrow head on the graphics context c at (x, y) with given angle.\n\n var dx = Math.cos(angle);\n var dy = Math.sin(angle);\n c.beginPath();\n c.moveTo(x, y);\n c.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);\n c.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);\n c.fill();\n };\n\n Util.prototype.det = function(a, b, c, d, e, f, g, h, i) {\n // Determinant of given matrix elements.\n return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g;\n };\n\n Util.prototype.vectorMagnitude = function(v){\n // Returns magnitude (length) of a vector v\n return Math.sqrt(v.x * v.x + v.y * v.y);\n };\n\n Util.prototype.scalarProjection = function(a, b) {\n // Returns scalar projection of vector a onto vector b\n return (a.x * b.x + a.y * b.y) / this.vectorMagnitude(b);\n };\n\n Util.prototype.isCCW = function(a, b) {\n // Returns true iff vector b is in a counter-clockwise orientation relative to a\n return (a.x * b.y) - (b.x * a.y) > 0;\n };\n\n Util.prototype.circleFromThreePoints = function(x1, y1, x2, y2, x3, y3) {\n // Return {x, y, radius} of circle through (x1, y1), (x2, y2), (x3, y3).\n var a = this.det(x1, y1, 1, x2, y2, 1, x3, y3, 1);\n var bx = -this.det(x1 * x1 + y1 * y1, y1, 1, x2 * x2 + y2 * y2, y2, 1, x3 * x3 + y3 * y3, y3, 1);\n var by = this.det(x1 * x1 + y1 * y1, x1, 1, x2 * x2 + y2 * y2, x2, 1, x3 * x3 + y3 * y3, x3, 1);\n var c = -this.det(x1 * x1 + y1 * y1, x1, y1, x2 * x2 + y2 * y2, x2, y2, x3 * x3 + y3 * y3, x3, y3);\n return {\n 'x': -bx / (2 * a),\n 'y': -by / (2 * a),\n 'radius': Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))\n };\n };\n\n Util.prototype.isInside = function(pos, rect) {\n // True iff given point pos is inside rectangle.\n return pos.x > rect.x && pos.x < rect.x + rect.width && pos.y < rect.y + rect.height && pos.y > rect.y;\n };\n\n Util.prototype.crossBrowserKey = function(e) {\n // Return which key was pressed, given the event, in a browser-independent way.\n e = e || window.event;\n return e.which || e.keyCode;\n };\n\n\n Util.prototype.crossBrowserRelativeMousePos = function(e) {\n // Earlier complex version was breaking in Moodle 4, so replaced\n // with this much simpler version that should work with all modern\n // browsers.\n const rect = e.target.getBoundingClientRect();\n const x = e.clientX - rect.left; //x position within the element.\n const y = e.clientY - rect.top; //y position within the element.\n return {'x': x, 'y': y};\n };\n\n return new Util();\n});\n"],"names":["define","Util","greekLetterNames","prototype","convertLatexShortcuts","text","i","this","length","name","replace","RegExp","String","fromCharCode","toLowerCase","drawArrow","c","x","y","angle","dx","Math","cos","dy","sin","beginPath","moveTo","lineTo","fill","det","a","b","d","e","f","g","h","vectorMagnitude","v","sqrt","scalarProjection","isCCW","circleFromThreePoints","x1","y1","x2","y2","x3","y3","bx","by","abs","isInside","pos","rect","width","height","crossBrowserKey","window","event","which","keyCode","crossBrowserRelativeMousePos","target","getBoundingClientRect","clientX","left","clientY","top"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/textareas.min.js b/amd/build/textareas.min.js
index f7c2df89a..3826a6463 100644
--- a/amd/build/textareas.min.js
+++ b/amd/build/textareas.min.js
@@ -6,6 +6,6 @@
* @copyright Richard Lobb, 2015, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/textareas",["jquery"],(function($){function initTextArea(){$(this).data("clickInProgress",!1),$(this).data("capturingTab",!0),$(this).on("mousedown",(function(){$(this).data("clickInProgress",!0)})),$(this).on("focusin",(function(){$(this).data("capturingTab",$(this).data("clickInProgress"))})),$(this).on("click",(function(){$(this).data("clickInProgress",!1)})),$(this).on("keydown",(function(e){if(!(window.hasOwnProperty("behattesting")&&window.behattesting||void 0!==e.which&&0===e.which))if(9==e.keyCode&&$(this).data("capturingTab"))(e.shiftKey||insertString(this," "))&&e.preventDefault();else if(13===e.keyCode&&void 0!==this.selectionStart){for(var before=this.value.substring(0,this.selectionStart),eol=before.lastIndexOf("\n"),line=before.substring(eol+1),indent="",i=0;i{div.is(":visible")?(div.hide(),hdr.innerText=hdrText.replace("▾","▸")):(div.show(),div.trigger("mousemove"),hdr.innerText=hdrText.replace("▸","▾"))}))}}}));
+define("qtype_coderunner/textareas",["jquery"],(function($){function initTextArea(){$(this).data("clickInProgress",!1),$(this).data("capturingTab",!0),$(this).on("mousedown",(function(){$(this).data("clickInProgress",!0)})),$(this).on("focusin",(function(){$(this).data("capturingTab",$(this).data("clickInProgress"))})),$(this).on("click",(function(){$(this).data("clickInProgress",!1)})),$(this).on("keydown",(function(e){if(!(window.hasOwnProperty("behattesting")&&window.behattesting||void 0!==e.which&&0===e.which))if(9==e.keyCode&&$(this).data("capturingTab"))(e.shiftKey||insertString(this," "))&&e.preventDefault();else if(13===e.keyCode&&void 0!==this.selectionStart){for(var before=this.value.substring(0,this.selectionStart),eol=before.lastIndexOf("\n"),line=before.substring(eol+1),indent="",i=0;i.\n */\n\n/**\n * JavaScript for handling textareas and form actions in CodeRunner question\n * editing forms and student question answering forms.\n *\n * @module qtype_coderunner/textareas\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Function to initialise all code-input text-areas in a page.\n * Used by the form editor but can't be used for question text areas as\n * renderer.php is called once for each question in a quiz, and there is\n * no communication between the questions.\n */\n function setupAllTAs() {\n $('textarea.edit_code').each(initTextArea);\n }\n\n /**\n * Initialise a particular text area (TA), given its ID (which may contain\n * colons, so can't use jQuery selector).\n * @param {string} taId The ID of the textarea html element.\n */\n function initQuestionTA(taId) {\n $(document.getElementById(taId)).each(initTextArea);\n }\n\n\n /**\n * Set up to expand or collapse the question author's answer when the user clicks\n * on the show/hide answer button.\n * @param {string} linkId The ID of the link that the user clicks.\n * @param {sting} divId The ID of the div to be shown or hidden.\n */\n function setupShowHideAnswer(linkId, divId) {\n let link = $(document.getElementById(linkId)),\n div = $(document.getElementById(divId)),\n hdr = link.children()[0], // The
element.\n hdrText = hdr.innerText; // Its body.\n link.click(() => {\n if (div.is(\":visible\")) {\n div.hide();\n hdr.innerText = hdrText.replace(\"\\u25BE\", \"\\u25B8\");\n } else {\n div.show();\n div.trigger('mousemove'); // So user sees contents.\n hdr.innerText = hdrText.replace(\"\\u25B8\", \"\\u25BE\");\n }\n });\n }\n\n /**\n * Set up the JavaScript to handle a text area 'this'.\n * It just does rudimentary autoindent on return and replaces tabs with\n * 4 spaces always.\n * For info on key handling browser inconsistencies see\n * http://unixpapa.com/js/key.html\n * If the AceEditor is handling a text area, this code is unused as\n * the actual textarea is hidden by Ace.\n */\n function initTextArea() {\n var TAB = 9,\n ENTER = 13,\n ESC = 27,\n KEY_M = 77;\n\n $(this).data('clickInProgress', false);\n $(this).data('capturingTab', true);\n\n $(this).on('mousedown', function() {\n /*\n * Event order seems to be (\\ is where the mouse button is pressed, / released):\n * Chrome: \\ mousedown, mouseup, focusin / click.\n * Firefox/IE: \\ mousedown, focusin / mouseup, click.\n */\n $(this).data('clickInProgress', true);\n });\n\n $(this).on('focusin', function() {\n /*\n * At first, pressing TAB moves focus.\n */\n $(this).data('capturingTab', $(this).data('clickInProgress'));\n });\n\n $(this).on('click', function() {\n $(this).data('clickInProgress', false);\n });\n\n $(this).on('keydown', function(e) {\n /*\n * Don't autoindent when behat testing in progress.\n */\n if (window.hasOwnProperty('behattesting') && window.behattesting) { return; }\n\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode == TAB && $(this).data('capturingTab')) {\n /*\n * Ignore SHIFT/TAB. Insert 4 spaces on TAB.\n */\n if (e.shiftKey || insertString(this, \" \")) {\n e.preventDefault();\n }\n }\n else if (e.keyCode === ENTER && this.selectionStart !== undefined) {\n /*\n * Handle autoindent only on non-IE.\n */\n var before = this.value.substring(0, this.selectionStart);\n var eol = before.lastIndexOf(\"\\n\");\n var line = before.substring(eol + 1); // Take from eol to end.\n var indent = \"\";\n for (var i = 0; i < line.length && line.charAt(i) === ' '; i++) {\n indent = indent + \" \";\n }\n if (insertString(this, \"\\n\" + indent)) {\n e.preventDefault();\n }\n /*\n * Once the user has started typing, TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n else if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n /*\n * CTRL + M toggles TAB capturing mode.\n * This is the short-cut recommended by\n * https:www.w3.org/TR/wai-aria-practices/#richtext.\n */\n $(this).data('capturingTab', !$(this).data('capturingTab'));\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n /*\n * ESC always stops capturing TAB.\n */\n $(this).data('capturingTab', false);\n }\n else if (!(e.ctrlKey || e.altKey)) {\n /*\n * Once the user has started typing (not modifier keys) TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n }\n });\n }\n\n /**\n * Insert into the given textarea ta the given string sToInsert.\n * @param {html_element} ta The textarea to be updated.\n * @param {string} sToInsert The string to be inserted at the current selection\n * point.\n */\n function insertString(ta, sToInsert) {\n if (ta.selectionStart !== undefined) { // Firefox etc.\n var before = ta.value.substring(0, ta.selectionStart);\n var selSave = ta.selectionEnd;\n var after = ta.value.substring(ta.selectionEnd, ta.value.length);\n\n /**\n * Update the text field.\n */\n var tmp = ta.scrollTop; // Inhibit annoying auto-scroll.\n ta.value = before + sToInsert + after;\n var pos = selSave + sToInsert.length;\n ta.selectionStart = pos;\n ta.selectionEnd = pos;\n ta.scrollTop = tmp;\n return true;\n\n }\n else if (document.selection && document.selection.createRange) { // IE.\n /*\n * TODO: check if this still works. OLD CODE.\n */\n var r = document.selection.createRange();\n var dr = r.duplicate();\n dr.moveToElementText(ta);\n dr.setEndPoint(\"EndToEnd\", r);\n r.text = sToInsert;\n return true;\n }\n /*\n * Other browsers we can't handle.\n */\n else {\n return false;\n }\n }\n\n return {\n setupAllTAs: setupAllTAs,\n initQuestionTA: initQuestionTA,\n setupShowHideAnswer: setupShowHideAnswer,\n };\n});\n"],"names":["define","$","initTextArea","this","data","on","e","window","hasOwnProperty","behattesting","undefined","which","keyCode","shiftKey","insertString","preventDefault","selectionStart","before","value","substring","eol","lastIndexOf","line","indent","i","length","charAt","ctrlKey","altKey","ta","sToInsert","selSave","selectionEnd","after","tmp","scrollTop","pos","document","selection","createRange","r","dr","duplicate","moveToElementText","setEndPoint","text","setupAllTAs","each","initQuestionTA","taId","getElementById","setupShowHideAnswer","linkId","divId","link","div","hdr","children","hdrText","innerText","click","is","hide","replace","show","trigger"],"mappings":";;;;;;;;AA2BAA,oCAAO,CAAC,WAAW,SAASC,YAsDfC,eAMLD,EAAEE,MAAMC,KAAK,mBAAmB,GAChCH,EAAEE,MAAMC,KAAK,gBAAgB,GAE7BH,EAAEE,MAAME,GAAG,aAAa,WAMpBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,WAIlBJ,EAAEE,MAAMC,KAAK,eAAgBH,EAAEE,MAAMC,KAAK,uBAG9CH,EAAEE,MAAME,GAAG,SAAS,WAChBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,SAASC,QAIvBC,OAAOC,eAAe,iBAAmBD,OAAOE,mBAEpCC,IAAZJ,EAAEK,OAAmC,IAAZL,EAAEK,UAlCzB,GAmCEL,EAAEM,SAAkBX,EAAEE,MAAMC,KAAK,iBAI7BE,EAAEO,UAAYC,aAAaX,KAAM,UACjCG,EAAES,sBAGL,GA1CD,KA0CKT,EAAEM,cAA6CF,IAAxBP,KAAKa,eAA8B,SAI3DC,OAASd,KAAKe,MAAMC,UAAU,EAAGhB,KAAKa,gBACtCI,IAAMH,OAAOI,YAAY,MACzBC,KAAOL,OAAOE,UAAUC,IAAM,GAC9BG,OAAS,GACJC,EAAI,EAAGA,EAAIF,KAAKG,QAA6B,MAAnBH,KAAKI,OAAOF,GAAYA,IACvDD,QAAkB,IAElBT,aAAaX,KAAM,KAAOoB,SAC1BjB,EAAES,iBAKNd,EAAEE,MAAMC,KAAK,gBAAgB,QAzD7B,KA2DKE,EAAEM,SAAqBN,EAAEqB,UAAYrB,EAAEsB,QAM5C3B,EAAEE,MAAMC,KAAK,gBAAiBH,EAAEE,MAAMC,KAAK,iBAC3CE,EAAES,kBAnEJ,KAqEOT,EAAEM,QAIPX,EAAEE,MAAMC,KAAK,gBAAgB,GAEtBE,EAAEqB,SAAWrB,EAAEsB,QAItB3B,EAAEE,MAAMC,KAAK,gBAAgB,eAYpCU,aAAae,GAAIC,mBACIpB,IAAtBmB,GAAGb,eAA8B,KAC7BC,OAASY,GAAGX,MAAMC,UAAU,EAAGU,GAAGb,gBAClCe,QAAUF,GAAGG,aACbC,MAAQJ,GAAGX,MAAMC,UAAUU,GAAGG,aAAcH,GAAGX,MAAMO,QAKrDS,IAAML,GAAGM,UACbN,GAAGX,MAAQD,OAASa,UAAYG,UAC5BG,IAAML,QAAUD,UAAUL,cAC9BI,GAAGb,eAAiBoB,IACpBP,GAAGG,aAAeI,IAClBP,GAAGM,UAAYD,KACR,EAGN,GAAIG,SAASC,WAAaD,SAASC,UAAUC,YAAa,KAIvDC,EAAIH,SAASC,UAAUC,cACvBE,GAAKD,EAAEE,mBACXD,GAAGE,kBAAkBd,IACrBY,GAAGG,YAAY,WAAYJ,GAC3BA,EAAEK,KAAOf,WACF,SAMA,QAIR,CACHgB,uBAjLA7C,EAAE,sBAAsB8C,KAAK7C,eAkL7B8C,wBA1KoBC,MACpBhD,EAAEoC,SAASa,eAAeD,OAAOF,KAAK7C,eA0KtCiD,6BAhKyBC,OAAQC,WAC7BC,KAAOrD,EAAEoC,SAASa,eAAeE,SACjCG,IAAMtD,EAAEoC,SAASa,eAAeG,QAChCG,IAAMF,KAAKG,WAAW,GACtBC,QAAUF,IAAIG,UAClBL,KAAKM,OAAM,KACHL,IAAIM,GAAG,aACPN,IAAIO,OACJN,IAAIG,UAAYD,QAAQK,QAAQ,IAAU,OAE1CR,IAAIS,OACJT,IAAIU,QAAQ,aACZT,IAAIG,UAAYD,QAAQK,QAAQ,IAAU"}
\ No newline at end of file
+{"version":3,"file":"textareas.min.js","sources":["../src/textareas.js"],"sourcesContent":["/**\n * 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/**\n * JavaScript for handling textareas and form actions in CodeRunner question\n * editing forms and student question answering forms.\n *\n * @module qtype_coderunner/textareas\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Function to initialise all code-input text-areas in a page.\n * Used by the form editor but can't be used for question text areas as\n * renderer.php is called once for each question in a quiz, and there is\n * no communication between the questions.\n */\n function setupAllTAs() {\n $('textarea.edit_code').each(initTextArea);\n }\n\n /**\n * Initialise a particular text area (TA), given its ID (which may contain\n * colons, so can't use jQuery selector).\n * @param {string} taId The ID of the textarea html element.\n */\n function initQuestionTA(taId) {\n $(document.getElementById(taId)).each(initTextArea);\n }\n\n\n /**\n * Set up to expand or collapse the question author's answer when the user clicks\n * on the show/hide answer button.\n * @param {string} linkId The ID of the link that the user clicks.\n * @param {sting} divId The ID of the div to be shown or hidden.\n */\n function setupShowHideAnswer(linkId, divId) {\n let link = $(document.getElementById(linkId)),\n div = $(document.getElementById(divId)),\n hdr = link.children()[0], // The
element.\n hdrText = hdr.innerText; // Its body.\n link.click(() => {\n if (div.is(\":visible\")) {\n div.hide();\n hdr.innerText = hdrText.replace(\"\\u25BE\", \"\\u25B8\");\n } else {\n div.show();\n div.trigger('mousemove'); // So user sees contents.\n hdr.innerText = hdrText.replace(\"\\u25B8\", \"\\u25BE\");\n }\n });\n }\n\n /**\n * Set up the JavaScript to handle a text area 'this'.\n * It just does rudimentary autoindent on return and replaces tabs with\n * 4 spaces always.\n * For info on key handling browser inconsistencies see\n * http://unixpapa.com/js/key.html\n * If the AceEditor is handling a text area, this code is unused as\n * the actual textarea is hidden by Ace.\n */\n function initTextArea() {\n var TAB = 9,\n ENTER = 13,\n ESC = 27,\n KEY_M = 77;\n\n $(this).data('clickInProgress', false);\n $(this).data('capturingTab', true);\n\n $(this).on('mousedown', function() {\n /*\n * Event order seems to be (\\ is where the mouse button is pressed, / released):\n * Chrome: \\ mousedown, mouseup, focusin / click.\n * Firefox/IE: \\ mousedown, focusin / mouseup, click.\n */\n $(this).data('clickInProgress', true);\n });\n\n $(this).on('focusin', function() {\n /*\n * At first, pressing TAB moves focus.\n */\n $(this).data('capturingTab', $(this).data('clickInProgress'));\n });\n\n $(this).on('click', function() {\n $(this).data('clickInProgress', false);\n });\n\n $(this).on('keydown', function(e) {\n /*\n * Don't autoindent when behat testing in progress.\n */\n if (window.hasOwnProperty('behattesting') && window.behattesting) { return; }\n\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode == TAB && $(this).data('capturingTab')) {\n /*\n * Ignore SHIFT/TAB. Insert 4 spaces on TAB.\n */\n if (e.shiftKey || insertString(this, \" \")) {\n e.preventDefault();\n }\n }\n else if (e.keyCode === ENTER && this.selectionStart !== undefined) {\n /*\n * Handle autoindent only on non-IE.\n */\n var before = this.value.substring(0, this.selectionStart);\n var eol = before.lastIndexOf(\"\\n\");\n var line = before.substring(eol + 1); // Take from eol to end.\n var indent = \"\";\n for (var i = 0; i < line.length && line.charAt(i) === ' '; i++) {\n indent = indent + \" \";\n }\n if (insertString(this, \"\\n\" + indent)) {\n e.preventDefault();\n }\n /*\n * Once the user has started typing, TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n else if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n /*\n * CTRL + M toggles TAB capturing mode.\n * This is the short-cut recommended by\n * https:www.w3.org/TR/wai-aria-practices/#richtext.\n */\n $(this).data('capturingTab', !$(this).data('capturingTab'));\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n /*\n * ESC always stops capturing TAB.\n */\n $(this).data('capturingTab', false);\n }\n else if (!(e.ctrlKey || e.altKey)) {\n /*\n * Once the user has started typing (not modifier keys) TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n }\n });\n }\n\n /**\n * Insert into the given textarea ta the given string sToInsert.\n * @param {html_element} ta The textarea to be updated.\n * @param {string} sToInsert The string to be inserted at the current selection\n * point.\n */\n function insertString(ta, sToInsert) {\n if (ta.selectionStart !== undefined) { // Firefox etc.\n var before = ta.value.substring(0, ta.selectionStart);\n var selSave = ta.selectionEnd;\n var after = ta.value.substring(ta.selectionEnd, ta.value.length);\n\n /**\n * Update the text field.\n */\n var tmp = ta.scrollTop; // Inhibit annoying auto-scroll.\n ta.value = before + sToInsert + after;\n var pos = selSave + sToInsert.length;\n ta.selectionStart = pos;\n ta.selectionEnd = pos;\n ta.scrollTop = tmp;\n return true;\n\n }\n else if (document.selection && document.selection.createRange) { // IE.\n /*\n * TODO: check if this still works. OLD CODE.\n */\n var r = document.selection.createRange();\n var dr = r.duplicate();\n dr.moveToElementText(ta);\n dr.setEndPoint(\"EndToEnd\", r);\n r.text = sToInsert;\n return true;\n }\n /*\n * Other browsers we can't handle.\n */\n else {\n return false;\n }\n }\n\n return {\n setupAllTAs: setupAllTAs,\n initQuestionTA: initQuestionTA,\n setupShowHideAnswer: setupShowHideAnswer,\n };\n});\n"],"names":["define","$","initTextArea","this","data","on","e","window","hasOwnProperty","behattesting","undefined","which","keyCode","shiftKey","insertString","preventDefault","selectionStart","before","value","substring","eol","lastIndexOf","line","indent","i","length","charAt","ctrlKey","altKey","ta","sToInsert","selSave","selectionEnd","after","tmp","scrollTop","pos","document","selection","createRange","r","dr","duplicate","moveToElementText","setEndPoint","text","setupAllTAs","each","initQuestionTA","taId","getElementById","setupShowHideAnswer","linkId","divId","link","div","hdr","children","hdrText","innerText","click","is","hide","replace","show","trigger"],"mappings":";;;;;;;;AA2BAA,oCAAO,CAAC,WAAW,SAASC,YAsDfC,eAMLD,EAAEE,MAAMC,KAAK,mBAAmB,GAChCH,EAAEE,MAAMC,KAAK,gBAAgB,GAE7BH,EAAEE,MAAME,GAAG,aAAa,WAMpBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,WAIlBJ,EAAEE,MAAMC,KAAK,eAAgBH,EAAEE,MAAMC,KAAK,uBAG9CH,EAAEE,MAAME,GAAG,SAAS,WAChBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,SAASC,QAIvBC,OAAOC,eAAe,iBAAmBD,OAAOE,mBAEpCC,IAAZJ,EAAEK,OAAmC,IAAZL,EAAEK,UAlCzB,GAmCEL,EAAEM,SAAkBX,EAAEE,MAAMC,KAAK,iBAI7BE,EAAEO,UAAYC,aAAaX,KAAM,UACjCG,EAAES,sBAGL,GA1CD,KA0CKT,EAAEM,cAA6CF,IAAxBP,KAAKa,eAA8B,SAI3DC,OAASd,KAAKe,MAAMC,UAAU,EAAGhB,KAAKa,gBACtCI,IAAMH,OAAOI,YAAY,MACzBC,KAAOL,OAAOE,UAAUC,IAAM,GAC9BG,OAAS,GACJC,EAAI,EAAGA,EAAIF,KAAKG,QAA6B,MAAnBH,KAAKI,OAAOF,GAAYA,IACvDD,QAAkB,IAElBT,aAAaX,KAAM,KAAOoB,SAC1BjB,EAAES,iBAKNd,EAAEE,MAAMC,KAAK,gBAAgB,QAzD7B,KA2DKE,EAAEM,SAAqBN,EAAEqB,UAAYrB,EAAEsB,QAM5C3B,EAAEE,MAAMC,KAAK,gBAAiBH,EAAEE,MAAMC,KAAK,iBAC3CE,EAAES,kBAnEJ,KAqEOT,EAAEM,QAIPX,EAAEE,MAAMC,KAAK,gBAAgB,GAEtBE,EAAEqB,SAAWrB,EAAEsB,QAItB3B,EAAEE,MAAMC,KAAK,gBAAgB,eAYpCU,aAAae,GAAIC,mBACIpB,IAAtBmB,GAAGb,eAA8B,KAC7BC,OAASY,GAAGX,MAAMC,UAAU,EAAGU,GAAGb,gBAClCe,QAAUF,GAAGG,aACbC,MAAQJ,GAAGX,MAAMC,UAAUU,GAAGG,aAAcH,GAAGX,MAAMO,QAKrDS,IAAML,GAAGM,UACbN,GAAGX,MAAQD,OAASa,UAAYG,UAC5BG,IAAML,QAAUD,UAAUL,cAC9BI,GAAGb,eAAiBoB,IACpBP,GAAGG,aAAeI,IAClBP,GAAGM,UAAYD,KACR,EAGN,GAAIG,SAASC,WAAaD,SAASC,UAAUC,YAAa,KAIvDC,EAAIH,SAASC,UAAUC,cACvBE,GAAKD,EAAEE,mBACXD,GAAGE,kBAAkBd,IACrBY,GAAGG,YAAY,WAAYJ,GAC3BA,EAAEK,KAAOf,WACF,SAMA,QAIR,CACHgB,uBAjLA7C,EAAE,sBAAsB8C,KAAK7C,eAkL7B8C,wBA1KoBC,MACpBhD,EAAEoC,SAASa,eAAeD,OAAOF,KAAK7C,eA0KtCiD,6BAhKyBC,OAAQC,WAC7BC,KAAOrD,EAAEoC,SAASa,eAAeE,SACjCG,IAAMtD,EAAEoC,SAASa,eAAeG,QAChCG,IAAMF,KAAKG,WAAW,GACtBC,QAAUF,IAAIG,UAClBL,KAAKM,OAAM,WACHL,IAAIM,GAAG,aACPN,IAAIO,OACJN,IAAIG,UAAYD,QAAQK,QAAQ,IAAU,OAE1CR,IAAIS,OACJT,IAAIU,QAAQ,aACZT,IAAIG,UAAYD,QAAQK,QAAQ,IAAU"}
\ No newline at end of file
diff --git a/amd/build/ui_ace_gapfiller.min.js b/amd/build/ui_ace_gapfiller.min.js
index 3cf7ed422..fa7b24b07 100644
--- a/amd/build/ui_ace_gapfiller.min.js
+++ b/amd/build/ui_ace_gapfiller.min.js
@@ -38,6 +38,6 @@
* @copyright Matthew Toohey, 2021, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_ace_gapfiller",["jquery"],(function($){var Range;const validChars=/[ !"#$%&'()*+,`\-./0-9:;<=>?@A-Z\[\]\\^_a-z{}|~]/;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this;let code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){let cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){let nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,selectionRange.start,char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){let maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+1=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(let i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(let i=start;i?@A-Z\[\]\\^_a-z{}|~]/;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this,code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){var cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){var nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){var char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){var _char=e.args;validChars.test(_char)&&gap.insertChar(t.gaps,selectionRange.start,_char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){var maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+1=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(var i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(var i=start;i.\n\n/**\n * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","matchMedia","matches","setTheme","theme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_ace_gapfiller.min.js","sources":["../src/ui_ace_gapfiller.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 util.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 * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","matchMedia","matches","setTheme","theme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/userinterfacewrapper.min.js b/amd/build/userinterfacewrapper.min.js
index 0825142de..09ae46ac3 100644
--- a/amd/build/userinterfacewrapper.min.js
+++ b/amd/build/userinterfacewrapper.min.js
@@ -96,6 +96,6 @@
* 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)
*
*****************************************************************************/
-define("qtype_coderunner/userinterfacewrapper",["jquery"],(function($){function InterfaceWrapper(uiname,textareaId){let t=this;this.GUTTER=14,this.DEFAULT_SYNC_INTERVAL_SECS=5;this.taId=textareaId,this.loadFailId=textareaId+"_loadfailerr";const ta=document.getElementById(textareaId);this.textArea=$(ta);const params=this.textArea.attr("data-params");this.uiParams=params?JSON.parse(params):{},this.uiParams.lang=this.textArea.attr("data-lang"),this.readOnly=this.textArea.prop("readonly"),this.isLoading=!1,this.loadFailed=!1,this.retries=0;let h=parseInt(this.textArea.css("height")),content_lines=this.textArea.val().split("\n").length,rows=ta.rows;content_lines>rows&&(rows=Math.min(content_lines,50)),h=Math.max(h,19*rows,50),this.wrapperNode=$(""),this.textArea.after(this.wrapperNode),this.wrapperNode.hide(),this.wrapperNode.css({resize:"vertical",overflow:"hidden",minHeight:h,width:"100%",border:"1px solid darkgrey"}),this.textArea.data("current-ui-wrapper",this),this.uiInstance=null,this.loadUi(uiname,this.uiParams),$(document).mousemove((function(){t.checkForResize()})),$(window).resize((function(){t.checkForResize()})),this.textArea.closest("form").submit((function(){null!==t.uiInstance&&t.uiInstance.sync()})),$(document.body).on("keydown",(function(e){77===e.keyCode&&e.ctrlKey&&e.altKey&&(null!==t.uiInstance||t.loadFailed?t.stop():t.restart())}))}return InterfaceWrapper.prototype.loadUi=function(uiname,params){const t=this;function syncIntervalSecsBase(){return params.hasOwnProperty("sync_interval_secs")?parseInt(params.sync_interval_secs):t.DEFAULT_SYNC_INTERVAL_SECS}if(this.isLoading)return this.retries+=1,void(this.retries>20?(alert("Failed to load "+uiname+" UI component. If this error persists, please report it to the forum on coderunner.org.nz"),this.retries=0,this.loading=0):setTimeout((function(){t.loadUi(uiname,params)}),200));this.retries=0,this.params=params,this.stop(),this.uiname=uiname,""===this.uiname||"none"===this.uiname||sessionStorage.getItem("disableUis")?this.uiInstance=null:(this.isLoading=!0,require(["qtype_coderunner/ui_"+this.uiname],(function(ui){const h=t.wrapperNode.innerHeight()-t.GUTTER,w=t.wrapperNode.innerWidth(),uiInstance=new ui.Constructor(t.taId,w,h,params);if(uiInstance.failed()){t.loadFailed=!0,t.wrapperNode.hide(),uiInstance.destroy(),t.uiInstance=null,t.textArea.addClass("uiloadfailed");const loadFailDiv='';let jqLoadFailDiv=$(loadFailDiv);jqLoadFailDiv.insertBefore(t.textArea),langString=uiInstance.failMessage(),errorDiv=jqLoadFailDiv,require(["core/str"],(function(str){const s=str.get_string(langString,"qtype_coderunner"),fallback=str.get_string("ui_fallback","qtype_coderunner");$.when(s,fallback).done((function(s,fallback){errorDiv.html(s+" "+fallback)}))}))}else{t.hLast=0,t.wLast=0,t.textArea.hide(),t.wrapperNode.show(),t.wrapperNode.append(uiInstance.getElement()),t.uiInstance=uiInstance,t.loadFailed=!1,t.checkForResize();let uiInstancePrototype=Object.getPrototypeOf(uiInstance);uiInstancePrototype.syncIntervalSecs=uiInstancePrototype.syncIntervalSecs||syncIntervalSecsBase,t.startSyncTimer(uiInstance)}var langString,errorDiv;t.isLoading=!1})))},InterfaceWrapper.prototype.startSyncTimer=function(uiInstance){const timeout=uiInstance.syncIntervalSecs();this.uiInstance.timer=timeout?setInterval((function(){uiInstance.sync()}),1e3*timeout):null},InterfaceWrapper.prototype.stopSyncTimer=function(uiInstance){uiInstance.timer&&clearTimeout(uiInstance.timer)},InterfaceWrapper.prototype.stop=function(){null!==this.uiInstance&&(this.stopSyncTimer(this.uiInstance),this.textArea.show(),this.uiInstance.hasFocus()&&(this.textArea.focus(),this.textArea[0].selectionStart=this.textArea[0].value.length),this.uiInstance.destroy(),this.uiInstance=null,this.wrapperNode.hide()),this.loadFailed=!1,this.textArea.removeClass("uiloadfailed"),$(document.getElementById(this.loadFailId)).remove()},InterfaceWrapper.prototype.restart=function(){null===this.uiInstance&&this.loadUi(this.uiname,this.params)},InterfaceWrapper.prototype.checkForResize=function(){if(this.uiInstance){const h=this.wrapperNode.innerHeight(),w=this.wrapperNode.innerWidth();if(h!=this.hLast||w!=this.wLast){const xLeft=this.wrapperNode.offset().left,maxWidth=$(window).innerWidth()-xLeft-25,hAdjusted=h-this.GUTTER,wAdjusted=Math.min(maxWidth,w);this.uiInstance.resize(wAdjusted,hAdjusted),this.hLast=this.wrapperNode.innerHeight(),this.wLast=this.wrapperNode.innerWidth()}}},{newUiWrapper:function(uiname,textareaId){return uiname?new InterfaceWrapper(uiname,textareaId):null},InterfaceWrapper:InterfaceWrapper}}));
+define("qtype_coderunner/userinterfacewrapper",["jquery"],(function($){function InterfaceWrapper(uiname,textareaId){var t=this;this.GUTTER=14,this.DEFAULT_SYNC_INTERVAL_SECS=5;this.taId=textareaId,this.loadFailId=textareaId+"_loadfailerr";var ta=document.getElementById(textareaId);this.textArea=$(ta);var params=this.textArea.attr("data-params");this.uiParams=params?JSON.parse(params):{},this.uiParams.lang=this.textArea.attr("data-lang"),this.readOnly=this.textArea.prop("readonly"),this.isLoading=!1,this.loadFailed=!1,this.retries=0;var h=parseInt(this.textArea.css("height")),content_lines=this.textArea.val().split("\n").length,rows=ta.rows;content_lines>rows&&(rows=Math.min(content_lines,50)),h=Math.max(h,19*rows,50),this.wrapperNode=$(""),this.textArea.after(this.wrapperNode),this.wrapperNode.hide(),this.wrapperNode.css({resize:"vertical",overflow:"hidden",minHeight:h,width:"100%",border:"1px solid darkgrey"}),this.textArea.data("current-ui-wrapper",this),this.uiInstance=null,this.loadUi(uiname,this.uiParams),$(document).mousemove((function(){t.checkForResize()})),$(window).resize((function(){t.checkForResize()})),this.textArea.closest("form").submit((function(){null!==t.uiInstance&&t.uiInstance.sync()})),$(document.body).on("keydown",(function(e){77===e.keyCode&&e.ctrlKey&&e.altKey&&(null!==t.uiInstance||t.loadFailed?t.stop():t.restart())}))}return InterfaceWrapper.prototype.loadUi=function(uiname,params){var t=this;function syncIntervalSecsBase(){return params.hasOwnProperty("sync_interval_secs")?parseInt(params.sync_interval_secs):t.DEFAULT_SYNC_INTERVAL_SECS}if(this.isLoading)return this.retries+=1,void(this.retries>20?(alert("Failed to load "+uiname+" UI component. If this error persists, please report it to the forum on coderunner.org.nz"),this.retries=0,this.loading=0):setTimeout((function(){t.loadUi(uiname,params)}),200));this.retries=0,this.params=params,this.stop(),this.uiname=uiname,""===this.uiname||"none"===this.uiname||sessionStorage.getItem("disableUis")?this.uiInstance=null:(this.isLoading=!0,require(["qtype_coderunner/ui_"+this.uiname],(function(ui){var langString,errorDiv,h=t.wrapperNode.innerHeight()-t.GUTTER,w=t.wrapperNode.innerWidth(),uiInstance=new ui.Constructor(t.taId,w,h,params);if(uiInstance.failed()){t.loadFailed=!0,t.wrapperNode.hide(),uiInstance.destroy(),t.uiInstance=null,t.textArea.addClass("uiloadfailed");var loadFailDiv='',jqLoadFailDiv=$(loadFailDiv);jqLoadFailDiv.insertBefore(t.textArea),langString=uiInstance.failMessage(),errorDiv=jqLoadFailDiv,require(["core/str"],(function(str){var s=str.get_string(langString,"qtype_coderunner"),fallback=str.get_string("ui_fallback","qtype_coderunner");$.when(s,fallback).done((function(s,fallback){errorDiv.html(s+" "+fallback)}))}))}else{t.hLast=0,t.wLast=0,t.textArea.hide(),t.wrapperNode.show(),t.wrapperNode.append(uiInstance.getElement()),t.uiInstance=uiInstance,t.loadFailed=!1,t.checkForResize();var uiInstancePrototype=Object.getPrototypeOf(uiInstance);uiInstancePrototype.syncIntervalSecs=uiInstancePrototype.syncIntervalSecs||syncIntervalSecsBase,t.startSyncTimer(uiInstance)}t.isLoading=!1})))},InterfaceWrapper.prototype.startSyncTimer=function(uiInstance){var timeout=uiInstance.syncIntervalSecs();this.uiInstance.timer=timeout?setInterval((function(){uiInstance.sync()}),1e3*timeout):null},InterfaceWrapper.prototype.stopSyncTimer=function(uiInstance){uiInstance.timer&&clearTimeout(uiInstance.timer)},InterfaceWrapper.prototype.stop=function(){null!==this.uiInstance&&(this.stopSyncTimer(this.uiInstance),this.textArea.show(),this.uiInstance.hasFocus()&&(this.textArea.focus(),this.textArea[0].selectionStart=this.textArea[0].value.length),this.uiInstance.destroy(),this.uiInstance=null,this.wrapperNode.hide()),this.loadFailed=!1,this.textArea.removeClass("uiloadfailed"),$(document.getElementById(this.loadFailId)).remove()},InterfaceWrapper.prototype.restart=function(){null===this.uiInstance&&this.loadUi(this.uiname,this.params)},InterfaceWrapper.prototype.checkForResize=function(){if(this.uiInstance){var h=this.wrapperNode.innerHeight(),w=this.wrapperNode.innerWidth();if(h!=this.hLast||w!=this.wLast){var xLeft=this.wrapperNode.offset().left,maxWidth=$(window).innerWidth()-xLeft-25,hAdjusted=h-this.GUTTER,wAdjusted=Math.min(maxWidth,w);this.uiInstance.resize(wAdjusted,hAdjusted),this.hLast=this.wrapperNode.innerHeight(),this.wLast=this.wrapperNode.innerWidth()}}},{newUiWrapper:function(uiname,textareaId){return uiname?new InterfaceWrapper(uiname,textareaId):null},InterfaceWrapper:InterfaceWrapper}}));
//# sourceMappingURL=userinterfacewrapper.min.js.map
\ No newline at end of file
diff --git a/amd/build/userinterfacewrapper.min.js.map b/amd/build/userinterfacewrapper.min.js.map
index f46f952fc..631c835e8 100644
--- a/amd/build/userinterfacewrapper.min.js.map
+++ b/amd/build/userinterfacewrapper.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"userinterfacewrapper.min.js","sources":["../src/userinterfacewrapper.js"],"sourcesContent":["/******************************************************************************\n *\n * This module provides a wrapper for user-interface modules, handling hiding\n * of the textArea that is being replaced by the UI element,\n * resizing of the UI component, and support of such usability functions as\n * ctrl-alt-M to disable/re-enable the entire user interface, including the\n * wrapper.\n *\n * @module coderunner/userinterfacewrapper\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * The InterfaceWrapper class is constructed either by Moodle PHP calls of\n * the form\n *\n * $PAGE->requires->js_call_amd($modulename, $functionname, $params)\n *\n * (e.g. from within render.php) or by JavaScript require calls, e.g. from\n * authorform.js when the question author changes UI type.\n *\n * The InterfaceWrapper provides:\n *\n * 1. A constructor InterfaceWrapper(uiname, textareaId) which\n * hides the given text area, replaces it with a wrapper div (resizable in\n * height by the user but with width resizing managed by changes in window\n * width), created an instance of nameInstance as defined in the file\n * ui_name.js (see below).\n * params is a record containing the decoded value of\n *\n * 2. A stop() method that destroys the embedded UI and hides the wrapper.\n *\n * 3. A restart() method that shows the wrapper again and re-creates the prior\n * embedded UI component within it.\n *\n * 4. A loadUi(uiname, params) method that kills any currently running UI element\n * (if there is one) and (re)loads the specified one. The params parameter\n * is a record that allows additional parameters to be passed in, such as\n * those from the question's uiParams field and, in the case of the\n * Ace UI, the 'lang' (language) that the editor is editing. This data\n * is supplied by the PHP via the data-params attribute of the answer's\n * base textarea.\n *\n * 5. Regular checking for any resizing of the wrapper, which are passed on to\n * the embedded UI element's resize() method.\n *\n * 6. Monitoring of alt-ctrl-M key presses which toggle the visibility of the\n * wrapper plus UI element and the syncronised textArea by calls to stop()\n * and restart\n *\n * =========================================================================\n *\n * The embedded user-interface module must be defined in a JavaScript file\n * of the form ui_name.js which must define a class nameInstance with\n * the following functionality:\n *\n * 1. A constructor SomeUiName(textareaId, width, height, params) that\n * builds an HTML component of the given width and height. textareaId is the\n * ID of the textArea from which the UI element should obtain its initial\n * serialisation and to which it should write the serialisation when its save\n * or destroy methods are called. params is a JavaScript object,\n * decoded from the JSON uiParams defined by the question plus any\n * additional data required, such as the 'lang' in the case of Ace.\n *\n * 2. A getElement() method that returns the HTML element that the\n * InterfaceWrapper is to insert into the document tree.\n *\n * 3. A method failed() that should return true unless the constructor\n * failed (e.g. because it was not able to de-serialise the text area's\n * contents). The wrapper will call destroy() on the object if failed()\n * returns true and abort the use of the UI element. The text area will\n * have the uiloadfailed class added, which CSS will display in some\n * error mode (e.g. a red border).\n *\n * 4. A method failMessage() that will be called only when failed() returns\n * True. It should be a defined CodeRunner language string key.\n *\n * 5. A sync() method that copies the serialised represention of the UI plugin's\n * data to the related TextArea. This is used when submit is clicked.\n *\n * 6. A destroy() method that should sync the contents to the text area then\n * destroy any HTML elements or other created content. This method is called\n * when CTRL-ALT-M is typed by the user to turn off all UI plugins\n *\n * 7. A resize(width, height) method that should resize the entire UI element\n * to the given dimensions.\n *\n * 8. A hasFocus() method that returns true if the UI element has focus.\n *\n * 9. A syncIntervalSecs() method that returns the time interval between\n * calls to the sync() method. 0 for no sync calls. The userinterfacewrapper\n * provides all instances with a generic (base-class) version that returns\n * the value of a UI parameter sync_interval_secs if given else uses the\n * UI interface wrapper default (currently 10).\n *\n * The return value from the module define is a record with a single field\n * 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)\n *\n *****************************************************************************/\n\n/**\n * 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 util.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\ndefine(['jquery'], function($) {\n /**\n * Constructor for a new user interface.\n * @param {string} uiname The name of the interface element (e.g. ace, graph, etc)\n * which should be in file ui_ace.js, ui_graph.js etc.\n * @param {string} textareaId The id of the text area that the UI is to manage.\n * The text area should have an attribute data-params, which is a\n * JSON encoded record containing whatever additional parameters might\n * be needed by the User interface. As a minimum it should contain all\n * the parameters from the uiparameters field of\n * the question so that question authors can pass in additional data\n * such as whether graph edges are bidirectional or not in the case of\n * the graph UI. Additionally the Ace editor requires a 'lang' field\n * to specify what language the editor is editing.\n * When the wrapper has been set up on a text area, the text area's\n * data attribute contains an entry for 'current-ui-wrapper' that is\n * a reference to the wrapper ('this').\n */\n function InterfaceWrapper(uiname, textareaId) {\n let t = this; // For use by embedded functions.\n\n this.GUTTER = 14; // Size of gutter at base of wrapper Node (pixels)\n this.DEFAULT_SYNC_INTERVAL_SECS = 5;\n\n const PIXELS_PER_ROW = 19; // For estimating height of textareas.\n const MAX_GROWN_ROWS = 50; // Upper limit to artifically grown textarea rows.\n const MIN_WRAPPER_HEIGHT = 50;\n\n this.taId = textareaId;\n this.loadFailId = textareaId + '_loadfailerr';\n const ta = document.getElementById(textareaId);\n this.textArea = $(ta);\n const params = this.textArea.attr('data-params');\n if (params) {\n this.uiParams = JSON.parse(params);\n } else {\n this.uiParams = {};\n }\n this.uiParams.lang = this.textArea.attr('data-lang');\n this.readOnly = this.textArea.prop('readonly');\n this.isLoading = false; // True if we're busy loading a UI element.\n this.loadFailed = false; // True if UI failed to initialise properly.\n this.retries = 0; // Number of failed attempts to load a UI component.\n\n let h = parseInt(this.textArea.css(\"height\"));\n let content_lines = this.textArea.val().split('\\n').length;\n let rows = ta.rows;\n if (content_lines > rows) {\n // Allow reloaded text areas with lots of text to grow bigger, within limits.\n rows = Math.min(content_lines, MAX_GROWN_ROWS);\n }\n h = Math.max(h, rows * PIXELS_PER_ROW, MIN_WRAPPER_HEIGHT);\n\n /**\n * Construct an empty hidden wrapper div, inserted directly after the\n * textArea, ready to contain the actual UI.\n */\n this.wrapperNode = $(\"\");\n this.textArea.after(this.wrapperNode);\n this.wrapperNode.hide();\n this.wrapperNode.css({\n resize: 'vertical',\n overflow: 'hidden',\n minHeight: h,\n width: \"100%\",\n border: \"1px solid darkgrey\"\n });\n\n /**\n * Record a reference to this wrapper in the text area's data attribute\n * for use by external javascript that needs to interact with the\n * wrapper, e.g. the multilanguage.js module.\n */\n this.textArea.data('current-ui-wrapper', this);\n\n /**\n * Load the UI into the wrapper (aysnchronous).\n */\n this.uiInstance = null; // Defined by loadUi asynchronously\n this.loadUi(uiname, this.uiParams); // Load the required UI element\n\n /**\n * Add event handlers\n */\n $(document).mousemove(function() {\n t.checkForResize();\n });\n $(window).resize(function() {\n t.checkForResize();\n });\n this.textArea.closest('form').submit(function() {\n if (t.uiInstance !== null) {\n t.uiInstance.sync();\n }\n });\n $(document.body).on('keydown', function(e) {\n const KEY_M = 77;\n if (e.keyCode === KEY_M && e.ctrlKey && e.altKey) {\n if (t.uiInstance !== null || t.loadFailed) {\n t.stop();\n } else {\n t.restart(); // Reactivate\n }\n }\n });\n }\n\n /**\n * Load the specified UI element (which in the case of Ace will need\n * to know the language, lang, as well - this must be supplied as\n * a 'lang' attribute of the record params.\n * When ui is up and running, this.uiInstance will reference it.\n * To avoid a potential race problem, if this method is already busy\n * with a load, try again in 200 msecs.\n * @param {string} uiname The name of the User Interface to be used.\n * @param {object} params The UI parameters object that passes parameters\n * to the actual UI object.\n */\n InterfaceWrapper.prototype.loadUi = function(uiname, params) {\n const t = this,\n errPart1 = 'Failed to load ',\n errPart2 = ' UI component. If this error persists, please report it to the forum on coderunner.org.nz';\n\n /**\n * Get the given language string and plug it into the given jQuery\n * div element as its html, plus a 'fallback' message on a separate line.\n * @param {string} langString The language string specifier for the error message,\n * to be loaded by AJAX.\n * @param {object} errorDiv The div object into which the error message\n * is to be inserted.\n */\n function setLoadFailMessage(langString, errorDiv) {\n require(['core/str'], function(str) {\n /**\n * Get langString text via AJAX\n */\n const\n s = str.get_string(langString, 'qtype_coderunner'),\n fallback = str.get_string('ui_fallback', 'qtype_coderunner');\n $.when(s, fallback).done(function(s, fallback) {\n errorDiv.html(s + ' ' + fallback);\n });\n });\n }\n\n /**\n * The default method for a UIs sync_interval_secs method.\n * Returns the sync_interval_secs parameter if given, else\n * DEFAULT_SYNC_INTERVAL_SECS.\n */\n function syncIntervalSecsBase() {\n if (params.hasOwnProperty('sync_interval_secs')) {\n return parseInt(params.sync_interval_secs);\n } else {\n return t.DEFAULT_SYNC_INTERVAL_SECS;\n }\n }\n\n if (this.isLoading) { // Oops, we're loading a UI element already\n this.retries += 1;\n if (this.retries > 20) {\n alert(errPart1 + uiname + errPart2);\n this.retries = 0;\n this.loading = 0;\n } else {\n setTimeout(function() {\n t.loadUi(uiname, params);\n }, 200); // Try again in 200 msecs\n }\n return;\n }\n this.retries = 0;\n this.params = params; // Save in case need to restart\n\n this.stop(); // Kill any active UI first\n this.uiname = uiname;\n\n if (this.uiname === '' || this.uiname === 'none' || sessionStorage.getItem('disableUis')) {\n this.uiInstance = null;\n } else {\n this.isLoading = true;\n require(['qtype_coderunner/ui_' + this.uiname],\n function(ui) {\n const h = t.wrapperNode.innerHeight() - t.GUTTER;\n const w = t.wrapperNode.innerWidth();\n const uiInstance = new ui.Constructor(t.taId, w, h, params);\n if (uiInstance.failed()) {\n /*\n * Constructor failed to load serialisation.\n * Set uiloadfailed class on text area.\n */\n t.loadFailed = true;\n t.wrapperNode.hide();\n uiInstance.destroy();\n t.uiInstance = null;\n t.textArea.addClass('uiloadfailed');\n const loadFailDiv = '';\n let jqLoadFailDiv = $(loadFailDiv);\n jqLoadFailDiv.insertBefore(t.textArea);\n setLoadFailMessage(uiInstance.failMessage(), jqLoadFailDiv); // Insert error by AJAX\n } else {\n t.hLast = 0; // Force resize (and hence redraw)\n t.wLast = 0; // ... on first call to checkForResize\n t.textArea.hide();\n t.wrapperNode.show();\n t.wrapperNode.append(uiInstance.getElement());\n t.uiInstance = uiInstance;\n t.loadFailed = false;\n t.checkForResize();\n\n /*\n * Set a default syncIntervalSecs method if uiInstance lacks one.\n */\n let uiInstancePrototype = Object.getPrototypeOf(uiInstance);\n uiInstancePrototype.syncIntervalSecs = uiInstancePrototype.syncIntervalSecs || syncIntervalSecsBase;\n t.startSyncTimer(uiInstance);\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Start a sync timer on the given uiInstance, unless its time interval is 0.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.startSyncTimer = function(uiInstance) {\n const timeout = uiInstance.syncIntervalSecs();\n if (timeout) {\n this.uiInstance.timer = setInterval(function () {\n uiInstance.sync();\n }, timeout * 1000);\n } else {\n this.uiInstance.timer = null;\n }\n };\n\n\n /**\n * Stop the sync timer on the given uiInstance, if running.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.stopSyncTimer = function(uiInstance) {\n if (uiInstance.timer) {\n clearTimeout(uiInstance.timer);\n }\n };\n\n\n InterfaceWrapper.prototype.stop = function() {\n /*\n * Disable (shutdown) the embedded ui component.\n * The wrapper remains active for ctrl-alt-M events, but is hidden.\n */\n if (this.uiInstance !== null) {\n this.stopSyncTimer(this.uiInstance);\n this.textArea.show();\n if (this.uiInstance.hasFocus()) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n this.uiInstance.destroy();\n this.uiInstance = null;\n this.wrapperNode.hide();\n }\n this.loadFailed = false;\n this.textArea.removeClass('uiloadfailed'); // Just in case it failed before\n $(document.getElementById(this.loadFailId)).remove();\n };\n\n /*\n * Re-enable the ui element (e.g. after alt-cntrl-M). This is\n * a full re-initialisation of the ui element.\n */\n InterfaceWrapper.prototype.restart = function() {\n if (this.uiInstance === null) {\n /**\n * Restart the UI component in the textarea\n */\n this.loadUi(this.uiname, this.params);\n }\n };\n\n\n /**\n * Check for wrapper resize - propagate to ui element.\n */\n InterfaceWrapper.prototype.checkForResize = function() {\n const SIZE_HACK = 25; // Horrible but best I can do. TODO: FIXME\n\n if (this.uiInstance) {\n const h = this.wrapperNode.innerHeight();\n const w = this.wrapperNode.innerWidth();\n if (h != this.hLast || w != this.wLast) {\n const xLeft = this.wrapperNode.offset().left;\n const maxWidth = $(window).innerWidth() - xLeft - SIZE_HACK;\n const hAdjusted = h - this.GUTTER;\n const wAdjusted = Math.min(maxWidth, w);\n this.uiInstance.resize(wAdjusted, hAdjusted);\n this.hLast = this.wrapperNode.innerHeight();\n this.wLast = this.wrapperNode.innerWidth();\n }\n }\n };\n\n /**\n * The external entry point from the PHP.\n * @param {string} uiname The name of the User Interface to use e.g. 'ace'\n * @param {string} textareaId The ID of the textarea to be wrapped.\n */\n function newUiWrapper(uiname, textareaId) {\n if (uiname) {\n return new InterfaceWrapper(uiname, textareaId);\n } else {\n return null;\n }\n }\n\n\n return {\n newUiWrapper: newUiWrapper,\n InterfaceWrapper: InterfaceWrapper\n };\n});\n"],"names":["define","$","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","taId","loadFailId","ta","document","getElementById","textArea","params","attr","uiParams","JSON","parse","lang","readOnly","prop","isLoading","loadFailed","retries","h","parseInt","css","content_lines","val","split","length","rows","Math","min","max","wrapperNode","after","hide","resize","overflow","minHeight","width","border","data","uiInstance","loadUi","mousemove","checkForResize","window","closest","submit","sync","body","on","e","keyCode","ctrlKey","altKey","stop","restart","prototype","syncIntervalSecsBase","hasOwnProperty","sync_interval_secs","alert","loading","setTimeout","sessionStorage","getItem","require","ui","innerHeight","w","innerWidth","Constructor","failed","destroy","addClass","loadFailDiv","jqLoadFailDiv","insertBefore","langString","failMessage","errorDiv","str","s","get_string","fallback","when","done","html","hLast","wLast","show","append","getElement","uiInstancePrototype","Object","getPrototypeOf","syncIntervalSecs","startSyncTimer","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","remove","xLeft","offset","left","maxWidth","hAdjusted","wAdjusted","newUiWrapper"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"userinterfacewrapper.min.js","sources":["../src/userinterfacewrapper.js"],"sourcesContent":["/******************************************************************************\n *\n * This module provides a wrapper for user-interface modules, handling hiding\n * of the textArea that is being replaced by the UI element,\n * resizing of the UI component, and support of such usability functions as\n * ctrl-alt-M to disable/re-enable the entire user interface, including the\n * wrapper.\n *\n * @module coderunner/userinterfacewrapper\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * The InterfaceWrapper class is constructed either by Moodle PHP calls of\n * the form\n *\n * $PAGE->requires->js_call_amd($modulename, $functionname, $params)\n *\n * (e.g. from within render.php) or by JavaScript require calls, e.g. from\n * authorform.js when the question author changes UI type.\n *\n * The InterfaceWrapper provides:\n *\n * 1. A constructor InterfaceWrapper(uiname, textareaId) which\n * hides the given text area, replaces it with a wrapper div (resizable in\n * height by the user but with width resizing managed by changes in window\n * width), created an instance of nameInstance as defined in the file\n * ui_name.js (see below).\n * params is a record containing the decoded value of\n *\n * 2. A stop() method that destroys the embedded UI and hides the wrapper.\n *\n * 3. A restart() method that shows the wrapper again and re-creates the prior\n * embedded UI component within it.\n *\n * 4. A loadUi(uiname, params) method that kills any currently running UI element\n * (if there is one) and (re)loads the specified one. The params parameter\n * is a record that allows additional parameters to be passed in, such as\n * those from the question's uiParams field and, in the case of the\n * Ace UI, the 'lang' (language) that the editor is editing. This data\n * is supplied by the PHP via the data-params attribute of the answer's\n * base textarea.\n *\n * 5. Regular checking for any resizing of the wrapper, which are passed on to\n * the embedded UI element's resize() method.\n *\n * 6. Monitoring of alt-ctrl-M key presses which toggle the visibility of the\n * wrapper plus UI element and the syncronised textArea by calls to stop()\n * and restart\n *\n * =========================================================================\n *\n * The embedded user-interface module must be defined in a JavaScript file\n * of the form ui_name.js which must define a class nameInstance with\n * the following functionality:\n *\n * 1. A constructor SomeUiName(textareaId, width, height, params) that\n * builds an HTML component of the given width and height. textareaId is the\n * ID of the textArea from which the UI element should obtain its initial\n * serialisation and to which it should write the serialisation when its save\n * or destroy methods are called. params is a JavaScript object,\n * decoded from the JSON uiParams defined by the question plus any\n * additional data required, such as the 'lang' in the case of Ace.\n *\n * 2. A getElement() method that returns the HTML element that the\n * InterfaceWrapper is to insert into the document tree.\n *\n * 3. A method failed() that should return true unless the constructor\n * failed (e.g. because it was not able to de-serialise the text area's\n * contents). The wrapper will call destroy() on the object if failed()\n * returns true and abort the use of the UI element. The text area will\n * have the uiloadfailed class added, which CSS will display in some\n * error mode (e.g. a red border).\n *\n * 4. A method failMessage() that will be called only when failed() returns\n * True. It should be a defined CodeRunner language string key.\n *\n * 5. A sync() method that copies the serialised represention of the UI plugin's\n * data to the related TextArea. This is used when submit is clicked.\n *\n * 6. A destroy() method that should sync the contents to the text area then\n * destroy any HTML elements or other created content. This method is called\n * when CTRL-ALT-M is typed by the user to turn off all UI plugins\n *\n * 7. A resize(width, height) method that should resize the entire UI element\n * to the given dimensions.\n *\n * 8. A hasFocus() method that returns true if the UI element has focus.\n *\n * 9. A syncIntervalSecs() method that returns the time interval between\n * calls to the sync() method. 0 for no sync calls. The userinterfacewrapper\n * provides all instances with a generic (base-class) version that returns\n * the value of a UI parameter sync_interval_secs if given else uses the\n * UI interface wrapper default (currently 10).\n *\n * The return value from the module define is a record with a single field\n * 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)\n *\n *****************************************************************************/\n\n/**\n * 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 util.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\ndefine(['jquery'], function($) {\n /**\n * Constructor for a new user interface.\n * @param {string} uiname The name of the interface element (e.g. ace, graph, etc)\n * which should be in file ui_ace.js, ui_graph.js etc.\n * @param {string} textareaId The id of the text area that the UI is to manage.\n * The text area should have an attribute data-params, which is a\n * JSON encoded record containing whatever additional parameters might\n * be needed by the User interface. As a minimum it should contain all\n * the parameters from the uiparameters field of\n * the question so that question authors can pass in additional data\n * such as whether graph edges are bidirectional or not in the case of\n * the graph UI. Additionally the Ace editor requires a 'lang' field\n * to specify what language the editor is editing.\n * When the wrapper has been set up on a text area, the text area's\n * data attribute contains an entry for 'current-ui-wrapper' that is\n * a reference to the wrapper ('this').\n */\n function InterfaceWrapper(uiname, textareaId) {\n let t = this; // For use by embedded functions.\n\n this.GUTTER = 14; // Size of gutter at base of wrapper Node (pixels)\n this.DEFAULT_SYNC_INTERVAL_SECS = 5;\n\n const PIXELS_PER_ROW = 19; // For estimating height of textareas.\n const MAX_GROWN_ROWS = 50; // Upper limit to artifically grown textarea rows.\n const MIN_WRAPPER_HEIGHT = 50;\n\n this.taId = textareaId;\n this.loadFailId = textareaId + '_loadfailerr';\n const ta = document.getElementById(textareaId);\n this.textArea = $(ta);\n const params = this.textArea.attr('data-params');\n if (params) {\n this.uiParams = JSON.parse(params);\n } else {\n this.uiParams = {};\n }\n this.uiParams.lang = this.textArea.attr('data-lang');\n this.readOnly = this.textArea.prop('readonly');\n this.isLoading = false; // True if we're busy loading a UI element.\n this.loadFailed = false; // True if UI failed to initialise properly.\n this.retries = 0; // Number of failed attempts to load a UI component.\n\n let h = parseInt(this.textArea.css(\"height\"));\n let content_lines = this.textArea.val().split('\\n').length;\n let rows = ta.rows;\n if (content_lines > rows) {\n // Allow reloaded text areas with lots of text to grow bigger, within limits.\n rows = Math.min(content_lines, MAX_GROWN_ROWS);\n }\n h = Math.max(h, rows * PIXELS_PER_ROW, MIN_WRAPPER_HEIGHT);\n\n /**\n * Construct an empty hidden wrapper div, inserted directly after the\n * textArea, ready to contain the actual UI.\n */\n this.wrapperNode = $(\"\");\n this.textArea.after(this.wrapperNode);\n this.wrapperNode.hide();\n this.wrapperNode.css({\n resize: 'vertical',\n overflow: 'hidden',\n minHeight: h,\n width: \"100%\",\n border: \"1px solid darkgrey\"\n });\n\n /**\n * Record a reference to this wrapper in the text area's data attribute\n * for use by external javascript that needs to interact with the\n * wrapper, e.g. the multilanguage.js module.\n */\n this.textArea.data('current-ui-wrapper', this);\n\n /**\n * Load the UI into the wrapper (aysnchronous).\n */\n this.uiInstance = null; // Defined by loadUi asynchronously\n this.loadUi(uiname, this.uiParams); // Load the required UI element\n\n /**\n * Add event handlers\n */\n $(document).mousemove(function() {\n t.checkForResize();\n });\n $(window).resize(function() {\n t.checkForResize();\n });\n this.textArea.closest('form').submit(function() {\n if (t.uiInstance !== null) {\n t.uiInstance.sync();\n }\n });\n $(document.body).on('keydown', function(e) {\n const KEY_M = 77;\n if (e.keyCode === KEY_M && e.ctrlKey && e.altKey) {\n if (t.uiInstance !== null || t.loadFailed) {\n t.stop();\n } else {\n t.restart(); // Reactivate\n }\n }\n });\n }\n\n /**\n * Load the specified UI element (which in the case of Ace will need\n * to know the language, lang, as well - this must be supplied as\n * a 'lang' attribute of the record params.\n * When ui is up and running, this.uiInstance will reference it.\n * To avoid a potential race problem, if this method is already busy\n * with a load, try again in 200 msecs.\n * @param {string} uiname The name of the User Interface to be used.\n * @param {object} params The UI parameters object that passes parameters\n * to the actual UI object.\n */\n InterfaceWrapper.prototype.loadUi = function(uiname, params) {\n const t = this,\n errPart1 = 'Failed to load ',\n errPart2 = ' UI component. If this error persists, please report it to the forum on coderunner.org.nz';\n\n /**\n * Get the given language string and plug it into the given jQuery\n * div element as its html, plus a 'fallback' message on a separate line.\n * @param {string} langString The language string specifier for the error message,\n * to be loaded by AJAX.\n * @param {object} errorDiv The div object into which the error message\n * is to be inserted.\n */\n function setLoadFailMessage(langString, errorDiv) {\n require(['core/str'], function(str) {\n /**\n * Get langString text via AJAX\n */\n const\n s = str.get_string(langString, 'qtype_coderunner'),\n fallback = str.get_string('ui_fallback', 'qtype_coderunner');\n $.when(s, fallback).done(function(s, fallback) {\n errorDiv.html(s + ' ' + fallback);\n });\n });\n }\n\n /**\n * The default method for a UIs sync_interval_secs method.\n * Returns the sync_interval_secs parameter if given, else\n * DEFAULT_SYNC_INTERVAL_SECS.\n */\n function syncIntervalSecsBase() {\n if (params.hasOwnProperty('sync_interval_secs')) {\n return parseInt(params.sync_interval_secs);\n } else {\n return t.DEFAULT_SYNC_INTERVAL_SECS;\n }\n }\n\n if (this.isLoading) { // Oops, we're loading a UI element already\n this.retries += 1;\n if (this.retries > 20) {\n alert(errPart1 + uiname + errPart2);\n this.retries = 0;\n this.loading = 0;\n } else {\n setTimeout(function() {\n t.loadUi(uiname, params);\n }, 200); // Try again in 200 msecs\n }\n return;\n }\n this.retries = 0;\n this.params = params; // Save in case need to restart\n\n this.stop(); // Kill any active UI first\n this.uiname = uiname;\n\n if (this.uiname === '' || this.uiname === 'none' || sessionStorage.getItem('disableUis')) {\n this.uiInstance = null;\n } else {\n this.isLoading = true;\n require(['qtype_coderunner/ui_' + this.uiname],\n function(ui) {\n const h = t.wrapperNode.innerHeight() - t.GUTTER;\n const w = t.wrapperNode.innerWidth();\n const uiInstance = new ui.Constructor(t.taId, w, h, params);\n if (uiInstance.failed()) {\n /*\n * Constructor failed to load serialisation.\n * Set uiloadfailed class on text area.\n */\n t.loadFailed = true;\n t.wrapperNode.hide();\n uiInstance.destroy();\n t.uiInstance = null;\n t.textArea.addClass('uiloadfailed');\n const loadFailDiv = '';\n let jqLoadFailDiv = $(loadFailDiv);\n jqLoadFailDiv.insertBefore(t.textArea);\n setLoadFailMessage(uiInstance.failMessage(), jqLoadFailDiv); // Insert error by AJAX\n } else {\n t.hLast = 0; // Force resize (and hence redraw)\n t.wLast = 0; // ... on first call to checkForResize\n t.textArea.hide();\n t.wrapperNode.show();\n t.wrapperNode.append(uiInstance.getElement());\n t.uiInstance = uiInstance;\n t.loadFailed = false;\n t.checkForResize();\n\n /*\n * Set a default syncIntervalSecs method if uiInstance lacks one.\n */\n let uiInstancePrototype = Object.getPrototypeOf(uiInstance);\n uiInstancePrototype.syncIntervalSecs = uiInstancePrototype.syncIntervalSecs || syncIntervalSecsBase;\n t.startSyncTimer(uiInstance);\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Start a sync timer on the given uiInstance, unless its time interval is 0.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.startSyncTimer = function(uiInstance) {\n const timeout = uiInstance.syncIntervalSecs();\n if (timeout) {\n this.uiInstance.timer = setInterval(function () {\n uiInstance.sync();\n }, timeout * 1000);\n } else {\n this.uiInstance.timer = null;\n }\n };\n\n\n /**\n * Stop the sync timer on the given uiInstance, if running.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.stopSyncTimer = function(uiInstance) {\n if (uiInstance.timer) {\n clearTimeout(uiInstance.timer);\n }\n };\n\n\n InterfaceWrapper.prototype.stop = function() {\n /*\n * Disable (shutdown) the embedded ui component.\n * The wrapper remains active for ctrl-alt-M events, but is hidden.\n */\n if (this.uiInstance !== null) {\n this.stopSyncTimer(this.uiInstance);\n this.textArea.show();\n if (this.uiInstance.hasFocus()) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n this.uiInstance.destroy();\n this.uiInstance = null;\n this.wrapperNode.hide();\n }\n this.loadFailed = false;\n this.textArea.removeClass('uiloadfailed'); // Just in case it failed before\n $(document.getElementById(this.loadFailId)).remove();\n };\n\n /*\n * Re-enable the ui element (e.g. after alt-cntrl-M). This is\n * a full re-initialisation of the ui element.\n */\n InterfaceWrapper.prototype.restart = function() {\n if (this.uiInstance === null) {\n /**\n * Restart the UI component in the textarea\n */\n this.loadUi(this.uiname, this.params);\n }\n };\n\n\n /**\n * Check for wrapper resize - propagate to ui element.\n */\n InterfaceWrapper.prototype.checkForResize = function() {\n const SIZE_HACK = 25; // Horrible but best I can do. TODO: FIXME\n\n if (this.uiInstance) {\n const h = this.wrapperNode.innerHeight();\n const w = this.wrapperNode.innerWidth();\n if (h != this.hLast || w != this.wLast) {\n const xLeft = this.wrapperNode.offset().left;\n const maxWidth = $(window).innerWidth() - xLeft - SIZE_HACK;\n const hAdjusted = h - this.GUTTER;\n const wAdjusted = Math.min(maxWidth, w);\n this.uiInstance.resize(wAdjusted, hAdjusted);\n this.hLast = this.wrapperNode.innerHeight();\n this.wLast = this.wrapperNode.innerWidth();\n }\n }\n };\n\n /**\n * The external entry point from the PHP.\n * @param {string} uiname The name of the User Interface to use e.g. 'ace'\n * @param {string} textareaId The ID of the textarea to be wrapped.\n */\n function newUiWrapper(uiname, textareaId) {\n if (uiname) {\n return new InterfaceWrapper(uiname, textareaId);\n } else {\n return null;\n }\n }\n\n\n return {\n newUiWrapper: newUiWrapper,\n InterfaceWrapper: InterfaceWrapper\n };\n});\n"],"names":["define","$","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","taId","loadFailId","ta","document","getElementById","textArea","params","attr","uiParams","JSON","parse","lang","readOnly","prop","isLoading","loadFailed","retries","h","parseInt","css","content_lines","val","split","length","rows","Math","min","max","wrapperNode","after","hide","resize","overflow","minHeight","width","border","data","uiInstance","loadUi","mousemove","checkForResize","window","closest","submit","sync","body","on","e","keyCode","ctrlKey","altKey","stop","restart","prototype","syncIntervalSecsBase","hasOwnProperty","sync_interval_secs","alert","loading","setTimeout","sessionStorage","getItem","require","ui","langString","errorDiv","innerHeight","w","innerWidth","Constructor","failed","destroy","addClass","loadFailDiv","jqLoadFailDiv","insertBefore","failMessage","str","s","get_string","fallback","when","done","html","hLast","wLast","show","append","getElement","uiInstancePrototype","Object","getPrototypeOf","syncIntervalSecs","startSyncTimer","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","remove","xLeft","offset","left","maxWidth","hAdjusted","wAdjusted","newUiWrapper"],"mappings":"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"}
\ No newline at end of file
From 50c0b1cc8c7e1d97a7b9614a057f7022e1d129b9 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Mon, 16 Jan 2023 11:36:21 +1300
Subject: [PATCH 033/188] Update test to handle the fact that preloaded answers
aren't actually graded any more.
---
tests/behat/reset_button.feature | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/behat/reset_button.feature b/tests/behat/reset_button.feature
index 587c8ad88..fc215c1d4 100644
--- a/tests/behat/reset_button.feature
+++ b/tests/behat/reset_button.feature
@@ -38,4 +38,4 @@ Feature: Preview the Python 3 sqr function CodeRunner question with a preload
And I press "Reset answer"
And I press "Check"
Then I should see "# Your answer goes here"
- And I should see "Marks for this submission: 0.00/31.00"
+ And I should see "You must complete or edit the preloaded answer."
From cf8dbf94fc0b8e5ce0c0966703bfd3ea54c08b25 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Mon, 16 Jan 2023 13:34:01 +1300
Subject: [PATCH 034/188] Merge Tim Hunt's updates to the tests.
---
.github/workflows/ci.yml | 8 ++++----
tests/phpquestions_test.php | 3 +++
tests/walkthrough_test.php | 2 --
3 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index adf937816..e7c328995 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,8 +9,8 @@ jobs:
fail-fast: false
matrix:
include:
- - php: '7.4'
- moodle-branch: 'MOODLE_400_STABLE'
+ - php: '8.0'
+ moodle-branch: 'MOODLE_401_STABLE'
database: 'pgsql'
- php: '7.3'
moodle-branch: 'MOODLE_400_STABLE'
@@ -18,7 +18,7 @@ jobs:
services:
postgres:
- image: postgres:10
+ image: postgres:13
env:
POSTGRES_USER: 'postgres'
POSTGRES_HOST_AUTH_METHOD: 'trust'
@@ -122,7 +122,7 @@ jobs:
run: moodle-plugin-ci mustache
- name: Grunt
- if: ${{ always() }}
+ if: ${{ matrix.moodle-branch == 'MOODLE_400_STABLE' }}
run: moodle-plugin-ci grunt
- name: PHPUnit tests
diff --git a/tests/phpquestions_test.php b/tests/phpquestions_test.php
index 27051921d..d24718519 100644
--- a/tests/phpquestions_test.php
+++ b/tests/phpquestions_test.php
@@ -40,6 +40,7 @@
class phpquestions_test extends \qtype_coderunner_testcase {
public function test_good_sqr_function() {
+ $this->check_language_available('php');
$q = $this->make_question('sqrphp');
$response = array('answer' => "grade_response($response);
@@ -53,6 +54,7 @@ public function test_good_sqr_function() {
public function test_bad_sqr_function() {
+ $this->check_language_available('php');
$q = $this->make_question('sqrphp');
$response = array('answer' => "grade_response($response);
@@ -66,6 +68,7 @@ public function test_bad_sqr_function() {
public function test_bad_syntax() {
+ $this->check_language_available('php');
$q = $this->make_question('sqrphp');
$response = array('answer' => "grade_response($response);
diff --git a/tests/walkthrough_test.php b/tests/walkthrough_test.php
index 3d2c8dd5a..28f43f5e1 100644
--- a/tests/walkthrough_test.php
+++ b/tests/walkthrough_test.php
@@ -24,8 +24,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-use qtype_coderunner\constants;
-
namespace qtype_coderunner;
defined('MOODLE_INTERNAL') || die();
From c06e36541a183729c48bc0ed27669888c551925b Mon Sep 17 00:00:00 2001
From: Michelle Hsieh
Date: Mon, 16 Jan 2023 19:34:01 +1300
Subject: [PATCH 035/188] Added duplicate prototype tests and fixed bugs
---
edit_coderunner_form.php | 15 +++-
lang/en/qtype_coderunner.php | 2 +-
tests/behat/duplicate_prototype.feature | 67 ++++++++++++++
tests/fixtures/prototype_c_via_python_v1.xml | 95 ++++++++++++++++++++
tests/fixtures/prototype_c_via_python_v2.xml | 95 ++++++++++++++++++++
5 files changed, 269 insertions(+), 5 deletions(-)
create mode 100644 tests/behat/duplicate_prototype.feature
create mode 100644 tests/fixtures/prototype_c_via_python_v1.xml
create mode 100644 tests/fixtures/prototype_c_via_python_v2.xml
diff --git a/edit_coderunner_form.php b/edit_coderunner_form.php
index 489db4687..754c12794 100644
--- a/edit_coderunner_form.php
+++ b/edit_coderunner_form.php
@@ -843,13 +843,20 @@ private function make_advanced_customisation_panel($mform) {
// Validate the given data and possible files.
public function validation($data, $files) {
$errors = parent::validation($data, $files);
- $this->formquestion = $this->make_question_from_form_data($data);
- $this->formquestion->brokenquestionmessage = $this->load_error_messages($data, $this->formquestion);
- if ($data['coderunnertype'] == 'Undefined' || $this->formquestion->brokenquestionmessage !== '') {
+ if (!isset($data['coderunnertype'])) {
+ if ($data['prototypetype'] == 2) {
+ // If the questiontype is Undefined or non-existent; still good for user prototype.
+ $data['coderunnertype'] = $data['typename'];
+ } else {
+ $data['coderunnertype'] = 'Undefined';
+ }
+ }
+ if ($data['coderunnertype'] == 'Undefined') {
$errors['coderunner_type_group'] = get_string('questiontype_required', 'qtype_coderunner');
- return $errors; // Don't continue checking in this case, including missing or extra prototypes.
+ return $errors; // Don't continue checking in these cases, including if there isn't a previous coderunnertype (duplicate, missings).
// Else template param validation breaks.
}
+ $this->formquestion = $this->make_question_from_form_data($data);
if ($data['cputimelimitsecs'] != '' &&
(!ctype_digit($data['cputimelimitsecs']) || intval($data['cputimelimitsecs']) <= 0)) {
$errors['sandboxcontrols'] = get_string('badcputime', 'qtype_coderunner');
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index 2a61bf38b..af116436a 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -382,7 +382,7 @@
$string['legacyuiparams'] = 'UI parameters can no longer be defined within the template parameters field. Please move the following to the UI parameters field instead: ';
$string['legacyuiparams2'] = 'UI parameters can no longer be defined within the template parameters field. Please move the following to the UI parameters field instead, removing the \'{$a->uiname}_\' prefix: ';
$string['listprototypeduplicates'] = 'Question ID: {$a->id}
Name: {$a->name}
Category: {$a->category}
';
-$string['loadprototypeerror'] = 'Reverted to question type: \'{$a->oldtype}\' Could not load question type \'{$a->crtype}\' as the prototype is non-unique in the following questions: {$a->outputstring}';
+$string['loadprototypeerror'] = 'Reverted to question type: \'{$a->oldtype}\' Could not load question type \'{$a->crtype}\' as the prototype is non-unique in the following questions:{$a->outputstring}';
$string['mark'] = 'Mark';
$string['marking'] = 'Mark allocation';
$string['markinggroup'] = 'Marking';
diff --git a/tests/behat/duplicate_prototype.feature b/tests/behat/duplicate_prototype.feature
new file mode 100644
index 000000000..d6adf8385
--- /dev/null
+++ b/tests/behat/duplicate_prototype.feature
@@ -0,0 +1,67 @@
+@qtype @qtype_coderunner @javascript @prototypetests @_file_upload
+Feature: duplicate_prototypes
+ In order to deal with duplicate prototypes
+ As a teacher
+ I should see an informative error message and be able to fix by editing the duplicates
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@asd.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And the following "question categories" exist:
+ | contextlevel | reference | Question category | name |
+ | Course | C1 | Top | Test questions |
+
+ # Upload the prototype_c_via_python_v1.xml in samples initially
+ And I am on the "Course 1" "core_question > course question import" page logged in as teacher1
+ And I upload "question/type/coderunner/tests/fixtures/prototype_c_via_python_v1.xml" file to "Import" filemanager
+ And I set the field "id_format_xml" to "1"
+ And I press "id_submitbutton"
+ And I press "Continue"
+
+ # Edit the prototype name to something else
+ And I am on the "DEMO_PROTOTYPE_C_using_python" "core_question > edit" page logged in as teacher1
+ And I click on "a[aria-controls='id_advancedcustomisationheadercontainer']" "css_element"
+ And I set the field "typename" to "python3_duplicate"
+ And I press "id_submitbutton"
+
+ # Upload the prototype_c_via_python_v2.xml in samples to make a duplicate
+ And I am on the "Course 1" "core_question > course question import" page logged in as teacher1
+ And I upload "question/type/coderunner/tests/fixtures/prototype_c_via_python_v2.xml" file to "Import" filemanager
+ And I set the field "id_format_xml" to "1"
+ And I press "id_submitbutton"
+ And I press "Continue"
+
+ # Now delete the latest version of the first prototype, leaving you with two identical prototypes
+ And I choose "Delete" action for "DEMO_PROTOTYPE_C_using_python" in the question bank
+ And I press "Delete"
+
+ Scenario: As a teacher, if I edit a question with a duplicate prototype I should see a duplicate prototype error
+ When I am on the "DEMO_duplicate_prototype" "core_question > edit" page logged in as teacher1
+ And I should see "This question was defined to be of type 'c_via_python' but the prototype is non-unique in the following questions:"
+ And I should see "Name: DEMO_PROTOTYPE_C_using_python"
+ Then I should see "Name: DEMO_duplicate_prototype"
+
+ Scenario: As a teacher, I should be warned if the prototype is duplicated when making a new question
+ When I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
+ And I press "Create a new question ..."
+ And I click on "input#item_qtype_coderunner" "css_element"
+ And I press "submitbutton"
+ And I set the field "id_coderunnertype" to "c_via_python" and dismiss the alert
+ And I should see "Reverted to question type: 'Undefined'"
+ And I should see "Could not load question type 'c_via_python' as the prototype is non-unique in the following questions:"
+ And I should see "Name: DEMO_PROTOTYPE_C_using_python"
+ Then I should see "Name: DEMO_duplicate_prototype"
+
+ Scenario: As a teacher, I should be able to fix the duplicate prototype by renaming it
+ When I choose "Delete" action for "DEMO_PROTOTYPE_C_using_python" in the question bank
+ And I press "Delete"
+ And I am on the "DEMO_duplicate_prototype" "core_question > edit" page logged in as teacher1
+ Then I should not see "This question was defined to be of type 'c_via_python' but the prototype is non-unique in the following questions:"
+
diff --git a/tests/fixtures/prototype_c_via_python_v1.xml b/tests/fixtures/prototype_c_via_python_v1.xml
new file mode 100644
index 000000000..fe3898c01
--- /dev/null
+++ b/tests/fixtures/prototype_c_via_python_v1.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+ $system$/EXPORT_UOC_PROTOTYPES
+
+
+
+
+
+
+
+ DEMO_PROTOTYPE_C_using_python
+
+
+ Running a new language using Python
This question type shows how a Python script can be used to run code in a different language. In this example, the different language is C, but it can be any language installed on the Jobe server.
Running other languages via Python scripts gives lots of extra flexibility, such as the ability to check the student code before running it or to change compilation or linking flags.
In this example a template parameter cflags can be used to supply a gcc command line substring of compiler flags, defaulting to
-std=c99 -Wall -Werror
This question type is equivalent to the c_program question type: the student is required to submit an entire C program, which is compiled and run for each test case, with the standard input supplied in that test case. Compiling for each test is perfectly acceptable for C, where the compile time is generally negligible compared to the total question submission turn-around time, but for languages with a very high compile cost a combinator template, with Allow multiple stdins set, could be used, albeit with a greatly increased complexity.
]]>
+
+
+
+
+ 1.0000000
+ 0.0000000
+ 0
+ c_via_python
+ 2
+ 1
+ 10, 20, ...
+ 0
+ 0
+ 18
+ 100
+
+ 1
+
+ 0:
+ # Ignore non-zero positive return codes
+ if e.output:
+ print(e.output)
+ else:
+ # But negative return codes are signals - abort
+ if e.output:
+ print(e.output, file=sys.stderr)
+ if e.returncode < 0:
+ print("Task failed with signal", -e.returncode, file=sys.stderr)
+ print("** Further testing aborted **", file=sys.stderr)
+
+]]>
+ 0
+ 0
+
+ 0
+
+ python3
+ C
+
+ EqualityGrader
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/prototype_c_via_python_v2.xml b/tests/fixtures/prototype_c_via_python_v2.xml
new file mode 100644
index 000000000..a8a054e44
--- /dev/null
+++ b/tests/fixtures/prototype_c_via_python_v2.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+ $system$/EXPORT_UOC_PROTOTYPES
+
+
+
+
+
+
+
+ DEMO_duplicate_prototype
+
+
+ Running a new language using Python
This question type shows how a Python script can be used to run code in a different language. In this example, the different language is C, but it can be any language installed on the Jobe server.
Running other languages via Python scripts gives lots of extra flexibility, such as the ability to check the student code before running it or to change compilation or linking flags.
In this example a template parameter cflags can be used to supply a gcc command line substring of compiler flags, defaulting to
-std=c99 -Wall -Werror
This question type is equivalent to the c_program question type: the student is required to submit an entire C program, which is compiled and run for each test case, with the standard input supplied in that test case. Compiling for each test is perfectly acceptable for C, where the compile time is generally negligible compared to the total question submission turn-around time, but for languages with a very high compile cost a combinator template, with Allow multiple stdins set, could be used, albeit with a greatly increased complexity.
]]>
+
+
+
+
+ 1.0000000
+ 0.0000000
+ 0
+ c_via_python
+ 2
+ 1
+ 10, 20, ...
+ 0
+ 0
+ 18
+ 100
+
+ 1
+
+ 0:
+ # Ignore non-zero positive return codes
+ if e.output:
+ print(e.output)
+ else:
+ # But negative return codes are signals - abort
+ if e.output:
+ print(e.output, file=sys.stderr)
+ if e.returncode < 0:
+ print("Task failed with signal", -e.returncode, file=sys.stderr)
+ print("** Further testing aborted **", file=sys.stderr)
+
+]]>
+ 0
+ 0
+
+ 0
+
+ python3
+ C
+
+ EqualityGrader
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From 4c24593f0285e7784db0003293e0bd3ae0673564 Mon Sep 17 00:00:00 2001
From: Michelle Hsieh
Date: Tue, 17 Jan 2023 17:10:18 +1300
Subject: [PATCH 036/188] Added test cases for template params and more Form
related tests
---
tests/behat/behat_coderunner.php | 4 +-
.../behat/check_graph_question_types.feature | 9 +-
.../check_python_template_params.feature | 2 +-
tests/behat/check_stepinfo.feature | 2 +-
.../behat/check_twig_student_variable.feature | 2 +-
.../behat/create_python3_sqr_function.feature | 2 +-
tests/behat/duplicate_prototype.feature | 13 +++
tests/behat/edit_question_precheck.feature | 105 ++++++++++++++++++
tests/behat/grading_scenarios.feature | 2 +-
tests/behat/make_prototype.feature | 18 +++
...run_python3_sqr_function_templated.feature | 2 +-
tests/behat/set_uiplugin.feature | 2 +-
tests/behat/template_params_error.feature | 101 +++++++++++++++++
tests/behat/twigprefix.feature | 3 +-
14 files changed, 252 insertions(+), 15 deletions(-)
create mode 100644 tests/behat/edit_question_precheck.feature
create mode 100644 tests/behat/template_params_error.feature
diff --git a/tests/behat/behat_coderunner.php b/tests/behat/behat_coderunner.php
index 1d7f7af61..20fc31ad5 100644
--- a/tests/behat/behat_coderunner.php
+++ b/tests/behat/behat_coderunner.php
@@ -83,7 +83,7 @@ public function i_set_behat_testing() {
* Step to set an HTML5 session variable 'disableUis' to true to prevent
* loading of the usual Ace (or Graph etc) UI plugin.
*
- * @When /^I disable UI plugins/
+ * @When /^I disable UI plugins in the CodeRunner question type/
*/
public function i_disable_ui_plugins() {
$javascript = "sessionStorage.setItem('disableUis', true);";
@@ -94,7 +94,7 @@ public function i_disable_ui_plugins() {
* Step to remove the HTML5 session variable 'disableUis' (if present)
* to re-enable loading of the usual Ace (or Graph etc) UI plugins.
*
- * @When /^I enable UI plugins/
+ * @When /^I enable UI plugins in the CodeRunner question type/
*/
public function i_enable_u_plugins() {
$javascript = "sessionStorage.removeItem('disableUis');";
diff --git a/tests/behat/check_graph_question_types.feature b/tests/behat/check_graph_question_types.feature
index 933331c80..8b65a60a4 100644
--- a/tests/behat/check_graph_question_types.feature
+++ b/tests/behat/check_graph_question_types.feature
@@ -20,7 +20,7 @@ Feature: Check that the directed and undirected graph question types work.
And I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
Scenario: Preview a coderunner directed graph question
- When I disable UI plugins
+ When I disable UI plugins in the CodeRunner question type
And I add a "CodeRunner" question filling the form with:
| id_coderunnertype | directed_graph |
| id_name | Test 2-node directed graph |
@@ -28,7 +28,7 @@ Feature: Check that the directed and undirected graph question types work.
| id_testcode_0 | print(list(sorted(graph.items()))) |
| id_expected_0 | [('A', [('B', 'wt')]), ('B', [])] |
| id_answer | {"edgeGeometry":[{"lineAngleAdjust":0,"parallelPart":0.5,"perpendicularPart":0}],"nodeGeometry":[[246,163],[426,160]],"nodes":[["A",false],["B",false]],"edges":[[0,1,"wt"]]}|
- And I enable UI plugins
+ And I enable UI plugins in the CodeRunner question type
And I choose "Preview" action for "Test 2-node directed graph" in the question bank
Then I should see a canvas
And I press "Fill in correct responses"
@@ -36,7 +36,7 @@ Feature: Check that the directed and undirected graph question types work.
Then I should see "Passed all tests!"
Scenario: Preview a coderunner undirected graph question
- When I disable UI plugins
+ When I disable UI plugins in the CodeRunner question type
And I add a "CodeRunner" question filling the form with:
| id_coderunnertype | undirected_graph |
| name | Test 2-node undirected graph |
@@ -44,8 +44,9 @@ Feature: Check that the directed and undirected graph question types work.
| id_testcode_0 | print(list(sorted(graph.items()))) |
| id_expected_0 | [('A', [('B', 'wt')]), ('B', [('A', 'wt')])] |
| id_answer | {"edgeGeometry":[{"lineAngleAdjust":0,"parallelPart":0.5,"perpendicularPart":0}],"nodeGeometry":[[246,163],[426,160]],"nodes":[["A",false],["B",false]],"edges":[[0,1,"wt"]]}|
- And I enable UI plugins
+ And I enable UI plugins in the CodeRunner question type
And I choose "Preview" action for "Test 2-node undirected graph" in the question bank
+ Then I should see a canvas
And I press "Fill in correct responses"
And I press "Check"
Then I should see "Passed all tests!"
diff --git a/tests/behat/check_python_template_params.feature b/tests/behat/check_python_template_params.feature
index fe18509ac..3582deda8 100644
--- a/tests/behat/check_python_template_params.feature
+++ b/tests/behat/check_python_template_params.feature
@@ -23,7 +23,7 @@ Feature: Check that Python and other languages can be used instead of Twig as a
| activity | name | course | idnumber |
| quiz | Test quiz | C1 | quiz1 |
And I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
- And I disable UI plugins
+ And I disable UI plugins in the CodeRunner question type
And I add a "CodeRunner" question filling the form with:
| id_coderunnertype | python3 |
| id_customise | 1 |
diff --git a/tests/behat/check_stepinfo.feature b/tests/behat/check_stepinfo.feature
index 8bb8a5dc6..69ebbc82d 100644
--- a/tests/behat/check_stepinfo.feature
+++ b/tests/behat/check_stepinfo.feature
@@ -24,7 +24,7 @@ Feature: Check that the QUESTION.stepinfo record is working.
| quiz | Test quiz | C1 | quiz1 |
And I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
- And I disable UI plugins
+ And I disable UI plugins in the CodeRunner question type
And I add a "CodeRunner" question filling the form with:
| id_coderunnertype | python3 |
| id_customise | 1 |
diff --git a/tests/behat/check_twig_student_variable.feature b/tests/behat/check_twig_student_variable.feature
index c3e7dffae..7d011155b 100644
--- a/tests/behat/check_twig_student_variable.feature
+++ b/tests/behat/check_twig_student_variable.feature
@@ -23,7 +23,7 @@ Feature: Check the STUDENT Twig variable allows access to current username in Co
| activity | name | course | idnumber |
| quiz | Test quiz | C1 | quiz1 |
And I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
- And I disable UI plugins
+ And I disable UI plugins in the CodeRunner question type
And I add a "CodeRunner" question filling the form with:
| id_coderunnertype | python3 |
| id_customise | 1 |
diff --git a/tests/behat/create_python3_sqr_function.feature b/tests/behat/create_python3_sqr_function.feature
index 907e511f4..e459fc9e9 100644
--- a/tests/behat/create_python3_sqr_function.feature
+++ b/tests/behat/create_python3_sqr_function.feature
@@ -17,7 +17,7 @@ Feature: Create a CodeRunner question (the sqr function example)
And I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
Scenario: As a teacher, I create a Python3 sqr(n) -> n**2 function CodeRunner question
- When I disable UI plugins
+ When I disable UI plugins in the CodeRunner question type
And I add a "CodeRunner" question filling the form with:
| id_coderunnertype | python3 |
| name | sqr acceptance question |
diff --git a/tests/behat/duplicate_prototype.feature b/tests/behat/duplicate_prototype.feature
index d6adf8385..9dda13d44 100644
--- a/tests/behat/duplicate_prototype.feature
+++ b/tests/behat/duplicate_prototype.feature
@@ -65,3 +65,16 @@ Feature: duplicate_prototypes
And I am on the "DEMO_duplicate_prototype" "core_question > edit" page logged in as teacher1
Then I should not see "This question was defined to be of type 'c_via_python' but the prototype is non-unique in the following questions:"
+ Scenario: As a teacher, I should not be allowed to save a duplicated prototype
+ When I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
+ And I press "Create a new question ..."
+ And I click on "input#item_qtype_coderunner" "css_element"
+ And I press "submitbutton"
+ And I set the field "id_coderunnertype" to "c_via_python" and dismiss the alert
+ And I set the field "name" to "question"
+ And I set the field "id_questiontext" to "Question text"
+ And I set the field "id_testcode_0" to "null"
+ And I set the field "id_expected_0" to "null"
+ And I should see "Reverted to question type: 'Undefined'"
+ And I press "id_submitbutton"
+ Then I should see "You must select the type of question"
diff --git a/tests/behat/edit_question_precheck.feature b/tests/behat/edit_question_precheck.feature
new file mode 100644
index 000000000..29ee74e74
--- /dev/null
+++ b/tests/behat/edit_question_precheck.feature
@@ -0,0 +1,105 @@
+@qtype @qtype_coderunner @javascript
+Feature: edit_question_precheck
+ In order to successfully edit CodeRunner questions
+ As a teacher
+ I should get informative error messages if saving was unsuccessful
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@asd.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And the following "question categories" exist:
+ | contextlevel | reference | questioncategory | name |
+ | Course | C1 | Top | Behat Testing |
+ And I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
+ And I press "Create a new question ..."
+ And I click on "input#item_qtype_coderunner" "css_element"
+ And I press "submitbutton"
+ And I set the field "id_coderunnertype" to "python3"
+ And I set the field "name" to "PROTOTYPE_test_prototype"
+ And I set the field "id_questiontext" to "Arbitrary prototype"
+ And I set the field "id_customise" to "1"
+ And I click on "a[aria-controls='id_advancedcustomisationheadercontainer']" "css_element"
+ And I set the field "prototypetype" to "Yes (user defined)"
+ And I set the field "typename" to "PROTOTYPE_test"
+ And I press "id_submitbutton"
+ And I add a "CodeRunner" question filling the form with:
+ | id_coderunnertype | python3 |
+ | name | Dummy question |
+ | id_questiontext | Do nothing |
+ | id_testcode_0 | Helloworld |
+ | id_expected_0 | Helloworld |
+ And I disable UI plugins in the CodeRunner question type
+
+ Scenario: As a teacher, I should be warned if question type was not selected
+ When I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
+ And I press "Create a new question ..."
+ And I click on "input#item_qtype_coderunner" "css_element"
+ And I press "submitbutton"
+ And I set the field "name" to "Trial"
+ And I set the field "id_questiontext" to "Trial"
+ And I set the field "id_testcode_0" to "Trial"
+ And I set the field "id_expected_0" to "Trial"
+ And I press "id_submitbutton"
+ Then I should see "You must select the type of question"
+
+ Scenario: As a teacher, I should be warned if the template params have invalid JSON
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the following fields to these values:
+ | id_templateparams | {"half":} |
+ And I press "id_submitbutton"
+ Then I should see "Template parameters must evaluate to blank or a valid JSON record"
+
+ Scenario: As a teacher, I should be warned if the result columns have invalid JSON
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_customise" to "1"
+ And I set the field "resultcolumns" to "notjson"
+ And I press "id_submitbutton"
+ Then I should see "Result columns field is not a valid JSON string"
+
+ Scenario: As a teacher, I should be warned if prototype name is used already
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_customise" to "1"
+ And I click on "a[aria-controls='id_advancedcustomisationheadercontainer']" "css_element"
+ And I set the field "prototypetype" to "Yes (user defined)"
+ And I set the field "typename" to "PROTOTYPE_test"
+ And I press "id_submitbutton"
+ Then I should see "Illegal name for new prototype: already in use"
+
+ Scenario: As a teacher, I should be warned if prototype name is empty if it is a user-defined prototype
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_customise" to "1"
+ And I click on "a[aria-controls='id_advancedcustomisationheadercontainer']" "css_element"
+ And I set the field "prototypetype" to "Yes (user defined)"
+ And I press "id_submitbutton"
+ Then I should see "New question type name cannot be empty"
+
+ Scenario: As a teacher, I should be warned if sandbox time limit is inappropriate
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_customise" to "1"
+ And I click on "a[aria-controls='id_advancedcustomisationheadercontainer']" "css_element"
+ And I set the field "cputimelimitsecs" to "notanumber"
+ And I press "id_submitbutton"
+ Then I should see "CPU time limit must be left blank or must be an integer greater than zero"
+
+ Scenario: As a teacher, I should be warned if sandbox memory limit is inappropriate
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_customise" to "1"
+ And I click on "a[aria-controls='id_advancedcustomisationheadercontainer']" "css_element"
+ And I set the field "memlimitmb" to "notanumber"
+ And I press "id_submitbutton"
+ Then I should see "Memory limit must either be left blank or must be a non-negative integer"
+
+ Scenario: As a teacher, I should be warned if sandbox parameters are not JSON
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_customise" to "1"
+ And I click on "a[aria-controls='id_advancedcustomisationheadercontainer']" "css_element"
+ And I set the field "sandboxparams" to "notJson"
+ And I press "id_submitbutton"
+ Then I should see "'Other' field (sandbox params) must be either blank or a valid JSON record"
diff --git a/tests/behat/grading_scenarios.feature b/tests/behat/grading_scenarios.feature
index 74b81abbb..cb7602a3c 100644
--- a/tests/behat/grading_scenarios.feature
+++ b/tests/behat/grading_scenarios.feature
@@ -20,7 +20,7 @@ Feature: Check grading with the Python 3 sqr function CodeRunner question
And the following "questions" exist:
| questioncategory | qtype | name |
| Test questions | coderunner | Square function |
- And I disable UI plugins
+ And I disable UI plugins in the CodeRunner question type
Scenario: Preview the Python3 sqr function CodeRunner question submit two different wrong answers then the right answer
When I am on the "Square function" "core_question > preview" page logged in as teacher1
diff --git a/tests/behat/make_prototype.feature b/tests/behat/make_prototype.feature
index 90c1ed6f8..2d64db6f5 100644
--- a/tests/behat/make_prototype.feature
+++ b/tests/behat/make_prototype.feature
@@ -59,3 +59,21 @@ Feature: make_prototype
Then I should see "Passed all tests!"
And I should not see "Show differences"
And I should see "Marks for this submission: 1.00/1.00"
+
+ Scenario: As a teacher, I should not be allowed to edit the question type if it IS a user-defined prototype
+ When I am on the "PROTOTYPE_test_prototype" "core_question > edit" page logged in as teacher1
+ Then I should see "This is a prototype; cannot change question type"
+
+ Scenario: As a teacher, I should be allowed to edit the question type if it USES a user-defined prototype
+ When I am on the "Prototype tester" "core_question > edit" page logged in as teacher1
+ And I should see "python3_test_prototype"
+ And I set the field "id_coderunnertype" to "python3"
+ Then I should not see "This is a prototype; cannot change question type"
+
+ Scenario: As a teacher, I should be able to toggle the prototyping off and be able to edit the question type
+ When I am on the "PROTOTYPE_test_prototype" "core_question > edit" page logged in as teacher1
+ And I should see "This is a prototype; cannot change question type"
+ And I click on "a[aria-controls='id_advancedcustomisationheadercontainer']" "css_element"
+ And I set the field "prototypetype" to "No"
+ And I set the field "id_coderunnertype" to "python3" and dismiss the alert
+ Then I should not see "This is a prototype; cannot change question type"
diff --git a/tests/behat/run_python3_sqr_function_templated.feature b/tests/behat/run_python3_sqr_function_templated.feature
index 70684f7fe..50f7ad053 100644
--- a/tests/behat/run_python3_sqr_function_templated.feature
+++ b/tests/behat/run_python3_sqr_function_templated.feature
@@ -18,7 +18,7 @@ Feature: Combinator template is called test-by-test if a runtime error occurs wh
| contextlevel | reference | questioncategory | name |
| Course | C1 | Top | Behat Testing |
And I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
- And I disable UI plugins
+ And I disable UI plugins in the CodeRunner question type
And I add a "CodeRunner" question filling the form with:
| id_coderunnertype | python3 |
| name | sqr acceptance question |
diff --git a/tests/behat/set_uiplugin.feature b/tests/behat/set_uiplugin.feature
index ba66d5653..53f60b1d0 100644
--- a/tests/behat/set_uiplugin.feature
+++ b/tests/behat/set_uiplugin.feature
@@ -20,7 +20,7 @@ Feature: Check that a selected UI plugin is saved
And the following "questions" exist:
| questioncategory | qtype | name | template |
| Test questions | coderunner | Square function | sqr |
- And I enable UI plugins
+ And I enable UI plugins in the CodeRunner question type
Scenario: Selecting the Graph UI plugin results in a canvas being displayed
When I am on the "Square function" "core_question > edit" page logged in as teacher1
diff --git a/tests/behat/template_params_error.feature b/tests/behat/template_params_error.feature
new file mode 100644
index 000000000..a24d5c06c
--- /dev/null
+++ b/tests/behat/template_params_error.feature
@@ -0,0 +1,101 @@
+@qtype @qtype_coderunner @javascript
+Feature: template_params_error
+ In order to successfully edit CodeRunner question template parameters
+ As a teacher
+ I should get informative template parameter error messages
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@asd.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And the following "question categories" exist:
+ | contextlevel | reference | questioncategory | name |
+ | Course | C1 | Top | Behat Testing |
+ And I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
+ And I add a "CodeRunner" question filling the form with:
+ | id_coderunnertype | python3 |
+ | name | Dummy question |
+ | id_questiontext | Do nothing |
+ | id_testcode_0 | Helloworld |
+ | id_expected_0 | Helloworld |
+ And I disable UI plugins in the CodeRunner question type
+
+ Scenario: As a teacher, I should be given an informative Twig error
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_templateparamslang" to "twig"
+ And I set the following fields to these values:
+ | id_templateparams | {{ "error" * 3 }} |
+ And I should not see "A non-numeric value encountered in"
+ And I press "id_submitbutton"
+ And I should see "Template parameters must evaluate to blank or a valid JSON record"
+ Then I should see "A non-numeric value encountered in"
+
+ Scenario: As a teacher, I should be given an informative Python error
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_templateparamslang" to "python3"
+ And I set the following fields to these values:
+ | id_templateparams | print("error) |
+ And I should not see "SyntaxError"
+ And I press "id_submitbutton"
+ And I should see "Template parameters must evaluate to blank or a valid JSON record"
+ Then I should see "SyntaxError"
+
+ Scenario: As a teacher, I should be given an informative C error
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_templateparamslang" to "c"
+ And I set the following fields to these values:
+ | id_templateparams | #include character"
+ And I press "id_submitbutton"
+ And I should see "Template parameters must evaluate to blank or a valid JSON record"
+ Then I should see "error: missing terminating > character"
+
+ Scenario: As a teacher, I should be given an informative Java error
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_templateparamslang" to "java"
+ And I set the following fields to these values:
+ | id_templateparams | public static void main(String[] ar |
+ And I should not see "prog.java:1: error:"
+ And I press "id_submitbutton"
+ And I should see "Template parameters must evaluate to blank or a valid JSON record"
+ Then I should see "prog.java:1: error:"
+
+ Scenario: As a teacher, I should be given an informative php error
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_templateparamslang" to "php"
+ And I set the field "id_templateparams" to:
+ """
+
+ """
+ And I should not see "PHP Parse error:"
+ And I press "id_submitbutton"
+ And I should see "Template parameters must evaluate to blank or a valid JSON record"
+ Then I should see "PHP Parse error:"
+
+ Scenario: As a teacher, I should be given an informative Octave error
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_templateparamslang" to "octave"
+ And I set the following fields to these values:
+ | id_templateparams | component = [1 3 |
+ And I should not see "Run error"
+ And I press "id_submitbutton"
+ And I should see "Template parameters must evaluate to blank or a valid JSON record"
+ Then I should see "Run error"
+
+ Scenario: As a teacher, I should be given an informative Pascal error
+ When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
+ And I set the field "id_templateparamslang" to "pascal"
+ And I set the following fields to these values:
+ | id_templateparams | program AProgram(output |
+ And I should not see "Fatal: Syntax error"
+ And I press "id_submitbutton"
+ And I should see "Template parameters must evaluate to blank or a valid JSON record"
+ Then I should see "Fatal: Syntax error"
diff --git a/tests/behat/twigprefix.feature b/tests/behat/twigprefix.feature
index f74c1d428..71d8793db 100644
--- a/tests/behat/twigprefix.feature
+++ b/tests/behat/twigprefix.feature
@@ -1,4 +1,3 @@
-
@qtype @qtype_coderunner @javascript @twigprefixtests
Feature: twigprefix
When I define a template parameter __twigprefix__ in a prototype
@@ -20,7 +19,7 @@ Feature: twigprefix
| Course | C1 | Top | Behat Testing |
And I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
And I set CodeRunner behat testing flag
- And I disable UI plugins
+ And I disable UI plugins in the CodeRunner question type
And I press "Create a new question ..."
And I click on "input#item_qtype_coderunner" "css_element"
And I press "submitbutton"
From 49234a7157001fe66a626c4451c3bdcc07598c56 Mon Sep 17 00:00:00 2001
From: Michelle Hsieh
Date: Tue, 17 Jan 2023 18:48:24 +1300
Subject: [PATCH 037/188] Updated alert closing handling to be stable
---
tests/behat/behat_coderunner.php | 27 ++++++++++++++++++---------
1 file changed, 18 insertions(+), 9 deletions(-)
diff --git a/tests/behat/behat_coderunner.php b/tests/behat/behat_coderunner.php
index 20fc31ad5..bae290738 100644
--- a/tests/behat/behat_coderunner.php
+++ b/tests/behat/behat_coderunner.php
@@ -23,8 +23,8 @@
*/
use Behat\Mink\Exception\ExpectationException as ExpectationException;
-use WebDriver\Exception\NoAlertOpenError;
-use WebDriver\Exception\UnexpectedAlertOpen;
+use Facebook\WebDriver\Exception\NoSuchAlertException as NoSuchAlertException;
+
class behat_coderunner extends behat_base {
/**
@@ -146,16 +146,25 @@ public function i_fill_in_my_template() {
/**
* Sets the given field to a given value and dismisses the expected alert.
* @When /^I set the field "(?P(?:[^"]|\\")*)" to "(?P(?:[^"]|\\")*)" and dismiss the alert$/
- *
- * This is currently just a hack. I used to be able to catch UnexpectedAlertOpen
- * but that's not working any more. I can catch a general exception
*/
public function i_set_the_field_and_dismiss_the_alert($field, $value) {
+ // Gets the field.
+ $fielditem = behat_field_manager::get_form_field_from_label($field, $this);
+
+ // Makes sure there is a field before continuing.
+ if ($fielditem) {
+ $fielditem->set_value($value);
+ } else {
+ throw new ExpectationException("No field '{$field}' found.", $this->getSession());
+ }
+ // Gets you to wait for the pending JS alert by sleeping.
+ sleep(1);
try {
- $this->execute('behat_forms::i_set_the_field_to', array($field, $this->escape($value)));
- $this->getSession()->getDriver()->getWebDriver()->switchTo()->alert()->dismiss(); // This has started working again!
- } catch (Exception $e) { // For some reason UnexpectedAlertOpen can't be caught.
- return;
+ // Gets the alert and its text.
+ $alert = $this->getSession()->getDriver()->getWebDriver()->switchTo()->alert();
+ $alert->accept();
+ } catch (NoSuchAlertException $ex) {
+ throw new ExpectationException("No alert was triggered appropriately", $this->getSession());
}
}
}
From 62e71bbad687c515c547e46386c158bb1da9c289 Mon Sep 17 00:00:00 2001
From: Michelle Hsieh
Date: Thu, 19 Jan 2023 13:00:52 +1300
Subject: [PATCH 038/188] Changed Twig template params test to check UI errors
in php8
---
tests/behat/template_params_error.feature | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/tests/behat/template_params_error.feature b/tests/behat/template_params_error.feature
index a24d5c06c..50629e362 100644
--- a/tests/behat/template_params_error.feature
+++ b/tests/behat/template_params_error.feature
@@ -30,11 +30,11 @@ Feature: template_params_error
When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
And I set the field "id_templateparamslang" to "twig"
And I set the following fields to these values:
- | id_templateparams | {{ "error" * 3 }} |
- And I should not see "A non-numeric value encountered in"
+ | id_templateparams | {{ /error }} |
+ And I should not see "Unexpected token"
And I press "id_submitbutton"
And I should see "Template parameters must evaluate to blank or a valid JSON record"
- Then I should see "A non-numeric value encountered in"
+ Then I should see "Unexpected token"
Scenario: As a teacher, I should be given an informative Python error
When I am on the "Dummy question" "core_question > edit" page logged in as teacher1
From 5d51464b777f3360306dbf6b825a15f9b0d9ac01 Mon Sep 17 00:00:00 2001
From: James Napier <61300251+jimbonothing64@users.noreply.github.com>
Date: Sun, 22 Jan 2023 13:35:49 +1300
Subject: [PATCH 039/188] Development (#161)
* init
* init
* final init...
* UI to serialization working
* serialization to UI working (both ways)
* code grunted
* before altering serialization
* fixed bug where garbled serialization caused crash
* change serialisation to use object
* html bug in reload method fixed
* object serialisation for both text areas
* added checkbox (very big!)
* fixed checkbox and button sizes
* fixed prefix-ans checkbox not remembering state
* added show/hide scratchpad serialization
* show/hide tickbox now shows/hides sp
* make grunt happy
* sp is now hidden when serialization tells it to be
* grunt
* run button runs hello world program
* run button runs serialized field
* run button now runs code as it should
* spacing fixed
* Added error messaging and trunction for run
* fixed language choice for run
* refactored UI html code
* fixed bug were new scratchpad not serializing
* fixed scratchpad show/hide
* fixed scratchpad show/hide (again)
* refactor function out of object
* add ui param for 'run' button
* added scratchpad name ui param
* sp html display now works
* added new params
* changed sb_ prefix to sp_ for ui params
* added ui param for changing prefix with ans text
* remove stray sb_ prefixes
* added run specific language param
* refactored combine code to be function not method
* added ui_params for wrapper
* trial version of wrapper/template
* added global template/wrapper option functionality
* wrapper functionality working
* Cleaned up comments
* fixed sandbox params
* bug fix: sp display area now clears in text and html mode
* bug fix: globalextra
* bug fix: \n bug in answer code leading to unterminated strings etc.
* change names in wrapper
extract \n bugfix to wrapper
* added ace editor to top
* added ace editor to scratchpad (serial to reload broken)
* Fixed serialization with ace
* Ace added using two UI wrappers, each containing an ACE UI
* Refactored reload method
* Refactored sub-UI creation
* Added syntax highlighting for ace
* bug fix: undefined ID for sub-textareas
* bug fix: fixed size and resizing of answer-box
* bug fix: sizing, weird flashing while loading
* bug fix: remove list from serialization
* grunted
* Serialization when empty now empty string
* remove get fields method
* removed blob names, replaced with scratchpad
* renamed blob ui files to scratchpad ui files
* changed dual blob filenames to scratchpad filenames
* implement hasFocus method
* add configure sandbox method
* fixed a tag to be html compliant
* added scratchpad ui acceptance tests
* added scratchpad ui acceptance tests for using run and serialization
* added scratchpad ui acceptance tests for using run and serialization
* acceptance tests for using run
* bug fix: serialization empty unless scratchpad shown
* bug fix: sandbox not init for test properly
* Added feedback when run program provides no output
* fixed validate on save breaking tests
* added serialisation tests
* fixed serialisation tests
* I see in answer box added (no pystring)
* I see in answer box added pystring
* Changed name of test to match others better
* Added wrapper tests
* Fixed serialisation tests
* Cleaned up scenario descriptions
* added I set ace field function
* fixed tests to work with Ace editors
* bug fix: ace now allowed for Scratchpad
* bug fix: test with wrong deffinition
* cleaned up comments
* bug fix: clicking label checks checkbox now
* invert prefix ans default state and serialization
* prefix ans functionality now matches inverted serialisation
* match tests with new defualt prefix on state of SP
* cleaned up serialization inversion
* uninvert serialisation
* uninvert serialisation
* uninvert serialisation
* updated default prefix text
* added help button
* simplified ui param names
* added scratchpad ui strings
* added language string support
* added language strings for sp
* changed test's UI param names to match new names
* cleaned up spacing, semicolons
* removed sp_ prefixes
* added langauge string support for buttons, help text ect
* spacing
* fixed wrong direction of scratchpad arrow
* clean up help popover
* switch show/hide to use bootstrap collapse
* added help_text UI param for setting help text
* modified wrapper_src to alow prototypeextra field use, disallowed wrapper entry
* fixed slow loading ace bug
* added spacing between precheck checkbox and label
* bug fix: scratchpad button oppening ALL scratchpads
* changed serialization to use lists
* fixed tab highlight being too big for space on scratchpad bar
* bug fix: scratchpad name param and lang string replacement
* bug fix: propper listy serialization for answer_code and test_code
* update tests to reflect changes to design
* update tests to test serialization missing fields
* ui can now handle serialization missing fields
* bug fix: ace not reading lang correctly
* added disable scratchpad ui param
* Eslint
* gurkin lint
* make use of mustache templates, jquerry removal
* re-add functionality to overwrite langauge strings with ui params
* switch to class notation
* es6ify
* es6
* commit before pull
* fix show/hide serialization
---
amd/build/graphutil.min.js | 2 +-
amd/build/graphutil.min.js.map | 2 +-
amd/build/textareas.min.js | 2 +-
amd/build/textareas.min.js.map | 2 +-
amd/build/ui_ace.min.js | 2 +-
amd/build/ui_ace.min.js.map | 2 +-
amd/build/ui_ace_gapfiller.min.js | 2 +-
amd/build/ui_ace_gapfiller.min.js.map | 2 +-
amd/build/userinterfacewrapper.min.js | 2 +-
amd/build/userinterfacewrapper.min.js.map | 2 +-
amd/src/ui_ace.js | 12 +
amd/src/ui_scratchpad.js | 506 +++++++++++++++++++++
amd/src/ui_scratchpad.json | 41 ++
classes/util.php | 2 +-
lang/en/qtype_coderunner.php | 16 +
templates/answer_textarea.mustache | 36 ++
templates/help_icon.mustache | 41 ++
templates/output_displayarea.mustache | 30 ++
templates/scratchpad.mustache | 61 +++
templates/scratchpad_controls.mustache | 43 ++
templates/scratchpad_ui.mustache | 48 ++
tests/behat/attachmentimportexport.feature | 1 +
tests/behat/behat_coderunner.php | 100 ++++
tests/behat/scratchpad_ui.feature | 291 ++++++++++++
tests/behat/scratchpad_ui_params.feature | 234 ++++++++++
25 files changed, 1471 insertions(+), 11 deletions(-)
create mode 100644 amd/src/ui_scratchpad.js
create mode 100644 amd/src/ui_scratchpad.json
create mode 100644 templates/answer_textarea.mustache
create mode 100644 templates/help_icon.mustache
create mode 100644 templates/output_displayarea.mustache
create mode 100644 templates/scratchpad.mustache
create mode 100644 templates/scratchpad_controls.mustache
create mode 100644 templates/scratchpad_ui.mustache
create mode 100644 tests/behat/scratchpad_ui.feature
create mode 100644 tests/behat/scratchpad_ui_params.feature
diff --git a/amd/build/graphutil.min.js b/amd/build/graphutil.min.js
index feb02fc20..93b742731 100644
--- a/amd/build/graphutil.min.js
+++ b/amd/build/graphutil.min.js
@@ -1,3 +1,3 @@
-define("qtype_coderunner/graphutil",(function(){function Util(){this.greekLetterNames=["Alpha","Beta","Gamma","Delta","Epsilon","Zeta","Eta","Theta","Iota","Kappa","Lambda","Mu","Nu","Xi","Omicron","Pi","Rho","Sigma","Tau","Upsilon","Phi","Chi","Psi","Omega"]}return Util.prototype.convertLatexShortcuts=function(text){for(var i=0;i16)))).replace(new RegExp("\\\\"+name.toLowerCase(),"g"),String.fromCharCode(945+i+(i>16)))}for(i=0;i<10;i++)text=text.replace(new RegExp("_"+i,"g"),String.fromCharCode(8320+i));return text=text.replace(new RegExp("_a","g"),String.fromCharCode(8336))},Util.prototype.drawArrow=function(c,x,y,angle){var dx=Math.cos(angle),dy=Math.sin(angle);c.beginPath(),c.moveTo(x,y),c.lineTo(x-8*dx+5*dy,y-8*dy-5*dx),c.lineTo(x-8*dx-5*dy,y-8*dy+5*dx),c.fill()},Util.prototype.det=function(a,b,c,d,e,f,g,h,i){return a*e*i+b*f*g+c*d*h-a*f*h-b*d*i-c*e*g},Util.prototype.vectorMagnitude=function(v){return Math.sqrt(v.x*v.x+v.y*v.y)},Util.prototype.scalarProjection=function(a,b){return(a.x*b.x+a.y*b.y)/this.vectorMagnitude(b)},Util.prototype.isCCW=function(a,b){return a.x*b.y-b.x*a.y>0},Util.prototype.circleFromThreePoints=function(x1,y1,x2,y2,x3,y3){var a=this.det(x1,y1,1,x2,y2,1,x3,y3,1),bx=-this.det(x1*x1+y1*y1,y1,1,x2*x2+y2*y2,y2,1,x3*x3+y3*y3,y3,1),by=this.det(x1*x1+y1*y1,x1,1,x2*x2+y2*y2,x2,1,x3*x3+y3*y3,x3,1),c=-this.det(x1*x1+y1*y1,x1,y1,x2*x2+y2*y2,x2,y2,x3*x3+y3*y3,x3,y3);return{x:-bx/(2*a),y:-by/(2*a),radius:Math.sqrt(bx*bx+by*by-4*a*c)/(2*Math.abs(a))}},Util.prototype.isInside=function(pos,rect){return pos.x>rect.x&&pos.xrect.y},Util.prototype.crossBrowserKey=function(e){return(e=e||window.event).which||e.keyCode},Util.prototype.crossBrowserRelativeMousePos=function(e){var rect=e.target.getBoundingClientRect();return{x:e.clientX-rect.left,y:e.clientY-rect.top}},new Util}));
+define("qtype_coderunner/graphutil",(function(){function Util(){this.greekLetterNames=["Alpha","Beta","Gamma","Delta","Epsilon","Zeta","Eta","Theta","Iota","Kappa","Lambda","Mu","Nu","Xi","Omicron","Pi","Rho","Sigma","Tau","Upsilon","Phi","Chi","Psi","Omega"]}return Util.prototype.convertLatexShortcuts=function(text){for(var i=0;i16)))).replace(new RegExp("\\\\"+name.toLowerCase(),"g"),String.fromCharCode(945+i+(i>16)))}for(i=0;i<10;i++)text=text.replace(new RegExp("_"+i,"g"),String.fromCharCode(8320+i));return text=text.replace(new RegExp("_a","g"),String.fromCharCode(8336))},Util.prototype.drawArrow=function(c,x,y,angle){var dx=Math.cos(angle),dy=Math.sin(angle);c.beginPath(),c.moveTo(x,y),c.lineTo(x-8*dx+5*dy,y-8*dy-5*dx),c.lineTo(x-8*dx-5*dy,y-8*dy+5*dx),c.fill()},Util.prototype.det=function(a,b,c,d,e,f,g,h,i){return a*e*i+b*f*g+c*d*h-a*f*h-b*d*i-c*e*g},Util.prototype.vectorMagnitude=function(v){return Math.sqrt(v.x*v.x+v.y*v.y)},Util.prototype.scalarProjection=function(a,b){return(a.x*b.x+a.y*b.y)/this.vectorMagnitude(b)},Util.prototype.isCCW=function(a,b){return a.x*b.y-b.x*a.y>0},Util.prototype.circleFromThreePoints=function(x1,y1,x2,y2,x3,y3){var a=this.det(x1,y1,1,x2,y2,1,x3,y3,1),bx=-this.det(x1*x1+y1*y1,y1,1,x2*x2+y2*y2,y2,1,x3*x3+y3*y3,y3,1),by=this.det(x1*x1+y1*y1,x1,1,x2*x2+y2*y2,x2,1,x3*x3+y3*y3,x3,1),c=-this.det(x1*x1+y1*y1,x1,y1,x2*x2+y2*y2,x2,y2,x3*x3+y3*y3,x3,y3);return{x:-bx/(2*a),y:-by/(2*a),radius:Math.sqrt(bx*bx+by*by-4*a*c)/(2*Math.abs(a))}},Util.prototype.isInside=function(pos,rect){return pos.x>rect.x&&pos.xrect.y},Util.prototype.crossBrowserKey=function(e){return(e=e||window.event).which||e.keyCode},Util.prototype.crossBrowserRelativeMousePos=function(e){const rect=e.target.getBoundingClientRect();return{x:e.clientX-rect.left,y:e.clientY-rect.top}},new Util}));
//# sourceMappingURL=graphutil.min.js.map
\ No newline at end of file
diff --git a/amd/build/graphutil.min.js.map b/amd/build/graphutil.min.js.map
index 5f11864b8..23ac65002 100644
--- a/amd/build/graphutil.min.js.map
+++ b/amd/build/graphutil.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"graphutil.min.js","sources":["../src/graphutil.js"],"sourcesContent":["/***********************************************************************\n *\n * Utility functions/data/constants for the ui_graph module.\n *\n ***********************************************************************/\n// Most of this code is taken from Finite State Machine Designer:\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(function() {\n /**\n * Contstructor for the Util class.\n */\n function Util() {\n\n this.greekLetterNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',\n 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda',\n 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',\n 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega' ];\n }\n\n Util.prototype.convertLatexShortcuts = function(text) {\n // Html greek characters.\n for(var i = 0; i < this.greekLetterNames.length; i++) {\n var name = this.greekLetterNames[i];\n text = text.replace(new RegExp('\\\\\\\\' + name, 'g'), String.fromCharCode(913 + i + (i > 16)));\n text = text.replace(new RegExp('\\\\\\\\' + name.toLowerCase(), 'g'), String.fromCharCode(945 + i + (i > 16)));\n }\n\n // Subscripts.\n for(var i = 0; i < 10; i++) {\n text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));\n }\n text = text.replace(new RegExp('_a', 'g'), String.fromCharCode(8336));\n return text;\n };\n\n Util.prototype.drawArrow = function(c, x, y, angle) {\n // Draw an arrow head on the graphics context c at (x, y) with given angle.\n\n var dx = Math.cos(angle);\n var dy = Math.sin(angle);\n c.beginPath();\n c.moveTo(x, y);\n c.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);\n c.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);\n c.fill();\n };\n\n Util.prototype.det = function(a, b, c, d, e, f, g, h, i) {\n // Determinant of given matrix elements.\n return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g;\n };\n\n Util.prototype.vectorMagnitude = function(v){\n // Returns magnitude (length) of a vector v\n return Math.sqrt(v.x * v.x + v.y * v.y);\n };\n\n Util.prototype.scalarProjection = function(a, b) {\n // Returns scalar projection of vector a onto vector b\n return (a.x * b.x + a.y * b.y) / this.vectorMagnitude(b);\n };\n\n Util.prototype.isCCW = function(a, b) {\n // Returns true iff vector b is in a counter-clockwise orientation relative to a\n return (a.x * b.y) - (b.x * a.y) > 0;\n };\n\n Util.prototype.circleFromThreePoints = function(x1, y1, x2, y2, x3, y3) {\n // Return {x, y, radius} of circle through (x1, y1), (x2, y2), (x3, y3).\n var a = this.det(x1, y1, 1, x2, y2, 1, x3, y3, 1);\n var bx = -this.det(x1 * x1 + y1 * y1, y1, 1, x2 * x2 + y2 * y2, y2, 1, x3 * x3 + y3 * y3, y3, 1);\n var by = this.det(x1 * x1 + y1 * y1, x1, 1, x2 * x2 + y2 * y2, x2, 1, x3 * x3 + y3 * y3, x3, 1);\n var c = -this.det(x1 * x1 + y1 * y1, x1, y1, x2 * x2 + y2 * y2, x2, y2, x3 * x3 + y3 * y3, x3, y3);\n return {\n 'x': -bx / (2 * a),\n 'y': -by / (2 * a),\n 'radius': Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))\n };\n };\n\n Util.prototype.isInside = function(pos, rect) {\n // True iff given point pos is inside rectangle.\n return pos.x > rect.x && pos.x < rect.x + rect.width && pos.y < rect.y + rect.height && pos.y > rect.y;\n };\n\n Util.prototype.crossBrowserKey = function(e) {\n // Return which key was pressed, given the event, in a browser-independent way.\n e = e || window.event;\n return e.which || e.keyCode;\n };\n\n\n Util.prototype.crossBrowserRelativeMousePos = function(e) {\n // Earlier complex version was breaking in Moodle 4, so replaced\n // with this much simpler version that should work with all modern\n // browsers.\n const rect = e.target.getBoundingClientRect();\n const x = e.clientX - rect.left; //x position within the element.\n const y = e.clientY - rect.top; //y position within the element.\n return {'x': x, 'y': y};\n };\n\n return new Util();\n});\n"],"names":["define","Util","greekLetterNames","prototype","convertLatexShortcuts","text","i","this","length","name","replace","RegExp","String","fromCharCode","toLowerCase","drawArrow","c","x","y","angle","dx","Math","cos","dy","sin","beginPath","moveTo","lineTo","fill","det","a","b","d","e","f","g","h","vectorMagnitude","v","sqrt","scalarProjection","isCCW","circleFromThreePoints","x1","y1","x2","y2","x3","y3","bx","by","abs","isInside","pos","rect","width","height","crossBrowserKey","window","event","which","keyCode","crossBrowserRelativeMousePos","target","getBoundingClientRect","clientX","left","clientY","top"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"graphutil.min.js","sources":["../src/graphutil.js"],"sourcesContent":["/***********************************************************************\n *\n * Utility functions/data/constants for the ui_graph module.\n *\n ***********************************************************************/\n// Most of this code is taken from Finite State Machine Designer:\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(function() {\n /**\n * Contstructor for the Util class.\n */\n function Util() {\n\n this.greekLetterNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',\n 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda',\n 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',\n 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega' ];\n }\n\n Util.prototype.convertLatexShortcuts = function(text) {\n // Html greek characters.\n for(var i = 0; i < this.greekLetterNames.length; i++) {\n var name = this.greekLetterNames[i];\n text = text.replace(new RegExp('\\\\\\\\' + name, 'g'), String.fromCharCode(913 + i + (i > 16)));\n text = text.replace(new RegExp('\\\\\\\\' + name.toLowerCase(), 'g'), String.fromCharCode(945 + i + (i > 16)));\n }\n\n // Subscripts.\n for(var i = 0; i < 10; i++) {\n text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));\n }\n text = text.replace(new RegExp('_a', 'g'), String.fromCharCode(8336));\n return text;\n };\n\n Util.prototype.drawArrow = function(c, x, y, angle) {\n // Draw an arrow head on the graphics context c at (x, y) with given angle.\n\n var dx = Math.cos(angle);\n var dy = Math.sin(angle);\n c.beginPath();\n c.moveTo(x, y);\n c.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);\n c.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);\n c.fill();\n };\n\n Util.prototype.det = function(a, b, c, d, e, f, g, h, i) {\n // Determinant of given matrix elements.\n return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g;\n };\n\n Util.prototype.vectorMagnitude = function(v){\n // Returns magnitude (length) of a vector v\n return Math.sqrt(v.x * v.x + v.y * v.y);\n };\n\n Util.prototype.scalarProjection = function(a, b) {\n // Returns scalar projection of vector a onto vector b\n return (a.x * b.x + a.y * b.y) / this.vectorMagnitude(b);\n };\n\n Util.prototype.isCCW = function(a, b) {\n // Returns true iff vector b is in a counter-clockwise orientation relative to a\n return (a.x * b.y) - (b.x * a.y) > 0;\n };\n\n Util.prototype.circleFromThreePoints = function(x1, y1, x2, y2, x3, y3) {\n // Return {x, y, radius} of circle through (x1, y1), (x2, y2), (x3, y3).\n var a = this.det(x1, y1, 1, x2, y2, 1, x3, y3, 1);\n var bx = -this.det(x1 * x1 + y1 * y1, y1, 1, x2 * x2 + y2 * y2, y2, 1, x3 * x3 + y3 * y3, y3, 1);\n var by = this.det(x1 * x1 + y1 * y1, x1, 1, x2 * x2 + y2 * y2, x2, 1, x3 * x3 + y3 * y3, x3, 1);\n var c = -this.det(x1 * x1 + y1 * y1, x1, y1, x2 * x2 + y2 * y2, x2, y2, x3 * x3 + y3 * y3, x3, y3);\n return {\n 'x': -bx / (2 * a),\n 'y': -by / (2 * a),\n 'radius': Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))\n };\n };\n\n Util.prototype.isInside = function(pos, rect) {\n // True iff given point pos is inside rectangle.\n return pos.x > rect.x && pos.x < rect.x + rect.width && pos.y < rect.y + rect.height && pos.y > rect.y;\n };\n\n Util.prototype.crossBrowserKey = function(e) {\n // Return which key was pressed, given the event, in a browser-independent way.\n e = e || window.event;\n return e.which || e.keyCode;\n };\n\n\n Util.prototype.crossBrowserRelativeMousePos = function(e) {\n // Earlier complex version was breaking in Moodle 4, so replaced\n // with this much simpler version that should work with all modern\n // browsers.\n const rect = e.target.getBoundingClientRect();\n const x = e.clientX - rect.left; //x position within the element.\n const y = e.clientY - rect.top; //y position within the element.\n return {'x': x, 'y': y};\n };\n\n return new Util();\n});\n"],"names":["define","Util","greekLetterNames","prototype","convertLatexShortcuts","text","i","this","length","name","replace","RegExp","String","fromCharCode","toLowerCase","drawArrow","c","x","y","angle","dx","Math","cos","dy","sin","beginPath","moveTo","lineTo","fill","det","a","b","d","e","f","g","h","vectorMagnitude","v","sqrt","scalarProjection","isCCW","circleFromThreePoints","x1","y1","x2","y2","x3","y3","bx","by","abs","isInside","pos","rect","width","height","crossBrowserKey","window","event","which","keyCode","crossBrowserRelativeMousePos","target","getBoundingClientRect","clientX","left","clientY","top"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/textareas.min.js b/amd/build/textareas.min.js
index 3826a6463..f7c2df89a 100644
--- a/amd/build/textareas.min.js
+++ b/amd/build/textareas.min.js
@@ -6,6 +6,6 @@
* @copyright Richard Lobb, 2015, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/textareas",["jquery"],(function($){function initTextArea(){$(this).data("clickInProgress",!1),$(this).data("capturingTab",!0),$(this).on("mousedown",(function(){$(this).data("clickInProgress",!0)})),$(this).on("focusin",(function(){$(this).data("capturingTab",$(this).data("clickInProgress"))})),$(this).on("click",(function(){$(this).data("clickInProgress",!1)})),$(this).on("keydown",(function(e){if(!(window.hasOwnProperty("behattesting")&&window.behattesting||void 0!==e.which&&0===e.which))if(9==e.keyCode&&$(this).data("capturingTab"))(e.shiftKey||insertString(this," "))&&e.preventDefault();else if(13===e.keyCode&&void 0!==this.selectionStart){for(var before=this.value.substring(0,this.selectionStart),eol=before.lastIndexOf("\n"),line=before.substring(eol+1),indent="",i=0;i{div.is(":visible")?(div.hide(),hdr.innerText=hdrText.replace("▾","▸")):(div.show(),div.trigger("mousemove"),hdr.innerText=hdrText.replace("▸","▾"))}))}}}));
//# sourceMappingURL=textareas.min.js.map
\ No newline at end of file
diff --git a/amd/build/textareas.min.js.map b/amd/build/textareas.min.js.map
index a8cbe49a4..617c95e64 100644
--- a/amd/build/textareas.min.js.map
+++ b/amd/build/textareas.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"textareas.min.js","sources":["../src/textareas.js"],"sourcesContent":["/**\n * 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/**\n * JavaScript for handling textareas and form actions in CodeRunner question\n * editing forms and student question answering forms.\n *\n * @module qtype_coderunner/textareas\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Function to initialise all code-input text-areas in a page.\n * Used by the form editor but can't be used for question text areas as\n * renderer.php is called once for each question in a quiz, and there is\n * no communication between the questions.\n */\n function setupAllTAs() {\n $('textarea.edit_code').each(initTextArea);\n }\n\n /**\n * Initialise a particular text area (TA), given its ID (which may contain\n * colons, so can't use jQuery selector).\n * @param {string} taId The ID of the textarea html element.\n */\n function initQuestionTA(taId) {\n $(document.getElementById(taId)).each(initTextArea);\n }\n\n\n /**\n * Set up to expand or collapse the question author's answer when the user clicks\n * on the show/hide answer button.\n * @param {string} linkId The ID of the link that the user clicks.\n * @param {sting} divId The ID of the div to be shown or hidden.\n */\n function setupShowHideAnswer(linkId, divId) {\n let link = $(document.getElementById(linkId)),\n div = $(document.getElementById(divId)),\n hdr = link.children()[0], // The
element.\n hdrText = hdr.innerText; // Its body.\n link.click(() => {\n if (div.is(\":visible\")) {\n div.hide();\n hdr.innerText = hdrText.replace(\"\\u25BE\", \"\\u25B8\");\n } else {\n div.show();\n div.trigger('mousemove'); // So user sees contents.\n hdr.innerText = hdrText.replace(\"\\u25B8\", \"\\u25BE\");\n }\n });\n }\n\n /**\n * Set up the JavaScript to handle a text area 'this'.\n * It just does rudimentary autoindent on return and replaces tabs with\n * 4 spaces always.\n * For info on key handling browser inconsistencies see\n * http://unixpapa.com/js/key.html\n * If the AceEditor is handling a text area, this code is unused as\n * the actual textarea is hidden by Ace.\n */\n function initTextArea() {\n var TAB = 9,\n ENTER = 13,\n ESC = 27,\n KEY_M = 77;\n\n $(this).data('clickInProgress', false);\n $(this).data('capturingTab', true);\n\n $(this).on('mousedown', function() {\n /*\n * Event order seems to be (\\ is where the mouse button is pressed, / released):\n * Chrome: \\ mousedown, mouseup, focusin / click.\n * Firefox/IE: \\ mousedown, focusin / mouseup, click.\n */\n $(this).data('clickInProgress', true);\n });\n\n $(this).on('focusin', function() {\n /*\n * At first, pressing TAB moves focus.\n */\n $(this).data('capturingTab', $(this).data('clickInProgress'));\n });\n\n $(this).on('click', function() {\n $(this).data('clickInProgress', false);\n });\n\n $(this).on('keydown', function(e) {\n /*\n * Don't autoindent when behat testing in progress.\n */\n if (window.hasOwnProperty('behattesting') && window.behattesting) { return; }\n\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode == TAB && $(this).data('capturingTab')) {\n /*\n * Ignore SHIFT/TAB. Insert 4 spaces on TAB.\n */\n if (e.shiftKey || insertString(this, \" \")) {\n e.preventDefault();\n }\n }\n else if (e.keyCode === ENTER && this.selectionStart !== undefined) {\n /*\n * Handle autoindent only on non-IE.\n */\n var before = this.value.substring(0, this.selectionStart);\n var eol = before.lastIndexOf(\"\\n\");\n var line = before.substring(eol + 1); // Take from eol to end.\n var indent = \"\";\n for (var i = 0; i < line.length && line.charAt(i) === ' '; i++) {\n indent = indent + \" \";\n }\n if (insertString(this, \"\\n\" + indent)) {\n e.preventDefault();\n }\n /*\n * Once the user has started typing, TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n else if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n /*\n * CTRL + M toggles TAB capturing mode.\n * This is the short-cut recommended by\n * https:www.w3.org/TR/wai-aria-practices/#richtext.\n */\n $(this).data('capturingTab', !$(this).data('capturingTab'));\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n /*\n * ESC always stops capturing TAB.\n */\n $(this).data('capturingTab', false);\n }\n else if (!(e.ctrlKey || e.altKey)) {\n /*\n * Once the user has started typing (not modifier keys) TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n }\n });\n }\n\n /**\n * Insert into the given textarea ta the given string sToInsert.\n * @param {html_element} ta The textarea to be updated.\n * @param {string} sToInsert The string to be inserted at the current selection\n * point.\n */\n function insertString(ta, sToInsert) {\n if (ta.selectionStart !== undefined) { // Firefox etc.\n var before = ta.value.substring(0, ta.selectionStart);\n var selSave = ta.selectionEnd;\n var after = ta.value.substring(ta.selectionEnd, ta.value.length);\n\n /**\n * Update the text field.\n */\n var tmp = ta.scrollTop; // Inhibit annoying auto-scroll.\n ta.value = before + sToInsert + after;\n var pos = selSave + sToInsert.length;\n ta.selectionStart = pos;\n ta.selectionEnd = pos;\n ta.scrollTop = tmp;\n return true;\n\n }\n else if (document.selection && document.selection.createRange) { // IE.\n /*\n * TODO: check if this still works. OLD CODE.\n */\n var r = document.selection.createRange();\n var dr = r.duplicate();\n dr.moveToElementText(ta);\n dr.setEndPoint(\"EndToEnd\", r);\n r.text = sToInsert;\n return true;\n }\n /*\n * Other browsers we can't handle.\n */\n else {\n return false;\n }\n }\n\n return {\n setupAllTAs: setupAllTAs,\n initQuestionTA: initQuestionTA,\n setupShowHideAnswer: setupShowHideAnswer,\n };\n});\n"],"names":["define","$","initTextArea","this","data","on","e","window","hasOwnProperty","behattesting","undefined","which","keyCode","shiftKey","insertString","preventDefault","selectionStart","before","value","substring","eol","lastIndexOf","line","indent","i","length","charAt","ctrlKey","altKey","ta","sToInsert","selSave","selectionEnd","after","tmp","scrollTop","pos","document","selection","createRange","r","dr","duplicate","moveToElementText","setEndPoint","text","setupAllTAs","each","initQuestionTA","taId","getElementById","setupShowHideAnswer","linkId","divId","link","div","hdr","children","hdrText","innerText","click","is","hide","replace","show","trigger"],"mappings":";;;;;;;;AA2BAA,oCAAO,CAAC,WAAW,SAASC,YAsDfC,eAMLD,EAAEE,MAAMC,KAAK,mBAAmB,GAChCH,EAAEE,MAAMC,KAAK,gBAAgB,GAE7BH,EAAEE,MAAME,GAAG,aAAa,WAMpBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,WAIlBJ,EAAEE,MAAMC,KAAK,eAAgBH,EAAEE,MAAMC,KAAK,uBAG9CH,EAAEE,MAAME,GAAG,SAAS,WAChBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,SAASC,QAIvBC,OAAOC,eAAe,iBAAmBD,OAAOE,mBAEpCC,IAAZJ,EAAEK,OAAmC,IAAZL,EAAEK,UAlCzB,GAmCEL,EAAEM,SAAkBX,EAAEE,MAAMC,KAAK,iBAI7BE,EAAEO,UAAYC,aAAaX,KAAM,UACjCG,EAAES,sBAGL,GA1CD,KA0CKT,EAAEM,cAA6CF,IAAxBP,KAAKa,eAA8B,SAI3DC,OAASd,KAAKe,MAAMC,UAAU,EAAGhB,KAAKa,gBACtCI,IAAMH,OAAOI,YAAY,MACzBC,KAAOL,OAAOE,UAAUC,IAAM,GAC9BG,OAAS,GACJC,EAAI,EAAGA,EAAIF,KAAKG,QAA6B,MAAnBH,KAAKI,OAAOF,GAAYA,IACvDD,QAAkB,IAElBT,aAAaX,KAAM,KAAOoB,SAC1BjB,EAAES,iBAKNd,EAAEE,MAAMC,KAAK,gBAAgB,QAzD7B,KA2DKE,EAAEM,SAAqBN,EAAEqB,UAAYrB,EAAEsB,QAM5C3B,EAAEE,MAAMC,KAAK,gBAAiBH,EAAEE,MAAMC,KAAK,iBAC3CE,EAAES,kBAnEJ,KAqEOT,EAAEM,QAIPX,EAAEE,MAAMC,KAAK,gBAAgB,GAEtBE,EAAEqB,SAAWrB,EAAEsB,QAItB3B,EAAEE,MAAMC,KAAK,gBAAgB,eAYpCU,aAAae,GAAIC,mBACIpB,IAAtBmB,GAAGb,eAA8B,KAC7BC,OAASY,GAAGX,MAAMC,UAAU,EAAGU,GAAGb,gBAClCe,QAAUF,GAAGG,aACbC,MAAQJ,GAAGX,MAAMC,UAAUU,GAAGG,aAAcH,GAAGX,MAAMO,QAKrDS,IAAML,GAAGM,UACbN,GAAGX,MAAQD,OAASa,UAAYG,UAC5BG,IAAML,QAAUD,UAAUL,cAC9BI,GAAGb,eAAiBoB,IACpBP,GAAGG,aAAeI,IAClBP,GAAGM,UAAYD,KACR,EAGN,GAAIG,SAASC,WAAaD,SAASC,UAAUC,YAAa,KAIvDC,EAAIH,SAASC,UAAUC,cACvBE,GAAKD,EAAEE,mBACXD,GAAGE,kBAAkBd,IACrBY,GAAGG,YAAY,WAAYJ,GAC3BA,EAAEK,KAAOf,WACF,SAMA,QAIR,CACHgB,uBAjLA7C,EAAE,sBAAsB8C,KAAK7C,eAkL7B8C,wBA1KoBC,MACpBhD,EAAEoC,SAASa,eAAeD,OAAOF,KAAK7C,eA0KtCiD,6BAhKyBC,OAAQC,WAC7BC,KAAOrD,EAAEoC,SAASa,eAAeE,SACjCG,IAAMtD,EAAEoC,SAASa,eAAeG,QAChCG,IAAMF,KAAKG,WAAW,GACtBC,QAAUF,IAAIG,UAClBL,KAAKM,OAAM,WACHL,IAAIM,GAAG,aACPN,IAAIO,OACJN,IAAIG,UAAYD,QAAQK,QAAQ,IAAU,OAE1CR,IAAIS,OACJT,IAAIU,QAAQ,aACZT,IAAIG,UAAYD,QAAQK,QAAQ,IAAU"}
\ No newline at end of file
+{"version":3,"file":"textareas.min.js","sources":["../src/textareas.js"],"sourcesContent":["/**\n * 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/**\n * JavaScript for handling textareas and form actions in CodeRunner question\n * editing forms and student question answering forms.\n *\n * @module qtype_coderunner/textareas\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Function to initialise all code-input text-areas in a page.\n * Used by the form editor but can't be used for question text areas as\n * renderer.php is called once for each question in a quiz, and there is\n * no communication between the questions.\n */\n function setupAllTAs() {\n $('textarea.edit_code').each(initTextArea);\n }\n\n /**\n * Initialise a particular text area (TA), given its ID (which may contain\n * colons, so can't use jQuery selector).\n * @param {string} taId The ID of the textarea html element.\n */\n function initQuestionTA(taId) {\n $(document.getElementById(taId)).each(initTextArea);\n }\n\n\n /**\n * Set up to expand or collapse the question author's answer when the user clicks\n * on the show/hide answer button.\n * @param {string} linkId The ID of the link that the user clicks.\n * @param {sting} divId The ID of the div to be shown or hidden.\n */\n function setupShowHideAnswer(linkId, divId) {\n let link = $(document.getElementById(linkId)),\n div = $(document.getElementById(divId)),\n hdr = link.children()[0], // The
element.\n hdrText = hdr.innerText; // Its body.\n link.click(() => {\n if (div.is(\":visible\")) {\n div.hide();\n hdr.innerText = hdrText.replace(\"\\u25BE\", \"\\u25B8\");\n } else {\n div.show();\n div.trigger('mousemove'); // So user sees contents.\n hdr.innerText = hdrText.replace(\"\\u25B8\", \"\\u25BE\");\n }\n });\n }\n\n /**\n * Set up the JavaScript to handle a text area 'this'.\n * It just does rudimentary autoindent on return and replaces tabs with\n * 4 spaces always.\n * For info on key handling browser inconsistencies see\n * http://unixpapa.com/js/key.html\n * If the AceEditor is handling a text area, this code is unused as\n * the actual textarea is hidden by Ace.\n */\n function initTextArea() {\n var TAB = 9,\n ENTER = 13,\n ESC = 27,\n KEY_M = 77;\n\n $(this).data('clickInProgress', false);\n $(this).data('capturingTab', true);\n\n $(this).on('mousedown', function() {\n /*\n * Event order seems to be (\\ is where the mouse button is pressed, / released):\n * Chrome: \\ mousedown, mouseup, focusin / click.\n * Firefox/IE: \\ mousedown, focusin / mouseup, click.\n */\n $(this).data('clickInProgress', true);\n });\n\n $(this).on('focusin', function() {\n /*\n * At first, pressing TAB moves focus.\n */\n $(this).data('capturingTab', $(this).data('clickInProgress'));\n });\n\n $(this).on('click', function() {\n $(this).data('clickInProgress', false);\n });\n\n $(this).on('keydown', function(e) {\n /*\n * Don't autoindent when behat testing in progress.\n */\n if (window.hasOwnProperty('behattesting') && window.behattesting) { return; }\n\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode == TAB && $(this).data('capturingTab')) {\n /*\n * Ignore SHIFT/TAB. Insert 4 spaces on TAB.\n */\n if (e.shiftKey || insertString(this, \" \")) {\n e.preventDefault();\n }\n }\n else if (e.keyCode === ENTER && this.selectionStart !== undefined) {\n /*\n * Handle autoindent only on non-IE.\n */\n var before = this.value.substring(0, this.selectionStart);\n var eol = before.lastIndexOf(\"\\n\");\n var line = before.substring(eol + 1); // Take from eol to end.\n var indent = \"\";\n for (var i = 0; i < line.length && line.charAt(i) === ' '; i++) {\n indent = indent + \" \";\n }\n if (insertString(this, \"\\n\" + indent)) {\n e.preventDefault();\n }\n /*\n * Once the user has started typing, TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n else if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n /*\n * CTRL + M toggles TAB capturing mode.\n * This is the short-cut recommended by\n * https:www.w3.org/TR/wai-aria-practices/#richtext.\n */\n $(this).data('capturingTab', !$(this).data('capturingTab'));\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n /*\n * ESC always stops capturing TAB.\n */\n $(this).data('capturingTab', false);\n }\n else if (!(e.ctrlKey || e.altKey)) {\n /*\n * Once the user has started typing (not modifier keys) TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n }\n });\n }\n\n /**\n * Insert into the given textarea ta the given string sToInsert.\n * @param {html_element} ta The textarea to be updated.\n * @param {string} sToInsert The string to be inserted at the current selection\n * point.\n */\n function insertString(ta, sToInsert) {\n if (ta.selectionStart !== undefined) { // Firefox etc.\n var before = ta.value.substring(0, ta.selectionStart);\n var selSave = ta.selectionEnd;\n var after = ta.value.substring(ta.selectionEnd, ta.value.length);\n\n /**\n * Update the text field.\n */\n var tmp = ta.scrollTop; // Inhibit annoying auto-scroll.\n ta.value = before + sToInsert + after;\n var pos = selSave + sToInsert.length;\n ta.selectionStart = pos;\n ta.selectionEnd = pos;\n ta.scrollTop = tmp;\n return true;\n\n }\n else if (document.selection && document.selection.createRange) { // IE.\n /*\n * TODO: check if this still works. OLD CODE.\n */\n var r = document.selection.createRange();\n var dr = r.duplicate();\n dr.moveToElementText(ta);\n dr.setEndPoint(\"EndToEnd\", r);\n r.text = sToInsert;\n return true;\n }\n /*\n * Other browsers we can't handle.\n */\n else {\n return false;\n }\n }\n\n return {\n setupAllTAs: setupAllTAs,\n initQuestionTA: initQuestionTA,\n setupShowHideAnswer: setupShowHideAnswer,\n };\n});\n"],"names":["define","$","initTextArea","this","data","on","e","window","hasOwnProperty","behattesting","undefined","which","keyCode","shiftKey","insertString","preventDefault","selectionStart","before","value","substring","eol","lastIndexOf","line","indent","i","length","charAt","ctrlKey","altKey","ta","sToInsert","selSave","selectionEnd","after","tmp","scrollTop","pos","document","selection","createRange","r","dr","duplicate","moveToElementText","setEndPoint","text","setupAllTAs","each","initQuestionTA","taId","getElementById","setupShowHideAnswer","linkId","divId","link","div","hdr","children","hdrText","innerText","click","is","hide","replace","show","trigger"],"mappings":";;;;;;;;AA2BAA,oCAAO,CAAC,WAAW,SAASC,YAsDfC,eAMLD,EAAEE,MAAMC,KAAK,mBAAmB,GAChCH,EAAEE,MAAMC,KAAK,gBAAgB,GAE7BH,EAAEE,MAAME,GAAG,aAAa,WAMpBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,WAIlBJ,EAAEE,MAAMC,KAAK,eAAgBH,EAAEE,MAAMC,KAAK,uBAG9CH,EAAEE,MAAME,GAAG,SAAS,WAChBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,SAASC,QAIvBC,OAAOC,eAAe,iBAAmBD,OAAOE,mBAEpCC,IAAZJ,EAAEK,OAAmC,IAAZL,EAAEK,UAlCzB,GAmCEL,EAAEM,SAAkBX,EAAEE,MAAMC,KAAK,iBAI7BE,EAAEO,UAAYC,aAAaX,KAAM,UACjCG,EAAES,sBAGL,GA1CD,KA0CKT,EAAEM,cAA6CF,IAAxBP,KAAKa,eAA8B,SAI3DC,OAASd,KAAKe,MAAMC,UAAU,EAAGhB,KAAKa,gBACtCI,IAAMH,OAAOI,YAAY,MACzBC,KAAOL,OAAOE,UAAUC,IAAM,GAC9BG,OAAS,GACJC,EAAI,EAAGA,EAAIF,KAAKG,QAA6B,MAAnBH,KAAKI,OAAOF,GAAYA,IACvDD,QAAkB,IAElBT,aAAaX,KAAM,KAAOoB,SAC1BjB,EAAES,iBAKNd,EAAEE,MAAMC,KAAK,gBAAgB,QAzD7B,KA2DKE,EAAEM,SAAqBN,EAAEqB,UAAYrB,EAAEsB,QAM5C3B,EAAEE,MAAMC,KAAK,gBAAiBH,EAAEE,MAAMC,KAAK,iBAC3CE,EAAES,kBAnEJ,KAqEOT,EAAEM,QAIPX,EAAEE,MAAMC,KAAK,gBAAgB,GAEtBE,EAAEqB,SAAWrB,EAAEsB,QAItB3B,EAAEE,MAAMC,KAAK,gBAAgB,eAYpCU,aAAae,GAAIC,mBACIpB,IAAtBmB,GAAGb,eAA8B,KAC7BC,OAASY,GAAGX,MAAMC,UAAU,EAAGU,GAAGb,gBAClCe,QAAUF,GAAGG,aACbC,MAAQJ,GAAGX,MAAMC,UAAUU,GAAGG,aAAcH,GAAGX,MAAMO,QAKrDS,IAAML,GAAGM,UACbN,GAAGX,MAAQD,OAASa,UAAYG,UAC5BG,IAAML,QAAUD,UAAUL,cAC9BI,GAAGb,eAAiBoB,IACpBP,GAAGG,aAAeI,IAClBP,GAAGM,UAAYD,KACR,EAGN,GAAIG,SAASC,WAAaD,SAASC,UAAUC,YAAa,KAIvDC,EAAIH,SAASC,UAAUC,cACvBE,GAAKD,EAAEE,mBACXD,GAAGE,kBAAkBd,IACrBY,GAAGG,YAAY,WAAYJ,GAC3BA,EAAEK,KAAOf,WACF,SAMA,QAIR,CACHgB,uBAjLA7C,EAAE,sBAAsB8C,KAAK7C,eAkL7B8C,wBA1KoBC,MACpBhD,EAAEoC,SAASa,eAAeD,OAAOF,KAAK7C,eA0KtCiD,6BAhKyBC,OAAQC,WAC7BC,KAAOrD,EAAEoC,SAASa,eAAeE,SACjCG,IAAMtD,EAAEoC,SAASa,eAAeG,QAChCG,IAAMF,KAAKG,WAAW,GACtBC,QAAUF,IAAIG,UAClBL,KAAKM,OAAM,KACHL,IAAIM,GAAG,aACPN,IAAIO,OACJN,IAAIG,UAAYD,QAAQK,QAAQ,IAAU,OAE1CR,IAAIS,OACJT,IAAIU,QAAQ,aACZT,IAAIG,UAAYD,QAAQK,QAAQ,IAAU"}
\ No newline at end of file
diff --git a/amd/build/ui_ace.min.js b/amd/build/ui_ace.min.js
index eff590b9a..05517f12c 100644
--- a/amd/build/ui_ace.min.js
+++ b/amd/build/ui_ace.min.js
@@ -14,6 +14,6 @@
* @copyright Richard Lobb, 2015, 2017, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_ace",["jquery"],(function($){function AceWrapper(textareaId,w,h,params){var textarea=$(document.getElementById(textareaId)),wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=textarea[0]===document.activeElement,lang=params.lang,t=this;try{window.ace.require("ace/ext/language_tools"),this.modelist=window.ace.require("ace/ext/modelist"),this.textarea=textarea,this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),textarea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,this.editor.getSession().setValue(this.textarea.val()),params.theme&&this.editor.setTheme("ace/theme/"+params.theme),window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):params.theme?this.editor.setTheme("ace/theme/"+params.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(textarea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.fail=!1}catch(err){this.fail=!0}}return AceWrapper.prototype.failed=function(){return this.fail},AceWrapper.prototype.failMessage=function(){return"ace_ui_notready"},AceWrapper.prototype.sync=function(){},AceWrapper.prototype.syncIntervalSecs=function(){return 0},AceWrapper.prototype.setLanguage=function(language){var session=this.editor.getSession(),mode=this.findMode(language);mode&&session.setMode(mode.mode)},AceWrapper.prototype.getElement=function(){return this.editNode},AceWrapper.prototype.captureTab=function(){this.capturingTab=!0,this.editor.commands.bindKeys({Tab:"indent","Shift-Tab":"outdent"})},AceWrapper.prototype.releaseTab=function(){this.capturingTab=!1,this.editor.commands.bindKeys({Tab:null,"Shift-Tab":null})},AceWrapper.prototype.setEventHandlers=function(){var t=this;this.editor.getSession().on("change",(function(){t.textarea.val(t.editor.getSession().getValue()),t.contents_changed=!0})),this.editor.on("blur",(function(){t.contents_changed&&t.textarea.trigger("change")})),this.editor.on("mousedown",(function(){t.clickInProgress=!0})),this.editor.on("focus",(function(){t.clickInProgress?t.captureTab():t.releaseTab()})),this.editor.on("click",(function(){t.clickInProgress=!1})),this.editor.container.addEventListener("keydown",(function(e){void 0!==e.which&&0===e.which||(77===e.keyCode&&e.ctrlKey&&!e.altKey?(t.capturingTab?t.releaseTab():t.captureTab(),e.preventDefault()):27===e.keyCode?t.releaseTab():e.shiftKey||e.ctrlKey||e.altKey||9==e.keyCode||t.captureTab())}),!0)},AceWrapper.prototype.destroy=function(){var focused;this.fail||(focused=this.editor.isFocused(),this.textarea.val(this.editor.getSession().getValue()),this.editor.destroy(),$(this.editNode).remove(),focused&&(this.textarea.focus(),this.textarea[0].selectionStart=this.textarea[0].value.length))},AceWrapper.prototype.hasFocus=function(){return this.editor.isFocused()},AceWrapper.prototype.findMode=function(language){var candidate,filename,result,candidates,nameMap={octave:"matlab",nodejs:"javascript","c#":"cs"};if("string"==typeof language){language.toLowerCase()in nameMap&&(language=nameMap[language.toLowerCase()]),candidates=[language,language.replace(/\d+$/,"")];for(var i=0;i"),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),textarea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,this.editor.getSession().setValue(this.textarea.val()),this.fixSlowLoad(),params.theme&&this.editor.setTheme("ace/theme/"+params.theme),window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):params.theme?this.editor.setTheme("ace/theme/"+params.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(textarea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.fail=!1}catch(err){this.fail=!0}}return AceWrapper.prototype.failed=function(){return this.fail},AceWrapper.prototype.failMessage=function(){return"ace_ui_notready"},AceWrapper.prototype.sync=function(){},AceWrapper.prototype.syncIntervalSecs=function(){return 0},AceWrapper.prototype.setLanguage=function(language){var session=this.editor.getSession(),mode=this.findMode(language);mode&&session.setMode(mode.mode)},AceWrapper.prototype.getElement=function(){return this.editNode},AceWrapper.prototype.captureTab=function(){this.capturingTab=!0,this.editor.commands.bindKeys({Tab:"indent","Shift-Tab":"outdent"})},AceWrapper.prototype.releaseTab=function(){this.capturingTab=!1,this.editor.commands.bindKeys({Tab:null,"Shift-Tab":null})},AceWrapper.prototype.fixSlowLoad=function(){const observer=new IntersectionObserver((()=>{$(document).trigger("mousemove")})),editNode=this.editNode.get(0);observer.observe(editNode)},AceWrapper.prototype.setEventHandlers=function(){var t=this;this.editor.getSession().on("change",(function(){t.textarea.val(t.editor.getSession().getValue()),t.contents_changed=!0})),this.editor.on("blur",(function(){t.contents_changed&&t.textarea.trigger("change")})),this.editor.on("mousedown",(function(){t.clickInProgress=!0})),this.editor.on("focus",(function(){t.clickInProgress?t.captureTab():t.releaseTab()})),this.editor.on("click",(function(){t.clickInProgress=!1})),this.editor.container.addEventListener("keydown",(function(e){void 0!==e.which&&0===e.which||(77===e.keyCode&&e.ctrlKey&&!e.altKey?(t.capturingTab?t.releaseTab():t.captureTab(),e.preventDefault()):27===e.keyCode?t.releaseTab():e.shiftKey||e.ctrlKey||e.altKey||9==e.keyCode||t.captureTab())}),!0)},AceWrapper.prototype.destroy=function(){var focused;this.fail||(focused=this.editor.isFocused(),this.textarea.val(this.editor.getSession().getValue()),this.editor.destroy(),$(this.editNode).remove(),focused&&(this.textarea.focus(),this.textarea[0].selectionStart=this.textarea[0].value.length))},AceWrapper.prototype.hasFocus=function(){return this.editor.isFocused()},AceWrapper.prototype.findMode=function(language){var candidate,filename,result,candidates,nameMap={octave:"matlab",nodejs:"javascript","c#":"cs"};if("string"==typeof language){language.toLowerCase()in nameMap&&(language=nameMap[language.toLowerCase()]),candidates=[language,language.replace(/\d+$/,"")];for(var i=0;i.\n\n/**\n * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n session.setValue(this.textarea.val());\n\n // Set theme if available (not currently enabled).\n if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n }\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // Nothing to do ... always sync'd\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 0;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","getSession","setValue","val","theme","setTheme","matchMedia","matches","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","failed","failMessage","sync","syncIntervalSecs","language","session","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","getValue","trigger","container","addEventListener","e","undefined","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_ace.min.js","sources":["../src/ui_ace.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 * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n session.setValue(this.textarea.val());\n\n this.fixSlowLoad();\n\n // Set theme if available (not currently enabled).\n if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n }\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // Nothing to do ... always sync'd\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 0;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n // Sometimes Ace editors do not load until the mouse is moved. To fix this,\n // 'move' the mouse using JQuerry when the editor div enters the viewport.\n AceWrapper.prototype.fixSlowLoad = function () {\n const observer = new IntersectionObserver( () => {\n $(document).trigger('mousemove');\n });\n const editNode = this.editNode.get(0); // Non-JQuerry node.\n observer.observe(editNode);\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","getSession","setValue","val","fixSlowLoad","theme","setTheme","matchMedia","matches","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","failed","failMessage","sync","syncIntervalSecs","language","session","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","observer","IntersectionObserver","trigger","observe","getValue","container","addEventListener","e","undefined","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_ace_gapfiller.min.js b/amd/build/ui_ace_gapfiller.min.js
index fa7b24b07..3cf7ed422 100644
--- a/amd/build/ui_ace_gapfiller.min.js
+++ b/amd/build/ui_ace_gapfiller.min.js
@@ -38,6 +38,6 @@
* @copyright Matthew Toohey, 2021, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_ace_gapfiller",["jquery"],(function($){var Range,validChars=/[ !"#$%&'()*+,`\-./0-9:;<=>?@A-Z\[\]\\^_a-z{}|~]/;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this,code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){var cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){var nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){var char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){var _char=e.args;validChars.test(_char)&&gap.insertChar(t.gaps,selectionRange.start,_char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){var maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+1=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(var i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(var i=start;i?@A-Z\[\]\\^_a-z{}|~]/;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this;let code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){let cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){let nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,selectionRange.start,char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){let maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+1=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(let i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(let i=start;i.\n\n/**\n * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","matchMedia","matches","setTheme","theme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_ace_gapfiller.min.js","sources":["../src/ui_ace_gapfiller.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 util.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 * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","matchMedia","matches","setTheme","theme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/userinterfacewrapper.min.js b/amd/build/userinterfacewrapper.min.js
index 09ae46ac3..0825142de 100644
--- a/amd/build/userinterfacewrapper.min.js
+++ b/amd/build/userinterfacewrapper.min.js
@@ -96,6 +96,6 @@
* 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)
*
*****************************************************************************/
-define("qtype_coderunner/userinterfacewrapper",["jquery"],(function($){function InterfaceWrapper(uiname,textareaId){var t=this;this.GUTTER=14,this.DEFAULT_SYNC_INTERVAL_SECS=5;this.taId=textareaId,this.loadFailId=textareaId+"_loadfailerr";var ta=document.getElementById(textareaId);this.textArea=$(ta);var params=this.textArea.attr("data-params");this.uiParams=params?JSON.parse(params):{},this.uiParams.lang=this.textArea.attr("data-lang"),this.readOnly=this.textArea.prop("readonly"),this.isLoading=!1,this.loadFailed=!1,this.retries=0;var h=parseInt(this.textArea.css("height")),content_lines=this.textArea.val().split("\n").length,rows=ta.rows;content_lines>rows&&(rows=Math.min(content_lines,50)),h=Math.max(h,19*rows,50),this.wrapperNode=$(""),this.textArea.after(this.wrapperNode),this.wrapperNode.hide(),this.wrapperNode.css({resize:"vertical",overflow:"hidden",minHeight:h,width:"100%",border:"1px solid darkgrey"}),this.textArea.data("current-ui-wrapper",this),this.uiInstance=null,this.loadUi(uiname,this.uiParams),$(document).mousemove((function(){t.checkForResize()})),$(window).resize((function(){t.checkForResize()})),this.textArea.closest("form").submit((function(){null!==t.uiInstance&&t.uiInstance.sync()})),$(document.body).on("keydown",(function(e){77===e.keyCode&&e.ctrlKey&&e.altKey&&(null!==t.uiInstance||t.loadFailed?t.stop():t.restart())}))}return InterfaceWrapper.prototype.loadUi=function(uiname,params){var t=this;function syncIntervalSecsBase(){return params.hasOwnProperty("sync_interval_secs")?parseInt(params.sync_interval_secs):t.DEFAULT_SYNC_INTERVAL_SECS}if(this.isLoading)return this.retries+=1,void(this.retries>20?(alert("Failed to load "+uiname+" UI component. If this error persists, please report it to the forum on coderunner.org.nz"),this.retries=0,this.loading=0):setTimeout((function(){t.loadUi(uiname,params)}),200));this.retries=0,this.params=params,this.stop(),this.uiname=uiname,""===this.uiname||"none"===this.uiname||sessionStorage.getItem("disableUis")?this.uiInstance=null:(this.isLoading=!0,require(["qtype_coderunner/ui_"+this.uiname],(function(ui){var langString,errorDiv,h=t.wrapperNode.innerHeight()-t.GUTTER,w=t.wrapperNode.innerWidth(),uiInstance=new ui.Constructor(t.taId,w,h,params);if(uiInstance.failed()){t.loadFailed=!0,t.wrapperNode.hide(),uiInstance.destroy(),t.uiInstance=null,t.textArea.addClass("uiloadfailed");var loadFailDiv='',jqLoadFailDiv=$(loadFailDiv);jqLoadFailDiv.insertBefore(t.textArea),langString=uiInstance.failMessage(),errorDiv=jqLoadFailDiv,require(["core/str"],(function(str){var s=str.get_string(langString,"qtype_coderunner"),fallback=str.get_string("ui_fallback","qtype_coderunner");$.when(s,fallback).done((function(s,fallback){errorDiv.html(s+" "+fallback)}))}))}else{t.hLast=0,t.wLast=0,t.textArea.hide(),t.wrapperNode.show(),t.wrapperNode.append(uiInstance.getElement()),t.uiInstance=uiInstance,t.loadFailed=!1,t.checkForResize();var uiInstancePrototype=Object.getPrototypeOf(uiInstance);uiInstancePrototype.syncIntervalSecs=uiInstancePrototype.syncIntervalSecs||syncIntervalSecsBase,t.startSyncTimer(uiInstance)}t.isLoading=!1})))},InterfaceWrapper.prototype.startSyncTimer=function(uiInstance){var timeout=uiInstance.syncIntervalSecs();this.uiInstance.timer=timeout?setInterval((function(){uiInstance.sync()}),1e3*timeout):null},InterfaceWrapper.prototype.stopSyncTimer=function(uiInstance){uiInstance.timer&&clearTimeout(uiInstance.timer)},InterfaceWrapper.prototype.stop=function(){null!==this.uiInstance&&(this.stopSyncTimer(this.uiInstance),this.textArea.show(),this.uiInstance.hasFocus()&&(this.textArea.focus(),this.textArea[0].selectionStart=this.textArea[0].value.length),this.uiInstance.destroy(),this.uiInstance=null,this.wrapperNode.hide()),this.loadFailed=!1,this.textArea.removeClass("uiloadfailed"),$(document.getElementById(this.loadFailId)).remove()},InterfaceWrapper.prototype.restart=function(){null===this.uiInstance&&this.loadUi(this.uiname,this.params)},InterfaceWrapper.prototype.checkForResize=function(){if(this.uiInstance){var h=this.wrapperNode.innerHeight(),w=this.wrapperNode.innerWidth();if(h!=this.hLast||w!=this.wLast){var xLeft=this.wrapperNode.offset().left,maxWidth=$(window).innerWidth()-xLeft-25,hAdjusted=h-this.GUTTER,wAdjusted=Math.min(maxWidth,w);this.uiInstance.resize(wAdjusted,hAdjusted),this.hLast=this.wrapperNode.innerHeight(),this.wLast=this.wrapperNode.innerWidth()}}},{newUiWrapper:function(uiname,textareaId){return uiname?new InterfaceWrapper(uiname,textareaId):null},InterfaceWrapper:InterfaceWrapper}}));
+define("qtype_coderunner/userinterfacewrapper",["jquery"],(function($){function InterfaceWrapper(uiname,textareaId){let t=this;this.GUTTER=14,this.DEFAULT_SYNC_INTERVAL_SECS=5;this.taId=textareaId,this.loadFailId=textareaId+"_loadfailerr";const ta=document.getElementById(textareaId);this.textArea=$(ta);const params=this.textArea.attr("data-params");this.uiParams=params?JSON.parse(params):{},this.uiParams.lang=this.textArea.attr("data-lang"),this.readOnly=this.textArea.prop("readonly"),this.isLoading=!1,this.loadFailed=!1,this.retries=0;let h=parseInt(this.textArea.css("height")),content_lines=this.textArea.val().split("\n").length,rows=ta.rows;content_lines>rows&&(rows=Math.min(content_lines,50)),h=Math.max(h,19*rows,50),this.wrapperNode=$(""),this.textArea.after(this.wrapperNode),this.wrapperNode.hide(),this.wrapperNode.css({resize:"vertical",overflow:"hidden",minHeight:h,width:"100%",border:"1px solid darkgrey"}),this.textArea.data("current-ui-wrapper",this),this.uiInstance=null,this.loadUi(uiname,this.uiParams),$(document).mousemove((function(){t.checkForResize()})),$(window).resize((function(){t.checkForResize()})),this.textArea.closest("form").submit((function(){null!==t.uiInstance&&t.uiInstance.sync()})),$(document.body).on("keydown",(function(e){77===e.keyCode&&e.ctrlKey&&e.altKey&&(null!==t.uiInstance||t.loadFailed?t.stop():t.restart())}))}return InterfaceWrapper.prototype.loadUi=function(uiname,params){const t=this;function syncIntervalSecsBase(){return params.hasOwnProperty("sync_interval_secs")?parseInt(params.sync_interval_secs):t.DEFAULT_SYNC_INTERVAL_SECS}if(this.isLoading)return this.retries+=1,void(this.retries>20?(alert("Failed to load "+uiname+" UI component. If this error persists, please report it to the forum on coderunner.org.nz"),this.retries=0,this.loading=0):setTimeout((function(){t.loadUi(uiname,params)}),200));this.retries=0,this.params=params,this.stop(),this.uiname=uiname,""===this.uiname||"none"===this.uiname||sessionStorage.getItem("disableUis")?this.uiInstance=null:(this.isLoading=!0,require(["qtype_coderunner/ui_"+this.uiname],(function(ui){const h=t.wrapperNode.innerHeight()-t.GUTTER,w=t.wrapperNode.innerWidth(),uiInstance=new ui.Constructor(t.taId,w,h,params);if(uiInstance.failed()){t.loadFailed=!0,t.wrapperNode.hide(),uiInstance.destroy(),t.uiInstance=null,t.textArea.addClass("uiloadfailed");const loadFailDiv='';let jqLoadFailDiv=$(loadFailDiv);jqLoadFailDiv.insertBefore(t.textArea),langString=uiInstance.failMessage(),errorDiv=jqLoadFailDiv,require(["core/str"],(function(str){const s=str.get_string(langString,"qtype_coderunner"),fallback=str.get_string("ui_fallback","qtype_coderunner");$.when(s,fallback).done((function(s,fallback){errorDiv.html(s+" "+fallback)}))}))}else{t.hLast=0,t.wLast=0,t.textArea.hide(),t.wrapperNode.show(),t.wrapperNode.append(uiInstance.getElement()),t.uiInstance=uiInstance,t.loadFailed=!1,t.checkForResize();let uiInstancePrototype=Object.getPrototypeOf(uiInstance);uiInstancePrototype.syncIntervalSecs=uiInstancePrototype.syncIntervalSecs||syncIntervalSecsBase,t.startSyncTimer(uiInstance)}var langString,errorDiv;t.isLoading=!1})))},InterfaceWrapper.prototype.startSyncTimer=function(uiInstance){const timeout=uiInstance.syncIntervalSecs();this.uiInstance.timer=timeout?setInterval((function(){uiInstance.sync()}),1e3*timeout):null},InterfaceWrapper.prototype.stopSyncTimer=function(uiInstance){uiInstance.timer&&clearTimeout(uiInstance.timer)},InterfaceWrapper.prototype.stop=function(){null!==this.uiInstance&&(this.stopSyncTimer(this.uiInstance),this.textArea.show(),this.uiInstance.hasFocus()&&(this.textArea.focus(),this.textArea[0].selectionStart=this.textArea[0].value.length),this.uiInstance.destroy(),this.uiInstance=null,this.wrapperNode.hide()),this.loadFailed=!1,this.textArea.removeClass("uiloadfailed"),$(document.getElementById(this.loadFailId)).remove()},InterfaceWrapper.prototype.restart=function(){null===this.uiInstance&&this.loadUi(this.uiname,this.params)},InterfaceWrapper.prototype.checkForResize=function(){if(this.uiInstance){const h=this.wrapperNode.innerHeight(),w=this.wrapperNode.innerWidth();if(h!=this.hLast||w!=this.wLast){const xLeft=this.wrapperNode.offset().left,maxWidth=$(window).innerWidth()-xLeft-25,hAdjusted=h-this.GUTTER,wAdjusted=Math.min(maxWidth,w);this.uiInstance.resize(wAdjusted,hAdjusted),this.hLast=this.wrapperNode.innerHeight(),this.wLast=this.wrapperNode.innerWidth()}}},{newUiWrapper:function(uiname,textareaId){return uiname?new InterfaceWrapper(uiname,textareaId):null},InterfaceWrapper:InterfaceWrapper}}));
//# sourceMappingURL=userinterfacewrapper.min.js.map
\ No newline at end of file
diff --git a/amd/build/userinterfacewrapper.min.js.map b/amd/build/userinterfacewrapper.min.js.map
index 631c835e8..f46f952fc 100644
--- a/amd/build/userinterfacewrapper.min.js.map
+++ b/amd/build/userinterfacewrapper.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"userinterfacewrapper.min.js","sources":["../src/userinterfacewrapper.js"],"sourcesContent":["/******************************************************************************\n *\n * This module provides a wrapper for user-interface modules, handling hiding\n * of the textArea that is being replaced by the UI element,\n * resizing of the UI component, and support of such usability functions as\n * ctrl-alt-M to disable/re-enable the entire user interface, including the\n * wrapper.\n *\n * @module coderunner/userinterfacewrapper\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * The InterfaceWrapper class is constructed either by Moodle PHP calls of\n * the form\n *\n * $PAGE->requires->js_call_amd($modulename, $functionname, $params)\n *\n * (e.g. from within render.php) or by JavaScript require calls, e.g. from\n * authorform.js when the question author changes UI type.\n *\n * The InterfaceWrapper provides:\n *\n * 1. A constructor InterfaceWrapper(uiname, textareaId) which\n * hides the given text area, replaces it with a wrapper div (resizable in\n * height by the user but with width resizing managed by changes in window\n * width), created an instance of nameInstance as defined in the file\n * ui_name.js (see below).\n * params is a record containing the decoded value of\n *\n * 2. A stop() method that destroys the embedded UI and hides the wrapper.\n *\n * 3. A restart() method that shows the wrapper again and re-creates the prior\n * embedded UI component within it.\n *\n * 4. A loadUi(uiname, params) method that kills any currently running UI element\n * (if there is one) and (re)loads the specified one. The params parameter\n * is a record that allows additional parameters to be passed in, such as\n * those from the question's uiParams field and, in the case of the\n * Ace UI, the 'lang' (language) that the editor is editing. This data\n * is supplied by the PHP via the data-params attribute of the answer's\n * base textarea.\n *\n * 5. Regular checking for any resizing of the wrapper, which are passed on to\n * the embedded UI element's resize() method.\n *\n * 6. Monitoring of alt-ctrl-M key presses which toggle the visibility of the\n * wrapper plus UI element and the syncronised textArea by calls to stop()\n * and restart\n *\n * =========================================================================\n *\n * The embedded user-interface module must be defined in a JavaScript file\n * of the form ui_name.js which must define a class nameInstance with\n * the following functionality:\n *\n * 1. A constructor SomeUiName(textareaId, width, height, params) that\n * builds an HTML component of the given width and height. textareaId is the\n * ID of the textArea from which the UI element should obtain its initial\n * serialisation and to which it should write the serialisation when its save\n * or destroy methods are called. params is a JavaScript object,\n * decoded from the JSON uiParams defined by the question plus any\n * additional data required, such as the 'lang' in the case of Ace.\n *\n * 2. A getElement() method that returns the HTML element that the\n * InterfaceWrapper is to insert into the document tree.\n *\n * 3. A method failed() that should return true unless the constructor\n * failed (e.g. because it was not able to de-serialise the text area's\n * contents). The wrapper will call destroy() on the object if failed()\n * returns true and abort the use of the UI element. The text area will\n * have the uiloadfailed class added, which CSS will display in some\n * error mode (e.g. a red border).\n *\n * 4. A method failMessage() that will be called only when failed() returns\n * True. It should be a defined CodeRunner language string key.\n *\n * 5. A sync() method that copies the serialised represention of the UI plugin's\n * data to the related TextArea. This is used when submit is clicked.\n *\n * 6. A destroy() method that should sync the contents to the text area then\n * destroy any HTML elements or other created content. This method is called\n * when CTRL-ALT-M is typed by the user to turn off all UI plugins\n *\n * 7. A resize(width, height) method that should resize the entire UI element\n * to the given dimensions.\n *\n * 8. A hasFocus() method that returns true if the UI element has focus.\n *\n * 9. A syncIntervalSecs() method that returns the time interval between\n * calls to the sync() method. 0 for no sync calls. The userinterfacewrapper\n * provides all instances with a generic (base-class) version that returns\n * the value of a UI parameter sync_interval_secs if given else uses the\n * UI interface wrapper default (currently 10).\n *\n * The return value from the module define is a record with a single field\n * 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)\n *\n *****************************************************************************/\n\n/**\n * 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 util.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\ndefine(['jquery'], function($) {\n /**\n * Constructor for a new user interface.\n * @param {string} uiname The name of the interface element (e.g. ace, graph, etc)\n * which should be in file ui_ace.js, ui_graph.js etc.\n * @param {string} textareaId The id of the text area that the UI is to manage.\n * The text area should have an attribute data-params, which is a\n * JSON encoded record containing whatever additional parameters might\n * be needed by the User interface. As a minimum it should contain all\n * the parameters from the uiparameters field of\n * the question so that question authors can pass in additional data\n * such as whether graph edges are bidirectional or not in the case of\n * the graph UI. Additionally the Ace editor requires a 'lang' field\n * to specify what language the editor is editing.\n * When the wrapper has been set up on a text area, the text area's\n * data attribute contains an entry for 'current-ui-wrapper' that is\n * a reference to the wrapper ('this').\n */\n function InterfaceWrapper(uiname, textareaId) {\n let t = this; // For use by embedded functions.\n\n this.GUTTER = 14; // Size of gutter at base of wrapper Node (pixels)\n this.DEFAULT_SYNC_INTERVAL_SECS = 5;\n\n const PIXELS_PER_ROW = 19; // For estimating height of textareas.\n const MAX_GROWN_ROWS = 50; // Upper limit to artifically grown textarea rows.\n const MIN_WRAPPER_HEIGHT = 50;\n\n this.taId = textareaId;\n this.loadFailId = textareaId + '_loadfailerr';\n const ta = document.getElementById(textareaId);\n this.textArea = $(ta);\n const params = this.textArea.attr('data-params');\n if (params) {\n this.uiParams = JSON.parse(params);\n } else {\n this.uiParams = {};\n }\n this.uiParams.lang = this.textArea.attr('data-lang');\n this.readOnly = this.textArea.prop('readonly');\n this.isLoading = false; // True if we're busy loading a UI element.\n this.loadFailed = false; // True if UI failed to initialise properly.\n this.retries = 0; // Number of failed attempts to load a UI component.\n\n let h = parseInt(this.textArea.css(\"height\"));\n let content_lines = this.textArea.val().split('\\n').length;\n let rows = ta.rows;\n if (content_lines > rows) {\n // Allow reloaded text areas with lots of text to grow bigger, within limits.\n rows = Math.min(content_lines, MAX_GROWN_ROWS);\n }\n h = Math.max(h, rows * PIXELS_PER_ROW, MIN_WRAPPER_HEIGHT);\n\n /**\n * Construct an empty hidden wrapper div, inserted directly after the\n * textArea, ready to contain the actual UI.\n */\n this.wrapperNode = $(\"\");\n this.textArea.after(this.wrapperNode);\n this.wrapperNode.hide();\n this.wrapperNode.css({\n resize: 'vertical',\n overflow: 'hidden',\n minHeight: h,\n width: \"100%\",\n border: \"1px solid darkgrey\"\n });\n\n /**\n * Record a reference to this wrapper in the text area's data attribute\n * for use by external javascript that needs to interact with the\n * wrapper, e.g. the multilanguage.js module.\n */\n this.textArea.data('current-ui-wrapper', this);\n\n /**\n * Load the UI into the wrapper (aysnchronous).\n */\n this.uiInstance = null; // Defined by loadUi asynchronously\n this.loadUi(uiname, this.uiParams); // Load the required UI element\n\n /**\n * Add event handlers\n */\n $(document).mousemove(function() {\n t.checkForResize();\n });\n $(window).resize(function() {\n t.checkForResize();\n });\n this.textArea.closest('form').submit(function() {\n if (t.uiInstance !== null) {\n t.uiInstance.sync();\n }\n });\n $(document.body).on('keydown', function(e) {\n const KEY_M = 77;\n if (e.keyCode === KEY_M && e.ctrlKey && e.altKey) {\n if (t.uiInstance !== null || t.loadFailed) {\n t.stop();\n } else {\n t.restart(); // Reactivate\n }\n }\n });\n }\n\n /**\n * Load the specified UI element (which in the case of Ace will need\n * to know the language, lang, as well - this must be supplied as\n * a 'lang' attribute of the record params.\n * When ui is up and running, this.uiInstance will reference it.\n * To avoid a potential race problem, if this method is already busy\n * with a load, try again in 200 msecs.\n * @param {string} uiname The name of the User Interface to be used.\n * @param {object} params The UI parameters object that passes parameters\n * to the actual UI object.\n */\n InterfaceWrapper.prototype.loadUi = function(uiname, params) {\n const t = this,\n errPart1 = 'Failed to load ',\n errPart2 = ' UI component. If this error persists, please report it to the forum on coderunner.org.nz';\n\n /**\n * Get the given language string and plug it into the given jQuery\n * div element as its html, plus a 'fallback' message on a separate line.\n * @param {string} langString The language string specifier for the error message,\n * to be loaded by AJAX.\n * @param {object} errorDiv The div object into which the error message\n * is to be inserted.\n */\n function setLoadFailMessage(langString, errorDiv) {\n require(['core/str'], function(str) {\n /**\n * Get langString text via AJAX\n */\n const\n s = str.get_string(langString, 'qtype_coderunner'),\n fallback = str.get_string('ui_fallback', 'qtype_coderunner');\n $.when(s, fallback).done(function(s, fallback) {\n errorDiv.html(s + ' ' + fallback);\n });\n });\n }\n\n /**\n * The default method for a UIs sync_interval_secs method.\n * Returns the sync_interval_secs parameter if given, else\n * DEFAULT_SYNC_INTERVAL_SECS.\n */\n function syncIntervalSecsBase() {\n if (params.hasOwnProperty('sync_interval_secs')) {\n return parseInt(params.sync_interval_secs);\n } else {\n return t.DEFAULT_SYNC_INTERVAL_SECS;\n }\n }\n\n if (this.isLoading) { // Oops, we're loading a UI element already\n this.retries += 1;\n if (this.retries > 20) {\n alert(errPart1 + uiname + errPart2);\n this.retries = 0;\n this.loading = 0;\n } else {\n setTimeout(function() {\n t.loadUi(uiname, params);\n }, 200); // Try again in 200 msecs\n }\n return;\n }\n this.retries = 0;\n this.params = params; // Save in case need to restart\n\n this.stop(); // Kill any active UI first\n this.uiname = uiname;\n\n if (this.uiname === '' || this.uiname === 'none' || sessionStorage.getItem('disableUis')) {\n this.uiInstance = null;\n } else {\n this.isLoading = true;\n require(['qtype_coderunner/ui_' + this.uiname],\n function(ui) {\n const h = t.wrapperNode.innerHeight() - t.GUTTER;\n const w = t.wrapperNode.innerWidth();\n const uiInstance = new ui.Constructor(t.taId, w, h, params);\n if (uiInstance.failed()) {\n /*\n * Constructor failed to load serialisation.\n * Set uiloadfailed class on text area.\n */\n t.loadFailed = true;\n t.wrapperNode.hide();\n uiInstance.destroy();\n t.uiInstance = null;\n t.textArea.addClass('uiloadfailed');\n const loadFailDiv = '';\n let jqLoadFailDiv = $(loadFailDiv);\n jqLoadFailDiv.insertBefore(t.textArea);\n setLoadFailMessage(uiInstance.failMessage(), jqLoadFailDiv); // Insert error by AJAX\n } else {\n t.hLast = 0; // Force resize (and hence redraw)\n t.wLast = 0; // ... on first call to checkForResize\n t.textArea.hide();\n t.wrapperNode.show();\n t.wrapperNode.append(uiInstance.getElement());\n t.uiInstance = uiInstance;\n t.loadFailed = false;\n t.checkForResize();\n\n /*\n * Set a default syncIntervalSecs method if uiInstance lacks one.\n */\n let uiInstancePrototype = Object.getPrototypeOf(uiInstance);\n uiInstancePrototype.syncIntervalSecs = uiInstancePrototype.syncIntervalSecs || syncIntervalSecsBase;\n t.startSyncTimer(uiInstance);\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Start a sync timer on the given uiInstance, unless its time interval is 0.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.startSyncTimer = function(uiInstance) {\n const timeout = uiInstance.syncIntervalSecs();\n if (timeout) {\n this.uiInstance.timer = setInterval(function () {\n uiInstance.sync();\n }, timeout * 1000);\n } else {\n this.uiInstance.timer = null;\n }\n };\n\n\n /**\n * Stop the sync timer on the given uiInstance, if running.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.stopSyncTimer = function(uiInstance) {\n if (uiInstance.timer) {\n clearTimeout(uiInstance.timer);\n }\n };\n\n\n InterfaceWrapper.prototype.stop = function() {\n /*\n * Disable (shutdown) the embedded ui component.\n * The wrapper remains active for ctrl-alt-M events, but is hidden.\n */\n if (this.uiInstance !== null) {\n this.stopSyncTimer(this.uiInstance);\n this.textArea.show();\n if (this.uiInstance.hasFocus()) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n this.uiInstance.destroy();\n this.uiInstance = null;\n this.wrapperNode.hide();\n }\n this.loadFailed = false;\n this.textArea.removeClass('uiloadfailed'); // Just in case it failed before\n $(document.getElementById(this.loadFailId)).remove();\n };\n\n /*\n * Re-enable the ui element (e.g. after alt-cntrl-M). This is\n * a full re-initialisation of the ui element.\n */\n InterfaceWrapper.prototype.restart = function() {\n if (this.uiInstance === null) {\n /**\n * Restart the UI component in the textarea\n */\n this.loadUi(this.uiname, this.params);\n }\n };\n\n\n /**\n * Check for wrapper resize - propagate to ui element.\n */\n InterfaceWrapper.prototype.checkForResize = function() {\n const SIZE_HACK = 25; // Horrible but best I can do. TODO: FIXME\n\n if (this.uiInstance) {\n const h = this.wrapperNode.innerHeight();\n const w = this.wrapperNode.innerWidth();\n if (h != this.hLast || w != this.wLast) {\n const xLeft = this.wrapperNode.offset().left;\n const maxWidth = $(window).innerWidth() - xLeft - SIZE_HACK;\n const hAdjusted = h - this.GUTTER;\n const wAdjusted = Math.min(maxWidth, w);\n this.uiInstance.resize(wAdjusted, hAdjusted);\n this.hLast = this.wrapperNode.innerHeight();\n this.wLast = this.wrapperNode.innerWidth();\n }\n }\n };\n\n /**\n * The external entry point from the PHP.\n * @param {string} uiname The name of the User Interface to use e.g. 'ace'\n * @param {string} textareaId The ID of the textarea to be wrapped.\n */\n function newUiWrapper(uiname, textareaId) {\n if (uiname) {\n return new InterfaceWrapper(uiname, textareaId);\n } else {\n return null;\n }\n }\n\n\n return {\n newUiWrapper: newUiWrapper,\n InterfaceWrapper: InterfaceWrapper\n };\n});\n"],"names":["define","$","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","taId","loadFailId","ta","document","getElementById","textArea","params","attr","uiParams","JSON","parse","lang","readOnly","prop","isLoading","loadFailed","retries","h","parseInt","css","content_lines","val","split","length","rows","Math","min","max","wrapperNode","after","hide","resize","overflow","minHeight","width","border","data","uiInstance","loadUi","mousemove","checkForResize","window","closest","submit","sync","body","on","e","keyCode","ctrlKey","altKey","stop","restart","prototype","syncIntervalSecsBase","hasOwnProperty","sync_interval_secs","alert","loading","setTimeout","sessionStorage","getItem","require","ui","langString","errorDiv","innerHeight","w","innerWidth","Constructor","failed","destroy","addClass","loadFailDiv","jqLoadFailDiv","insertBefore","failMessage","str","s","get_string","fallback","when","done","html","hLast","wLast","show","append","getElement","uiInstancePrototype","Object","getPrototypeOf","syncIntervalSecs","startSyncTimer","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","remove","xLeft","offset","left","maxWidth","hAdjusted","wAdjusted","newUiWrapper"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"userinterfacewrapper.min.js","sources":["../src/userinterfacewrapper.js"],"sourcesContent":["/******************************************************************************\n *\n * This module provides a wrapper for user-interface modules, handling hiding\n * of the textArea that is being replaced by the UI element,\n * resizing of the UI component, and support of such usability functions as\n * ctrl-alt-M to disable/re-enable the entire user interface, including the\n * wrapper.\n *\n * @module coderunner/userinterfacewrapper\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * The InterfaceWrapper class is constructed either by Moodle PHP calls of\n * the form\n *\n * $PAGE->requires->js_call_amd($modulename, $functionname, $params)\n *\n * (e.g. from within render.php) or by JavaScript require calls, e.g. from\n * authorform.js when the question author changes UI type.\n *\n * The InterfaceWrapper provides:\n *\n * 1. A constructor InterfaceWrapper(uiname, textareaId) which\n * hides the given text area, replaces it with a wrapper div (resizable in\n * height by the user but with width resizing managed by changes in window\n * width), created an instance of nameInstance as defined in the file\n * ui_name.js (see below).\n * params is a record containing the decoded value of\n *\n * 2. A stop() method that destroys the embedded UI and hides the wrapper.\n *\n * 3. A restart() method that shows the wrapper again and re-creates the prior\n * embedded UI component within it.\n *\n * 4. A loadUi(uiname, params) method that kills any currently running UI element\n * (if there is one) and (re)loads the specified one. The params parameter\n * is a record that allows additional parameters to be passed in, such as\n * those from the question's uiParams field and, in the case of the\n * Ace UI, the 'lang' (language) that the editor is editing. This data\n * is supplied by the PHP via the data-params attribute of the answer's\n * base textarea.\n *\n * 5. Regular checking for any resizing of the wrapper, which are passed on to\n * the embedded UI element's resize() method.\n *\n * 6. Monitoring of alt-ctrl-M key presses which toggle the visibility of the\n * wrapper plus UI element and the syncronised textArea by calls to stop()\n * and restart\n *\n * =========================================================================\n *\n * The embedded user-interface module must be defined in a JavaScript file\n * of the form ui_name.js which must define a class nameInstance with\n * the following functionality:\n *\n * 1. A constructor SomeUiName(textareaId, width, height, params) that\n * builds an HTML component of the given width and height. textareaId is the\n * ID of the textArea from which the UI element should obtain its initial\n * serialisation and to which it should write the serialisation when its save\n * or destroy methods are called. params is a JavaScript object,\n * decoded from the JSON uiParams defined by the question plus any\n * additional data required, such as the 'lang' in the case of Ace.\n *\n * 2. A getElement() method that returns the HTML element that the\n * InterfaceWrapper is to insert into the document tree.\n *\n * 3. A method failed() that should return true unless the constructor\n * failed (e.g. because it was not able to de-serialise the text area's\n * contents). The wrapper will call destroy() on the object if failed()\n * returns true and abort the use of the UI element. The text area will\n * have the uiloadfailed class added, which CSS will display in some\n * error mode (e.g. a red border).\n *\n * 4. A method failMessage() that will be called only when failed() returns\n * True. It should be a defined CodeRunner language string key.\n *\n * 5. A sync() method that copies the serialised represention of the UI plugin's\n * data to the related TextArea. This is used when submit is clicked.\n *\n * 6. A destroy() method that should sync the contents to the text area then\n * destroy any HTML elements or other created content. This method is called\n * when CTRL-ALT-M is typed by the user to turn off all UI plugins\n *\n * 7. A resize(width, height) method that should resize the entire UI element\n * to the given dimensions.\n *\n * 8. A hasFocus() method that returns true if the UI element has focus.\n *\n * 9. A syncIntervalSecs() method that returns the time interval between\n * calls to the sync() method. 0 for no sync calls. The userinterfacewrapper\n * provides all instances with a generic (base-class) version that returns\n * the value of a UI parameter sync_interval_secs if given else uses the\n * UI interface wrapper default (currently 10).\n *\n * The return value from the module define is a record with a single field\n * 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)\n *\n *****************************************************************************/\n\n/**\n * 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 util.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\ndefine(['jquery'], function($) {\n /**\n * Constructor for a new user interface.\n * @param {string} uiname The name of the interface element (e.g. ace, graph, etc)\n * which should be in file ui_ace.js, ui_graph.js etc.\n * @param {string} textareaId The id of the text area that the UI is to manage.\n * The text area should have an attribute data-params, which is a\n * JSON encoded record containing whatever additional parameters might\n * be needed by the User interface. As a minimum it should contain all\n * the parameters from the uiparameters field of\n * the question so that question authors can pass in additional data\n * such as whether graph edges are bidirectional or not in the case of\n * the graph UI. Additionally the Ace editor requires a 'lang' field\n * to specify what language the editor is editing.\n * When the wrapper has been set up on a text area, the text area's\n * data attribute contains an entry for 'current-ui-wrapper' that is\n * a reference to the wrapper ('this').\n */\n function InterfaceWrapper(uiname, textareaId) {\n let t = this; // For use by embedded functions.\n\n this.GUTTER = 14; // Size of gutter at base of wrapper Node (pixels)\n this.DEFAULT_SYNC_INTERVAL_SECS = 5;\n\n const PIXELS_PER_ROW = 19; // For estimating height of textareas.\n const MAX_GROWN_ROWS = 50; // Upper limit to artifically grown textarea rows.\n const MIN_WRAPPER_HEIGHT = 50;\n\n this.taId = textareaId;\n this.loadFailId = textareaId + '_loadfailerr';\n const ta = document.getElementById(textareaId);\n this.textArea = $(ta);\n const params = this.textArea.attr('data-params');\n if (params) {\n this.uiParams = JSON.parse(params);\n } else {\n this.uiParams = {};\n }\n this.uiParams.lang = this.textArea.attr('data-lang');\n this.readOnly = this.textArea.prop('readonly');\n this.isLoading = false; // True if we're busy loading a UI element.\n this.loadFailed = false; // True if UI failed to initialise properly.\n this.retries = 0; // Number of failed attempts to load a UI component.\n\n let h = parseInt(this.textArea.css(\"height\"));\n let content_lines = this.textArea.val().split('\\n').length;\n let rows = ta.rows;\n if (content_lines > rows) {\n // Allow reloaded text areas with lots of text to grow bigger, within limits.\n rows = Math.min(content_lines, MAX_GROWN_ROWS);\n }\n h = Math.max(h, rows * PIXELS_PER_ROW, MIN_WRAPPER_HEIGHT);\n\n /**\n * Construct an empty hidden wrapper div, inserted directly after the\n * textArea, ready to contain the actual UI.\n */\n this.wrapperNode = $(\"\");\n this.textArea.after(this.wrapperNode);\n this.wrapperNode.hide();\n this.wrapperNode.css({\n resize: 'vertical',\n overflow: 'hidden',\n minHeight: h,\n width: \"100%\",\n border: \"1px solid darkgrey\"\n });\n\n /**\n * Record a reference to this wrapper in the text area's data attribute\n * for use by external javascript that needs to interact with the\n * wrapper, e.g. the multilanguage.js module.\n */\n this.textArea.data('current-ui-wrapper', this);\n\n /**\n * Load the UI into the wrapper (aysnchronous).\n */\n this.uiInstance = null; // Defined by loadUi asynchronously\n this.loadUi(uiname, this.uiParams); // Load the required UI element\n\n /**\n * Add event handlers\n */\n $(document).mousemove(function() {\n t.checkForResize();\n });\n $(window).resize(function() {\n t.checkForResize();\n });\n this.textArea.closest('form').submit(function() {\n if (t.uiInstance !== null) {\n t.uiInstance.sync();\n }\n });\n $(document.body).on('keydown', function(e) {\n const KEY_M = 77;\n if (e.keyCode === KEY_M && e.ctrlKey && e.altKey) {\n if (t.uiInstance !== null || t.loadFailed) {\n t.stop();\n } else {\n t.restart(); // Reactivate\n }\n }\n });\n }\n\n /**\n * Load the specified UI element (which in the case of Ace will need\n * to know the language, lang, as well - this must be supplied as\n * a 'lang' attribute of the record params.\n * When ui is up and running, this.uiInstance will reference it.\n * To avoid a potential race problem, if this method is already busy\n * with a load, try again in 200 msecs.\n * @param {string} uiname The name of the User Interface to be used.\n * @param {object} params The UI parameters object that passes parameters\n * to the actual UI object.\n */\n InterfaceWrapper.prototype.loadUi = function(uiname, params) {\n const t = this,\n errPart1 = 'Failed to load ',\n errPart2 = ' UI component. If this error persists, please report it to the forum on coderunner.org.nz';\n\n /**\n * Get the given language string and plug it into the given jQuery\n * div element as its html, plus a 'fallback' message on a separate line.\n * @param {string} langString The language string specifier for the error message,\n * to be loaded by AJAX.\n * @param {object} errorDiv The div object into which the error message\n * is to be inserted.\n */\n function setLoadFailMessage(langString, errorDiv) {\n require(['core/str'], function(str) {\n /**\n * Get langString text via AJAX\n */\n const\n s = str.get_string(langString, 'qtype_coderunner'),\n fallback = str.get_string('ui_fallback', 'qtype_coderunner');\n $.when(s, fallback).done(function(s, fallback) {\n errorDiv.html(s + ' ' + fallback);\n });\n });\n }\n\n /**\n * The default method for a UIs sync_interval_secs method.\n * Returns the sync_interval_secs parameter if given, else\n * DEFAULT_SYNC_INTERVAL_SECS.\n */\n function syncIntervalSecsBase() {\n if (params.hasOwnProperty('sync_interval_secs')) {\n return parseInt(params.sync_interval_secs);\n } else {\n return t.DEFAULT_SYNC_INTERVAL_SECS;\n }\n }\n\n if (this.isLoading) { // Oops, we're loading a UI element already\n this.retries += 1;\n if (this.retries > 20) {\n alert(errPart1 + uiname + errPart2);\n this.retries = 0;\n this.loading = 0;\n } else {\n setTimeout(function() {\n t.loadUi(uiname, params);\n }, 200); // Try again in 200 msecs\n }\n return;\n }\n this.retries = 0;\n this.params = params; // Save in case need to restart\n\n this.stop(); // Kill any active UI first\n this.uiname = uiname;\n\n if (this.uiname === '' || this.uiname === 'none' || sessionStorage.getItem('disableUis')) {\n this.uiInstance = null;\n } else {\n this.isLoading = true;\n require(['qtype_coderunner/ui_' + this.uiname],\n function(ui) {\n const h = t.wrapperNode.innerHeight() - t.GUTTER;\n const w = t.wrapperNode.innerWidth();\n const uiInstance = new ui.Constructor(t.taId, w, h, params);\n if (uiInstance.failed()) {\n /*\n * Constructor failed to load serialisation.\n * Set uiloadfailed class on text area.\n */\n t.loadFailed = true;\n t.wrapperNode.hide();\n uiInstance.destroy();\n t.uiInstance = null;\n t.textArea.addClass('uiloadfailed');\n const loadFailDiv = '';\n let jqLoadFailDiv = $(loadFailDiv);\n jqLoadFailDiv.insertBefore(t.textArea);\n setLoadFailMessage(uiInstance.failMessage(), jqLoadFailDiv); // Insert error by AJAX\n } else {\n t.hLast = 0; // Force resize (and hence redraw)\n t.wLast = 0; // ... on first call to checkForResize\n t.textArea.hide();\n t.wrapperNode.show();\n t.wrapperNode.append(uiInstance.getElement());\n t.uiInstance = uiInstance;\n t.loadFailed = false;\n t.checkForResize();\n\n /*\n * Set a default syncIntervalSecs method if uiInstance lacks one.\n */\n let uiInstancePrototype = Object.getPrototypeOf(uiInstance);\n uiInstancePrototype.syncIntervalSecs = uiInstancePrototype.syncIntervalSecs || syncIntervalSecsBase;\n t.startSyncTimer(uiInstance);\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Start a sync timer on the given uiInstance, unless its time interval is 0.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.startSyncTimer = function(uiInstance) {\n const timeout = uiInstance.syncIntervalSecs();\n if (timeout) {\n this.uiInstance.timer = setInterval(function () {\n uiInstance.sync();\n }, timeout * 1000);\n } else {\n this.uiInstance.timer = null;\n }\n };\n\n\n /**\n * Stop the sync timer on the given uiInstance, if running.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.stopSyncTimer = function(uiInstance) {\n if (uiInstance.timer) {\n clearTimeout(uiInstance.timer);\n }\n };\n\n\n InterfaceWrapper.prototype.stop = function() {\n /*\n * Disable (shutdown) the embedded ui component.\n * The wrapper remains active for ctrl-alt-M events, but is hidden.\n */\n if (this.uiInstance !== null) {\n this.stopSyncTimer(this.uiInstance);\n this.textArea.show();\n if (this.uiInstance.hasFocus()) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n this.uiInstance.destroy();\n this.uiInstance = null;\n this.wrapperNode.hide();\n }\n this.loadFailed = false;\n this.textArea.removeClass('uiloadfailed'); // Just in case it failed before\n $(document.getElementById(this.loadFailId)).remove();\n };\n\n /*\n * Re-enable the ui element (e.g. after alt-cntrl-M). This is\n * a full re-initialisation of the ui element.\n */\n InterfaceWrapper.prototype.restart = function() {\n if (this.uiInstance === null) {\n /**\n * Restart the UI component in the textarea\n */\n this.loadUi(this.uiname, this.params);\n }\n };\n\n\n /**\n * Check for wrapper resize - propagate to ui element.\n */\n InterfaceWrapper.prototype.checkForResize = function() {\n const SIZE_HACK = 25; // Horrible but best I can do. TODO: FIXME\n\n if (this.uiInstance) {\n const h = this.wrapperNode.innerHeight();\n const w = this.wrapperNode.innerWidth();\n if (h != this.hLast || w != this.wLast) {\n const xLeft = this.wrapperNode.offset().left;\n const maxWidth = $(window).innerWidth() - xLeft - SIZE_HACK;\n const hAdjusted = h - this.GUTTER;\n const wAdjusted = Math.min(maxWidth, w);\n this.uiInstance.resize(wAdjusted, hAdjusted);\n this.hLast = this.wrapperNode.innerHeight();\n this.wLast = this.wrapperNode.innerWidth();\n }\n }\n };\n\n /**\n * The external entry point from the PHP.\n * @param {string} uiname The name of the User Interface to use e.g. 'ace'\n * @param {string} textareaId The ID of the textarea to be wrapped.\n */\n function newUiWrapper(uiname, textareaId) {\n if (uiname) {\n return new InterfaceWrapper(uiname, textareaId);\n } else {\n return null;\n }\n }\n\n\n return {\n newUiWrapper: newUiWrapper,\n InterfaceWrapper: InterfaceWrapper\n };\n});\n"],"names":["define","$","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","taId","loadFailId","ta","document","getElementById","textArea","params","attr","uiParams","JSON","parse","lang","readOnly","prop","isLoading","loadFailed","retries","h","parseInt","css","content_lines","val","split","length","rows","Math","min","max","wrapperNode","after","hide","resize","overflow","minHeight","width","border","data","uiInstance","loadUi","mousemove","checkForResize","window","closest","submit","sync","body","on","e","keyCode","ctrlKey","altKey","stop","restart","prototype","syncIntervalSecsBase","hasOwnProperty","sync_interval_secs","alert","loading","setTimeout","sessionStorage","getItem","require","ui","innerHeight","w","innerWidth","Constructor","failed","destroy","addClass","loadFailDiv","jqLoadFailDiv","insertBefore","langString","failMessage","errorDiv","str","s","get_string","fallback","when","done","html","hLast","wLast","show","append","getElement","uiInstancePrototype","Object","getPrototypeOf","syncIntervalSecs","startSyncTimer","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","remove","xLeft","offset","left","maxWidth","hAdjusted","wAdjusted","newUiWrapper"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/ui_ace.js b/amd/src/ui_ace.js
index bbb06a48c..23e316467 100644
--- a/amd/src/ui_ace.js
+++ b/amd/src/ui_ace.js
@@ -86,6 +86,8 @@ define(['jquery'], function($) {
session = this.editor.getSession();
session.setValue(this.textarea.val());
+ this.fixSlowLoad();
+
// Set theme if available (not currently enabled).
if (params.theme) {
this.editor.setTheme("ace/theme/" + params.theme);
@@ -177,6 +179,16 @@ define(['jquery'], function($) {
this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});
};
+ // Sometimes Ace editors do not load until the mouse is moved. To fix this,
+ // 'move' the mouse using JQuerry when the editor div enters the viewport.
+ AceWrapper.prototype.fixSlowLoad = function () {
+ const observer = new IntersectionObserver( () => {
+ $(document).trigger('mousemove');
+ });
+ const editNode = this.editNode.get(0); // Non-JQuerry node.
+ observer.observe(editNode);
+ };
+
AceWrapper.prototype.setEventHandlers = function () {
var TAB = 9,
ESC = 27,
diff --git a/amd/src/ui_scratchpad.js b/amd/src/ui_scratchpad.js
new file mode 100644
index 000000000..934725939
--- /dev/null
+++ b/amd/src/ui_scratchpad.js
@@ -0,0 +1,506 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more util.details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Implementation of the html_ui user interface plugin. For overall details
+ * of the UI plugin architecture, see userinterfacewrapper.js.
+ *
+ * This plugin replaces the usual textarea answer element with a div
+ * containing the author-supplied HTML. The serialisation of that HTML,
+ * which is what is essentially copied back into the textarea for submissions
+ * as the answer, is a JSON object. The fields of that object are the names
+ * of all author-supplied HTML elements with a class 'coderunner-ui-element';
+ * all such objects are expected to have a 'name' attribute as well. The
+ * associated field values are lists. Each list contains all the values, in
+ * document order, of the results of calling the jquery val() method in turn
+ * on each of the UI elements with that name.
+ * This means that at least input, select and textarea
+ * elements are supported. The author is responsible for checking the
+ * compatibility of other elements with jquery's val() method.
+ *
+ * The HTML to use in the answer area must be provided as the contents of
+ * either the globalextra field or the prototypeextra field in the question
+ * authoring form. The choice of which is set by the html_src UI parameter, which
+ * must be either 'globalextra' or 'prototypeextra'.
+ *
+ * If any fields of the answer html are to be preloaded, these should be specified
+ * in the answer preload with json of the form '{"": "",...}'
+ * where fieldValueList is a list of all the values to be assigned to the fields
+ * with the given name, in document order.
+ *
+ * To accommodate the possibility of dynamic HTML, any leftover preload values,
+ * that is, values that cannot be positioned within the HTML either because
+ * there is no field of the required name or because, in the case of a list,
+ * there are insufficient elements, are assigned to the data['leftovers']
+ * attribute of the outer html div, as a sub-object of the original object.
+ * This outer div can be located as the 'closest' (in a jQuery sense)
+ * div.qtype-coderunner-html-outer-div. The author-supplied HTML must include
+ * JavaScript to make use of the 'leftovers'.
+ *
+ * As a special case of the serialisation, if all values in the serialisation
+ * are either empty strings or a list of empty strings, the serialisation is
+ * itself the empty string.
+ *
+ * @module coderunner/ui_html
+ * @copyright Richard Lobb, 2022, The University of Canterbury
+ * @copyright James Napier, 2022, The University of Canterbury
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import $ from 'jquery';
+import ajax from 'core/ajax';
+import {get_string as getLangString} from 'core/str';
+import Templates from 'core/templates';
+
+import {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';
+
+
+const RESULT_SUCCESS = 15; // Code for a correct Jobe run.
+const DEFAULT_MAX_OUTPUT_LEN = 30000;
+
+
+/**
+ * Escape text special HTML characters.
+ * @param {string} text
+ * @returns {string} text with various special chars replaced with equivalent
+ * html entities. Newlines are replaced with .
+ */
+const escapeHtml = text => {
+ const map = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": '''
+ };
+ return text.replace(/[&<>"']/g, function(m) {
+ return map[m];
+ });
+};
+
+/**
+ * Analyse the response for errors. There are two sorts of error: sandbox failures,
+ * for which the field response.error is non-zero meaning the run didn't take
+ * place at all and failures in the run
+ * itself, such as compile errors, timeouts, runtime errors etc. The
+ * various codes are documented in the CodeRunner file sandbox.php.
+ * Some error returns, notably compilation error and runtime error, are not
+ * treated as errors here, since the stdout + stderr should reveal what
+ * happened anyway. More obscure errors are lumped together as 'Unknown
+ * runtime error'.
+ * @param {object} response The response from the web-service sandbox request.
+ * @returns string The language string to use for an error message or '' if
+ * no error message.
+ */
+const diagnose = response => {
+ // Table of error conditions.
+ // Each row is response.error, response.result, langstring
+ // response.result is ignored if response.error is non-zero.
+ // Any condition not in the table is deemed an "Unknown runtime error".
+ const ERROR_RESPONSES = [
+ [1, 0, 'error_access_denied'], // Sandbox AUTH_ERROR
+ [2, 0, 'error_unknown_language'], // Sandbox WRONG_LANG_ID
+ [3, 0, 'error_access_denied'], // Sandbox ACCESS_DENIED
+ [4, 0, 'error_submission_limit_reached'], // Sandbox SUBMISSION_LIMIT_EXCEEDED
+ [5, 0, 'error_sandbox_server_overload'], // Sandbox SERVER_OVERLOAD
+ [0, 11, ''], // RESULT_COMPILATION_ERROR
+ [0, 12, ''], // RESULT_RUNTIME_ERROR
+ [0, 13, 'error_timeout'], // RESULT TIME_LIMIT
+ [0, RESULT_SUCCESS, ''], // RESULT_SUCCESS
+ [0, 17, 'error_memory_limit'], // RESULT_MEMORY_LIMIT
+ [0, 21, 'error_sandbox_server_overload'], // RESULT_SERVER_OVERLOAD
+ [0, 30, 'error_excessive_output'] // RESULT OUTPUT_LIMIT
+ ];
+ for (const row of ERROR_RESPONSES) {
+ if (row[0] == response.error && (response.error != 0 || response.result == row[1])) {
+ return row[2];
+ }
+ }
+ return 'error_unknown_runtime';
+};
+
+/**
+ * Get the specified language string using
+ * AJAX and plug it into the given textarea
+ * @param {string} langStringName The language string name.
+ * @param {DOMnode} textarea The textarea into which the error message
+ * should be plugged.
+ * @param {string} additionalText Extra text to follow the result code.
+ */
+const setLangString = async(langStringName, textarea, additionalText) => {
+ const message = await getLangString(langStringName, 'filter_ace_inline');
+ textarea.show();
+ textarea.html(escapeHtml("*** " + message + " ***\n" + additionalText));
+};
+
+/**
+ * Concatenates the cmpinfo, stdout and stderr fields of the sandbox
+ * response, truncating both stdout and stderr to a given maximum length
+ * if necessary (in which case '... (truncated)' is appended.
+ * @param {object} response Sandbox response object
+ * @param {int} maxLen The maximum length of the trimmed stringlen.
+ */
+const combinedOutput = (response, maxLen) => {
+ const limit = s => s.length <= maxLen ? s : s.substr(0, maxLen) + '... (truncated)';
+ return response.cmpinfo + limit(response.output) + limit(response.stderr);
+};
+
+/**
+ * Invert serialisation from '1' to '', vice versa.
+ * @param {string} current serialisation.
+ * @returns {string} inverted serialisation.
+ */
+const invertSerial = current => current[0] == '1' ? [''] : ['1'];
+
+/**
+ * Insert the answer code and test code into the wrapper. This may
+ * defined by the user, in UI Params or globalextra. If prefixAns is
+ * false: do not include answerCode in final wrapper.
+ * @param {string} answerCode text.
+ * @param {string} testCode text.
+ * @param {string} prefixAns '1' for true, '' for false.
+ * @param {string} template provided in UI Params or globalextra.
+ * @returns {string} filled template.
+ */
+const fillWrapper = (answerCode, testCode, prefixAns, template) => {
+ if (!template) {
+ template = '{{ ANSWER_CODE }}\n' +
+ '{{ SCRATCHPAD_CODE }}';
+ }
+ if (!prefixAns) {
+ answerCode = '';
+ }
+ template = template.replaceAll('{{ ANSWER_CODE }}', answerCode);
+ template = template.replaceAll('{{ SCRATCHPAD_CODE }}', testCode);
+ return template;
+};
+
+/**
+ * Returns anew object containg default values. If a matching key exists in
+ * prescribed, the corresponding value from prescribed will replace the defualt value.
+ * Does not add keys/values to the result if that key is not in defualts.
+ * @param {object} defaults object with values to be overwritten.
+ * @param {object} prescribed settings, typically set by a user.
+ * @returns {object} filled with defualt values, overwritten by their prescribed value (iff included).
+ */
+const overwriteValues = (defaults, prescribed) => {
+ let overwritten = {...defaults};
+ if (prescribed) {
+ for (const [key, value] of Object.entries(defaults)) {
+ overwritten[key] = prescribed[key] || value;
+ }
+ }
+ return overwritten;
+};
+
+
+/**
+ * Is an element currently hidden?
+ * @param {Element} el to check visibility of.
+ * @returns {boolean} true if el is visible.
+ */
+const isVisible = (el) => {
+ const styles = window.getComputedStyle(el);
+ return styles.display === 'none' || styles.visibility === 'hidden';
+};
+
+/**
+ * Constructor for the ScratchpadUi object.
+ * @param {string} textAreaId The ID of the html textarea.
+ * @param {int} width The width in pixels of the textarea.
+ * @param {int} height The height in pixels of the textarea.
+ * @param {object} uiParams The UI parameter object.
+ */
+class ScratchpadUi {
+ constructor(textAreaId, width, height, uiParams) {
+ const DEF_UI_PARAMS = {
+ scratchpad_name: '',
+ button_name: '',
+ prefix_name: '',
+ help_text: '',
+ run_lang: uiParams.lang, // Use answer's ace language if not specified.
+ html_output: false,
+ disable_scratchpad: false,
+ wrapper_src: null
+ };
+
+ this.textArea = document.getElementById(textAreaId);
+ this.textAreaId = textAreaId;
+ this.height = height;
+ this.readOnly = this.textArea.readonly;
+ this.fail = false;
+
+ this.lang = uiParams.lang;
+
+ uiParams.num_rows = this.textArea.readOnly;
+ this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);
+
+ // Find the run wrapper source location.
+ this.runWrapper = null;
+ const wrapperSrc = this.uiParams.wrapper_src;
+ if (wrapperSrc) {
+ if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {
+ this.runWrapper = this.textArea.dataset[wrapperSrc];
+ } else {
+ // TODO: raise some sort of exception? Invalid, params.
+ // Bad wrapper src provided by user...
+ this.runWrapper = null;
+ }
+ }
+
+ this.outerDiv = null; // TODO: Investigate...
+ this.scratchpadDiv = null;
+ this.reload(); // Draw my beautiful blobs.
+ }
+
+ failed() {
+ return this.fail;
+ }
+
+ failMessage() {
+ return this.failString;
+ }
+
+ sync() {
+ if (!this.context) {
+ return;
+ }
+ const prefixAns = document.getElementById(this.context.prefix_ans.id);
+ const showHide = document.getElementById(this.context.show_hide.id);
+
+ let serialisation = {
+ answer_code: [''],
+ test_code: [''],
+ show_hide: [''],
+ prefix_ans: ['']
+ };
+ if (this.answerTextarea) {
+ serialisation.answer_code = [this.answerTextarea.value];
+ }
+ if (this.testTextarea) {
+ serialisation.test_code = [this.testTextarea.value];
+ }
+ if (!isVisible(showHide)) {
+ serialisation.show_hide = ['1'];
+ }
+ if (prefixAns?.checked) {
+ serialisation.prefix_ans = ['1'];
+ }
+
+ serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);
+ if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {
+ serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);
+ this.textArea.value = JSON.stringify(serialisation);
+ } else {
+ this.textArea.value = ''; // All fields empty...
+ }
+ }
+
+ getElement() {
+ return this.outerDiv;
+ }
+
+ async handleRunButtonClick(ajax, outputDisplayArea) {
+ outputDisplayArea = $(outputDisplayArea);
+ this.sync(); // Use up-to-date serialization.
+
+ const htmlOutput = this.uiParams.html_output;
+ const maxLen = this.uiParams['max-output-length'] || DEFAULT_MAX_OUTPUT_LEN;
+ const preloadString = $(this.textArea).val();
+ const serial = this.serialize(preloadString);
+ const params = this.uiParams.params;
+ const code = fillWrapper(
+ serial.answer_code,
+ serial.test_code,
+ serial.prefix_ans[0],
+ this.runWrapper
+ );
+
+ // Clear all output areas.
+ outputDisplayArea.html('');
+ if (htmlOutput) {
+ outputDisplayArea.hide();
+ }
+ outputDisplayArea.next('div.filter-ace-inline-html').remove(); // TODO: Naming
+
+
+ ajax.call([{
+ methodname: 'qtype_coderunner_run_in_sandbox',
+ args: {
+ contextid: M.cfg.contextid, // Moodle context ID
+ sourcecode: code,
+ language: this.uiParams.run_lang,
+ params: JSON.stringify(params) // Sandbox params
+ },
+ done: function(responseJson) {
+ const response = JSON.parse(responseJson);
+ const error = diagnose(response);
+ if (error === '') {
+ // If no errors or compilation error or runtime error
+ if (!htmlOutput || response.result !== RESULT_SUCCESS) {
+ // Either it's not HTML output or it is but we have compilation or runtime errors.
+ const text = combinedOutput(response, maxLen);
+ outputDisplayArea.show();
+ if (text.trim() === '') {
+ outputDisplayArea.html('< No output! >');
+ } else {
+ outputDisplayArea.html(escapeHtml(text));
+ }
+ } else { // Valid HTML output - just plug in the raw html to the DOM.
+ // Repeat the deletion of previous output in case of multiple button clicks.
+ outputDisplayArea.next('div.filter-ace-inline-html').remove();
+
+ const html = $("
" +
+ response.output + "
");
+ outputDisplayArea.after(html);
+ }
+ } else {
+ // If an error occurs, display the language string in the
+ // outputDisplayArea plus additional info.
+ let extra = response.error == 0 ? combinedOutput(response, maxLen) : '';
+ if (error === 'error_unknown_runtime') {
+ extra += response.error ? '(Sandbox error code ' + response.error + ')' :
+ '(Run result: ' + response.result + ')';
+ }
+ setLangString(error, outputDisplayArea, extra);
+ }
+ },
+ fail: function(error) {
+ alert(error.message);
+ }
+ }]);
+ }
+
+ updateContext(preload) {
+ this.context = {
+ "id": this.textAreaId,
+ "disable_scratchpad": this.uiParams.disable_scratchpad,
+ "scratchpad_name": this.uiParams.scratchpad_name,
+ "button_name": this.uiParams.button_name,
+ "prefix_name": this.uiParams.prefix_name,
+ "help_text": {"text": this.uiParams.help_text}, // TODO: context doesnt match...
+ "answer_code": {
+ "id": this.textAreaId + '_answer-code',
+ "text": preload.answer_code[0],
+ "lang": this.lang,
+ "rows": this.uiParams.rows
+ },
+ "test_code": {
+ "id": this.textAreaId + '_test-code',
+ "text": preload.test_code[0],
+ "lang": this.lang,
+ "rows": 6
+ },
+ "show_hide": {
+ "id": this.textAreaId + '_scratchpad',
+ "show": preload.show_hide[0]
+ },
+ "prefix_ans": {
+ "id": this.textAreaId + '_prefix-ans',
+ "checked": preload.prefix_ans[0]
+ },
+ "output_display": {
+ "id": this.textAreaId + '_output-displayarea'
+ },
+ "jquery_escape": function() {
+ return function(text, render) {
+ return $.escapeSelector(render(text));
+ };
+ }
+ };
+ }
+
+ serialize(preloadString) {
+ const defaultSerial = {
+ answer_code: [''],
+ test_code: [''],
+ show_hide: [''],
+ prefix_ans: ['1'] // Ticked by default!
+ };
+ let serial;
+ if (preloadString) {
+ serial = JSON.parse(preloadString);
+ }
+ serial = overwriteValues(defaultSerial, serial);
+ return serial;
+ }
+
+ async reload() {
+ const preloadString = this.textArea.value;
+ let preload;
+ try {
+ preload = this.serialize(preloadString);
+ } catch (error) {
+ this.fail = true;
+ this.failString = 'scratchpad_ui_invalidserialisation';
+ return;
+ }
+
+ this.updateContext(preload);
+
+ try {
+ const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);
+
+ const div = document.createElement('div');
+ div.innerHTML = html;
+ document.getElementById(this.textAreaId)
+ .nextSibling
+ .innerHTML = html;
+ this.answerTextarea = document.getElementById(this.context.answer_code.id);
+ this.testTextarea = document.getElementById(this.context.test_code.id);
+
+ this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);
+ if (this.testTextarea) {
+ this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);
+ }
+
+ const runButton = document.getElementById(this.textAreaId + '_run-btn');
+ const outputDisplayarea = document.getElementById(this.context.output_display.id);
+ if (runButton) {
+ runButton.addEventListener('click', () => this.handleRunButtonClick(ajax, outputDisplayarea));
+ }
+ } catch (e) {
+ this.fail = true;
+ this.failString = "UI template failed to load."; // TODO: Lang-string goes here.
+ }
+
+ // No resizing the outer wrapper. Instead, resize the two sub UIs,
+ // they will expand accordingly.
+ document.getElementById(this.textAreaId + '_wrapper').style.resize = 'none';
+ }
+
+ resize() {} // Nothing to see here. Move along please.
+
+ hasFocus() {
+ let focused = false;
+ if (this.answerCodeUi?.uiInstance.hasFocus()) {
+ focused = true;
+ }
+ if (this.testCodeUi?.uiInstance.hasFocus()) {
+ focused = true;
+ }
+ return focused;
+ }
+
+ destroy() {
+ this.sync();
+ this.outerDiv?.remove();
+ this.outerDiv = null;
+ }
+}
+
+
+export {ScratchpadUi as Constructor};
diff --git a/amd/src/ui_scratchpad.json b/amd/src/ui_scratchpad.json
new file mode 100644
index 000000000..5dedf9fd1
--- /dev/null
+++ b/amd/src/ui_scratchpad.json
@@ -0,0 +1,41 @@
+{
+ "name": "ui_scratchpad",
+ "parameters": {
+ "scratchpad_name": {
+ "type": "string",
+ "default": ""
+ },
+ "button_name": {
+ "type": "string",
+ "default": ""
+ },
+ "prefix_name": {
+ "type": "string",
+ "default": ""
+ },
+ "help_text": {
+ "type": "string",
+ "default": ""
+ },
+ "run_lang": {
+ "type": "string",
+ "default": null
+ },
+ "wrapper_src": {
+ "type": "string",
+ "default": null
+ },
+ "params": {
+ "type": "object",
+ "default": null
+ },
+ "html_output": {
+ "type": "boolean",
+ "default": false
+ },
+ "disable_scratchpad": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+}
diff --git a/classes/util.php b/classes/util.php
index f03824a89..cff92266c 100644
--- a/classes/util.php
+++ b/classes/util.php
@@ -36,7 +36,7 @@ public static function load_uiplugin_js($question, $textareaid) {
$uiplugin = $question->uiplugin === null ? 'ace' : strtolower($question->uiplugin);
if ($uiplugin !== '' && $uiplugin !== 'none') {
$params = array($uiplugin, $textareaid); // Params to plugin's init function.
- if (strpos($uiplugin, 'ace') !== false || strpos($uiplugin, 'html') !== false) {
+ if (strpos($uiplugin, 'ace') !== false || strpos($uiplugin, 'html') !== false ||strpos($uiplugin, 'scratchpad') !== false ) {
self::load_ace();
}
$PAGE->requires->js_call_amd('qtype_coderunner/userinterfacewrapper', 'newUiWrapper', $params);
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index 6f10fb2fc..cd0b8834b 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -1242,3 +1242,19 @@ function should be applied, e.g. {{STUDENT_ANSWER | e(\'py\')}} is
$string['wssubmissionrateexceeded'] = 'You have exceeded the maximum hourly \'Try it!\' submission rate. Request denied.';
$string['xmlcoderunnerformaterror'] = 'XML format error in coderunner question';
+
+
+$string['scratchpadui_scratchpad_name_descr'] = 'The display name of the scratchpad, used to hide/un-hide the scratchpad.';
+$string['scratchpadui_button_name_descr'] = 'The run button text';
+$string['scratchpadui_prefix_name_descr'] = 'The prefix with answer check-box label text.';
+$string['scratchpadui_run_lang_descr'] = 'The language used to run code when the run button is clicked, this should be the language your wrapper is written in (if applicable).';
+$string['scratchpadui_run_wrapper_descr'] = 'The wrapper to be used by the run button: setting to `globalextra` will use text in global extra as the wrapper. Otherwise, the string in this parameter will be used';
+$string['scratchpadui_params_descr'] = 'The prefix with answer check-box label text.';
+$string['scratchpadui_html_output_descr'] = 'Display the output from run as raw HTML instead of text.';
+
+$string['scratchpadui_def_button_name'] = 'Run!';
+$string['scratchpadui_def_scratchpad_name'] = 'Scratchpad';
+$string['scratchpadui_def_prefix_name'] = 'Prefix with Answer';
+$string['scratchpadui_def_help_text'] = 'You can enter code into this panel and click \'Run\' to execute it.
+By default, the code in this panel is prefixed with the contents of the answer box, giving you an easy way to test your answer.
+You can uncheck the \'Prefix with answer\' checkbox to run the code in this panel standalone, e.g. to explore how small code fragments behave.';
diff --git a/templates/answer_textarea.mustache b/templates/answer_textarea.mustache
new file mode 100644
index 000000000..826940eb1
--- /dev/null
+++ b/templates/answer_textarea.mustache
@@ -0,0 +1,36 @@
+{{!
+ This file is part of Moodle - https://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template qtype_coderunner/answer_textarea
+
+
+
+ Example context (json):
+ {
+ "id": "question:12",
+ "text": "print('hello world!')"
+ "rows": 16,
+ "name": "answer_code",
+ "lang": "C"
+ }
+}}
+
diff --git a/templates/help_icon.mustache b/templates/help_icon.mustache
new file mode 100644
index 000000000..8b602566b
--- /dev/null
+++ b/templates/help_icon.mustache
@@ -0,0 +1,41 @@
+{{!
+ This file is part of Moodle - https://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template qtype_coderunner/help_icon
+
+ Help icon.
+
+ Example context (json):
+ {
+ "title": "Help with something",
+ "url": "http://example.org/help",
+ "linktext": "",
+ "icon":{
+ "attributes": [
+ {"name": "class", "value": "iconhelp"},
+ {"name": "src", "value": "../../../pix/help.svg"},
+ {"name": "alt", "value": "Help icon"}
+ ]
+ }
+ }
+}}
+
+ {{#pix}}help, core, {{{alt}}}{{/pix}}
+
diff --git a/templates/output_displayarea.mustache b/templates/output_displayarea.mustache
new file mode 100644
index 000000000..c8f92106d
--- /dev/null
+++ b/templates/output_displayarea.mustache
@@ -0,0 +1,30 @@
+{{!
+ This file is part of Moodle - https://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template qtype_coderunner/output_displayarea
+
+
+
+ Example context (json):
+ {
+ "id": "question:12",
+ "text": "hello world"
+ }
+}}
+
{{ text }}
diff --git a/templates/scratchpad.mustache b/templates/scratchpad.mustache
new file mode 100644
index 000000000..f5fa5574d
--- /dev/null
+++ b/templates/scratchpad.mustache
@@ -0,0 +1,61 @@
+{{!
+ This file is part of Moodle - https://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template qtype_coderunner/scratchpad
+
+ Example context (json):
+ {
+ "id": "question:12",
+ "answer_code": {
+ "id": "question:12_answer-code"
+ "rows": 16,
+ "name": "answer_code",
+ "lang": "C"
+ },
+ "test_code": {
+ "id": "question:12_test-code",
+ "rows": 4,
+ "name": "test_code",
+ "lang": "C"
+ },
+ "prefix_ans": {
+ "id": "question:12_prefix-ans"
+ "checked": true
+ },
+ "output_display": {
+ "id": "question:12",
+ }
+ }
+}}
+
+
+
+
+
+
+
+ {{# str }}scratchpadui_def_scratchpad_name, qtype_coderunner{{/ str }}
+
+{{# show_hide }}
diff --git a/templates/scratchpad_controls.mustache b/templates/scratchpad_controls.mustache
new file mode 100644
index 000000000..991e30326
--- /dev/null
+++ b/templates/scratchpad_controls.mustache
@@ -0,0 +1,43 @@
+{{!
+ This file is part of Moodle - https://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template qtype_coderunner/scratchpad_controls
+
+ Example context (json):
+ {
+ "id": "question:12",
+ "prefix_ans": {
+ "id": "question:12_prefix-ans"
+ "checked": true
+ },
+ }
+}}
+
diff --git a/templates/scratchpad_ui.mustache b/templates/scratchpad_ui.mustache
new file mode 100644
index 000000000..e9b3fef68
--- /dev/null
+++ b/templates/scratchpad_ui.mustache
@@ -0,0 +1,48 @@
+{{!
+ This file is part of Moodle - https://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template qtype_coderunner/scratchpad
+
+ Example context (json):
+ {
+ "id": "question:12",
+ "answer_code": {
+ "id": "question:12_answer-code"
+ "rows": 16,
+ "name": "answer_code",
+ "lang": "C"
+ },
+ "test_code": {
+ "id": "question:12_test-code",
+ "rows": 4,
+ "name": "test_code",
+ "lang": "C"
+ },
+ "prefix_ans": {
+ "id": "question:12_prefix-ans"
+ "checked": true
+ },
+ "output_display": {
+ "id": "question:12",
+ "text": "hello world"
+ }
+ }
+}}
+
diff --git a/tests/behat/attachmentimportexport.feature b/tests/behat/attachmentimportexport.feature
index 890832c05..21c82836f 100644
--- a/tests/behat/attachmentimportexport.feature
+++ b/tests/behat/attachmentimportexport.feature
@@ -20,6 +20,7 @@ Feature: Test importing and exporting of question with attachments
And the following "questions" exist:
| questioncategory | qtype | name |
| Test questions | coderunner | Square function |
+ And the CodeRunner sandbox is enabled
And I am on the "Square function" "core_question > edit" page logged in as teacher
And I click on "a[aria-controls='id_attachmentoptionscontainer']" "css_element"
And I set the field "Answer" to "from sqrmodule import sqr"
diff --git a/tests/behat/behat_coderunner.php b/tests/behat/behat_coderunner.php
index bae290738..b666ea11a 100644
--- a/tests/behat/behat_coderunner.php
+++ b/tests/behat/behat_coderunner.php
@@ -27,6 +27,106 @@
class behat_coderunner extends behat_base {
+ /**
+ * Sets the webserver sandbox to enabled for testing purposes.
+ *
+ * @Given /^the CodeRunner sandbox is enabled/
+ */
+ public function the_coderunner_sandbox_is_enabled() {
+ set_config('wsenabled', 1, 'qtype_coderunner');
+ set_config('jobesandbox_enabled', 1, 'qtype_coderunner');
+ set_config('jobe_host', '172.17.0.1:4000', 'qtype_coderunner');
+ }
+
+ /**
+ * Checks that a given string appears within answer textarea.
+ * Intended for checking UI serialization
+ * @Then /^I should see in answer field "(?P(?:[^"]|\\")*)"$/
+ * @throws ExpectationException
+ * @param string $expected The string that we expect to find
+ */
+ public function i_should_see_in_answer($expected) {
+ $xpath = '//textarea[contains(@class, "coderunner-answer")]';
+ $driver = $this->getSession()->getDriver();
+ if (!$driver->find($xpath)) {
+ $error = "Answer box not found!";
+ throw new ExpectationException($error, $this->getSession());
+ }
+ $page = $this->getSession()->getPage();
+ $val = $page->find('xpath',$xpath)->getValue();
+ if ($val !== $expected) {
+ $error = "'$val' does not match '$expected'";
+ throw new ExpectationException($error, $this->getSession());
+ }
+ }
+
+ /**
+ * Sets answer textarea (seen after presing ctrl+m) to a value
+ * @Then /^I set answer field to "(?P(?:[^"]|\\")*)"$/
+ * @throws ExpectationException
+ * @param string $expected The string that we expect to find
+ */
+ public function i_set_answer($value) {
+ $xpath = '//textarea[contains(@class, "coderunner-answer")]';
+ $driver = $this->getSession()->getDriver();
+ if (!$driver->find($xpath)) {
+ $error = "Answer box not found!";
+ throw new ExpectationException($error, $this->getSession());
+ }
+ $page = $this->getSession()->getPage();
+ $val = $page->find('xpath',$xpath)->setValue($value);
+ }
+
+ /**
+ * Sets answer textarea (seen after presing ctrl+m) to a value
+ * @Then /^I set answer field to:$/
+ * @throws ExpectationException
+ * @param string $expected The string that we expect to find
+ */
+ public function i_set_answer_pystring($pystring) {
+ $this->i_set_answer($pystring->getRaw());
+ }
+
+ /**
+ * Checks that a given string appears within answer textarea.
+ * Intended for checking UI serialization
+ * @Then /^I should see in answer field:$/
+ */
+ public function i_should_see_in_answer_pystring(Behat\Gherkin\Node\PyStringNode $pystring) {
+ $this->i_should_see_in_answer($pystring->getRaw());
+ }
+
+ /**
+ * Sets the ace editor content to provided string, using name of associated textarea.
+ * NOTE: this assumes the existence of a text area next to a
+ * UI wrapper div containing the Ace div!
+ * Intended as a replacement for I set field to , for ace fields.
+ * @Then /^I set the ace field "(?P(?:[^"]|\\")*)" to "(?P(?:[^"]|\\")*)"$/
+ * @throws ExpectationException
+ * @param string $expected The string that we expect to find
+ */
+ public function i_set_ace_field($elname, $value) {
+ $xpath = "//textarea[@name='$elname']/../div/div";
+ $driver = $this->getSession()->getDriver();
+ // Does the div managed by Ace exist?
+ if (!$driver->find($xpath)) {
+ $error = "Ace editor not found!";
+ throw new ExpectationException($error, $this->getSession());
+ }
+ // We inject JS into the browser to set the Ace editor contents...
+ // (Gross) JS to take the x-path for the div managed by Ace,
+ // open editor for that div, and set the editors value.
+ $javascript = "const editorNode = document.evaluate("
+ . "`$xpath`,"
+ . "document,"
+ . "null,"
+ . "XPathResult.ANY_TYPE,null,"
+ . ");"
+ . "const editor = ace.edit(editorNode.iterateNext());"
+ . "editor.setValue(`$value`);";
+ $this->getSession()->executeScript($javascript);
+ }
+
/**
* Checks that a given string appears within a visible ins or del element
* that has a background-color attribute that is not 'inherit'.
diff --git a/tests/behat/scratchpad_ui.feature b/tests/behat/scratchpad_ui.feature
new file mode 100644
index 000000000..82429f6f7
--- /dev/null
+++ b/tests/behat/scratchpad_ui.feature
@@ -0,0 +1,291 @@
+@qtype @qtype_coderunner @javascript @scratchpad
+Feature: Test the Scratchpad UI
+ In order to use the Scratchpad UI
+ As a teacher
+ I should be able specify the required html in either globalextra or prototypeextra
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@asd.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And the following "question categories" exist:
+ | contextlevel | reference | name |
+ | Course | C1 | Test questions |
+ And the following "questions" exist:
+ | questioncategory | qtype | name | template |
+ | Test questions | coderunner | Print answer | printans |
+ And the CodeRunner sandbox is enabled
+
+ Scenario: Edit a CodeRunner question into a Scratchpad UI question
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ Then I should not see "Run!"
+ And I should see "Scratchpad"
+
+ When I click on "Scratchpad" "button"
+ Then I should see "Run!"
+ And I should see "Prefix with Answer"
+
+ When I click on "Scratchpad" "button"
+ Then I should not see "Run!"
+ And I should not see "Prefix Answer?"
+
+ Scenario: Click the run button with program that outputs nothing.
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+
+ Then I press "Run!"
+ And I should see "< No output! >"
+
+ Scenario: Click the run button with program that outputs in Scratchpad Code
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "test_code" to "print(\"hello\" + \" \" + \"world\")"
+
+ Then I press "Run!"
+ And I should see "hello world"
+
+ Scenario: Click the run button with program that outputs in Answer Code
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "answer_code" to "print(\"hello\" + \" \" + \"world\")"
+
+ Then I press "Run!"
+ And I should see "hello world"
+
+ Then I set the field "prefix_ans" to ""
+ And I press "Run!"
+ And I should not see "hello world"
+
+ Scenario: Click the run button with program that outputs in Answer Code and Scratchpad Code
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "answer_code" to "print(\"hello\" + \" \" + \"world\")"
+ And I set the ace field "test_code" to "print(\"goodbye\" + \" \" + \"world\")"
+
+ When I press "Run!"
+ Then I should see "hello world"
+ And I should see "goodbye world"
+
+ When I set the field "prefix_ans" to ""
+ And I press "Run!"
+ Then I should not see "hello world"
+ And I should see "goodbye world"
+
+ @serial
+ Scenario: Get empty UI serialization
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+ | id_expected_0 | "" |
+ And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ Then I press the CTRL + ALT M key
+ And I should see in answer field ""
+
+ When I press "Check"
+ Then I should see "The submission was invalid, and has been disregarded without penalty."
+
+ @serial
+ Scenario: Get UI serialization, with answer code, while Scratchpad Hidden
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+ | id_expected_0 | '' |
+ And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I set the ace field "answer_code" to "print('hello world')"
+ Then I press the CTRL + ALT M key
+ And I should see in answer field:
+ """
+ {"answer_code":["print('hello world')"],"test_code":[""],"show_hide":[""],"prefix_ans":["1"]}
+ """
+
+ @serial
+ Scenario: Get UI serialization, answer code entered, Scratchpad shown
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+ | id_expected_0 | '' |
+ And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I set the ace field "answer_code" to "print('hello world')"
+ And I click on "Scratchpad" "button"
+
+ Then I press the CTRL + ALT M key
+ And I should see in answer field:
+ """
+ {"answer_code":["print('hello world')"],"test_code":[""],"show_hide":["1"],"prefix_ans":["1"]}
+ """
+
+ @serial
+ Scenario: Get UI serialization, Scratchpad code entered, Scratchpad shown, NO prefix
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+ | id_expected_0 | '' |
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "test_code" to "print('hello world')"
+ And I set the field "prefix_ans" to ""
+
+ Then I press the CTRL + ALT M key
+ And I should see in answer field:
+ """
+ {"answer_code":[""],"test_code":["print('hello world')"],"show_hide":["1"],"prefix_ans":[""]}
+ """
+
+ When I press the CTRL + ALT M key
+ And I click on "Scratchpad" "button"
+ And I press the CTRL + ALT M key
+ Then I should see in answer field:
+ """
+ {"answer_code":[""],"test_code":["print('hello world')"],"show_hide":[""],"prefix_ans":[""]}
+ """
+
+ Scenario: Get UI serialization, while Scratchpad Shown and prefix box NOT ticked
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+ | id_expected_0 | '' |
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the field "prefix_ans" to ""
+
+ Then I press the CTRL + ALT M key
+ And I should see in answer field:
+ """
+ {"answer_code":[""],"test_code":[""],"show_hide":["1"],"prefix_ans":[""]}
+ """
+
+ @serial
+ Scenario: Get UI serialization, answer code and test code entered, Scratchpad shown, prefix NOT ticked
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+ | id_expected_0 | '' |
+ And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "answer_code" to "print('hello world')"
+ And I set the ace field "test_code" to "print('goodbye world')"
+ And I set the field "prefix_ans" to ""
+
+ Then I press the CTRL + ALT M key
+ And I should see in answer field:
+ """
+ {"answer_code":["print('hello world')"],"test_code":["print('goodbye world')"],"show_hide":["1"],"prefix_ans":[""]}
+ """
+
+ @serial
+ Scenario: Only enter serialization with answer_code value, no other fields. Useful for converting Ace questions to Scratchpad.
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+ | id_expected_0 | '' |
+ And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ Then I press the CTRL + ALT M key
+ And I set answer field to:
+ """
+ {"answer_code":["print('hello world')"]}
+ """
+ And I press the CTRL + ALT M key
+ Then I should see "print('hello world')"
+
+ When I press the CTRL + ALT M key
+ Then I should see in answer field:
+ """
+ {"answer_code":["print('hello world')"],"test_code":[""],"show_hide":[""],"prefix_ans":["1"]}
+ """
diff --git a/tests/behat/scratchpad_ui_params.feature b/tests/behat/scratchpad_ui_params.feature
new file mode 100644
index 000000000..c1feb0351
--- /dev/null
+++ b/tests/behat/scratchpad_ui_params.feature
@@ -0,0 +1,234 @@
+@qtype @qtype_coderunner @javascript @scratchpad @scratchpaduiparam
+Feature: Test the Scratchpad UI, UI Params
+ In order to use the Scratchpad UI
+ As a teacher
+ I should be able specify the UI Paramiters to change the Scratchpad UI
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@asd.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And the following "question categories" exist:
+ | contextlevel | reference | name |
+ | Course | C1 | Test questions |
+ And the following "questions" exist:
+ | questioncategory | qtype | name | template |
+ | Test questions | coderunner | Print answer | printans |
+ And the CodeRunner sandbox is enabled
+
+ And I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_validateonsave" to ""
+
+ Scenario: Edit a CodeRunner Scratchpad UI question, change all available UI params
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+
+ And I set the field "id_uiparameters" to:
+ """
+ {
+ "button_name": "Ran!",
+ "scratchpad_name":"Scratchblobert",
+ "html_output":true,
+ "prefix_name":"unhelpful label :)",
+ "wrapper_src": "globalextra",
+ "run_lang": "Python3",
+ "params": {
+ "numprocs":100,
+ "memlimit":1000
+ }
+ }
+ """
+ And I press "id_updatebutton"
+
+ Then I should not see "The UI parameters for this question or its prototype are broken. Proceed with caution."
+
+ Scenario: Change UI param for run button name
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"button_name": "superuniquename123"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I should not see "superuniquename123"
+ And I should not see "Run!"
+ And I should see "Scratchpad"
+
+ When I click on "Scratchpad" "button"
+ Then I should see "superuniquename123"
+ But I should not see "Run!"
+ And I should see "Prefix with Answer"
+
+ When I click on "Scratchpad" "button"
+ Then I should not see "superuniquename123"
+ And I should not see "Run!"
+ And I should not see "Prefix Answer?"
+
+ Scenario: Change UI param for Scratchpad name
+ When I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"scratchpad_name": "superuniquename123"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ Then I should not see "Run!"
+ And I should not see "Scratchpad"
+ But I should see "superuniquename123"
+
+ When I click on "superuniquename123" "button"
+ Then I should see "superuniquename123"
+ And I should see "Run!"
+ And I should see "Prefix with Answer"
+
+ When I click on "superuniquename123" "button"
+ And I should not see "Run!"
+ And I should not see "Prefix Answer?"
+
+ Scenario: Change UI param for run button name
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"prefix_name": "superuniquename123"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ Then I should see "Scratchpad"
+ And I should not see "superuniquename123"
+ And I should not see "Run!"
+
+ When I click on "Scratchpad" "button"
+ Then I should see "superuniquename123"
+ But I should not see "Prefix Answer?"
+ And I should see "Run!"
+
+ When I click on "Scratchpad" "button"
+ Then I should not see "superuniquename123"
+ And I should not see "Run!"
+ And I should not see "Prefix Answer?"
+
+ Scenario: Set HTML output to true, 'print' a button to output area
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"html_output": true} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "test_code" to "print('')"
+ Then I press "Run!"
+ And I press "Hi"
+
+ Scenario: Define wrapper in UI params and click run, insert both answer and Scratchpad code, NO prefix with answer, click run
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"wrapper_src": "globalextra"} |
+ And I set the field "globalextra" to:
+ """
+ print('Hello Wrapper', end=' ')
+ {{ ANSWER_CODE }}
+ {{ SCRATCHPAD_CODE }}
+ """
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "answer_code" to "print('Hello Answercode', end=' ')"
+ And I set the ace field "test_code" to "print('Hello Scratchpadcode', end=' ')"
+ And I set the field "prefix_ans" to ""
+ Then I press "Run!"
+ And I should see "Hello Wrapper Hello Scratchpadcode"
+
+ Scenario: Define wrapper in UI params and click run, insert both answer and Scratchpad code, prefix with answer, click run
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"wrapper_src": "globalextra"} |
+ And I set the field "globalextra" to:
+ """
+ print('Hello Wrapper', end=' ')
+ {{ ANSWER_CODE }}
+ {{ SCRATCHPAD_CODE }}
+ """
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "answer_code" to "print('Hello Answercode', end=' ')"
+ And I set the ace field "test_code" to "print('Hello Scratchpadcode', end=' ')"
+ Then I press "Run!"
+ And I should see "Hello Wrapper Hello Answercode Hello Scratchpadcode"
+
+ Scenario: Define wrapper in global extra, insert both answer and Scratchpad code, NO prefix with answer, click run
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"wrapper_src": "globalextra"} |
+ And I set the field "globalextra" to:
+ """
+ print('Hello Wrapper', end=' ')
+ {{ ANSWER_CODE }}
+ {{ SCRATCHPAD_CODE }}
+ """
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "answer_code" to "print('Hello Answercode', end=' ')"
+ And I set the ace field "test_code" to "print('Hello Scratchpadcode', end=' ')"
+ And I set the field "prefix_ans" to ""
+ Then I press "Run!"
+ And I should see "Hello Wrapper Hello Scratchpadcode"
+
+ Scenario: Define wrapper in global extra, insert both answer and Scratchpad code, prefix with answer, click run
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"wrapper_src": "globalextra"} |
+ And I set the field "globalextra" to:
+ """
+ print('Hello Wrapper', end=' ')
+ {{ ANSWER_CODE }}
+ {{ SCRATCHPAD_CODE }}
+ """
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "answer_code" to "print('Hello Answercode', end=' ')"
+ And I set the ace field "test_code" to "print('Hello Scratchpadcode', end=' ')"
+ And I set the field "prefix_ans" to "1"
+ Then I press "Run!"
+ And I should see "Hello Wrapper Hello Answercode Hello Scratchpadcode"
From 2836bb9ca8cde8ac6a170d4940f20a7c446af1be Mon Sep 17 00:00:00 2001
From: Michelle Hsieh
Date: Wed, 25 Jan 2023 10:21:16 +1300
Subject: [PATCH 040/188] Added fix for duplicate class names
---
classes/jobesandbox.php | 24 +++++++++++++++---------
lang/en/qtype_coderunner.php | 1 +
2 files changed, 16 insertions(+), 9 deletions(-)
diff --git a/classes/jobesandbox.php b/classes/jobesandbox.php
index 5ab0cd2dc..712582c42 100644
--- a/classes/jobesandbox.php
+++ b/classes/jobesandbox.php
@@ -138,14 +138,6 @@ public function execute($sourcecode, $language, $input, $files=null, $params=nul
$input .= "\n"; // Force newline on the end if necessary.
}
- $filelist = array();
- if ($files !== null) {
- foreach ($files as $filename => $contents) {
- $id = md5($contents);
- $filelist[] = array($id, $filename);
- }
- }
-
if ($language === 'java') {
$mainclass = $this->get_main_class($sourcecode);
if ($mainclass) {
@@ -157,6 +149,20 @@ public function execute($sourcecode, $language, $input, $files=null, $params=nul
$progname = "__tester__.$language";
}
+ $filelist = array();
+ if ($files !== null) {
+ foreach ($files as $filename => $contents) {
+ if ($filename == $progname) {
+ // If Jobe has named the progname the same as filename, throw an error.
+ $badname['error'] = self::JOBE_400_ERROR;
+ $badname['stderr'] = get_string('errorstring-duplicate-name', 'qtype_coderunner');
+ return (object) $badname;
+ }
+ $id = md5($contents);
+ $filelist[] = array($id, $filename);
+ }
+ }
+
$runspec = array(
'language_id' => $language,
'sourcecode' => $sourcecode,
@@ -203,7 +209,7 @@ public function execute($sourcecode, $language, $input, $files=null, $params=nul
$httpcode = $this->submit($postbody);
}
}
-
+
$runresult = array();
$runresult['sandboxinfo'] = array(
'jobeserver' => $this->jobeserver,
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index cd0b8834b..6180ac95b 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -153,6 +153,7 @@
$string['errorstring-ok'] = 'OK';
$string['errorstring-autherror'] = 'Unauthorised to use sandbox';
$string['errorstring-blocked-url'] = 'The URL is blocked. Check the Jobe URL and Moodle\'s HTTP security settings.';
+$string['errorstring-duplicate-name'] = 'Rename class name; this name conflicts with support files for this question.';
$string['errorstring-jobe400'] = 'Error from Jobe sandbox server: ';
$string['errorstring-jobe-failed'] = 'Jobe server request failed. ';
$string['errorstring-overload'] = 'Job could not be run due to server overload. Perhaps try again shortly?';
From 26c4c37c77e017f46a2633c7419b20ba96998a54 Mon Sep 17 00:00:00 2001
From: Michelle Hsieh
Date: Wed, 25 Jan 2023 16:01:34 +1300
Subject: [PATCH 041/188] Somewhat fix for Java progam names identical to
support file names
---
amd/build/ui_scratchpad.min.js | 48 ++++++++++++++++++++++++++++++
amd/build/ui_scratchpad.min.js.map | 1 +
2 files changed, 49 insertions(+)
create mode 100644 amd/build/ui_scratchpad.min.js
create mode 100644 amd/build/ui_scratchpad.min.js.map
diff --git a/amd/build/ui_scratchpad.min.js b/amd/build/ui_scratchpad.min.js
new file mode 100644
index 000000000..d53e27f65
--- /dev/null
+++ b/amd/build/ui_scratchpad.min.js
@@ -0,0 +1,48 @@
+define("qtype_coderunner/ui_scratchpad",["exports","jquery","core/ajax","core/str","core/templates","qtype_coderunner/userinterfacewrapper"],(function(_exports,_jquery,_ajax,_str,_templates,_userinterfacewrapper){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+/**
+ * Implementation of the html_ui user interface plugin. For overall details
+ * of the UI plugin architecture, see userinterfacewrapper.js.
+ *
+ * This plugin replaces the usual textarea answer element with a div
+ * containing the author-supplied HTML. The serialisation of that HTML,
+ * which is what is essentially copied back into the textarea for submissions
+ * as the answer, is a JSON object. The fields of that object are the names
+ * of all author-supplied HTML elements with a class 'coderunner-ui-element';
+ * all such objects are expected to have a 'name' attribute as well. The
+ * associated field values are lists. Each list contains all the values, in
+ * document order, of the results of calling the jquery val() method in turn
+ * on each of the UI elements with that name.
+ * This means that at least input, select and textarea
+ * elements are supported. The author is responsible for checking the
+ * compatibility of other elements with jquery's val() method.
+ *
+ * The HTML to use in the answer area must be provided as the contents of
+ * either the globalextra field or the prototypeextra field in the question
+ * authoring form. The choice of which is set by the html_src UI parameter, which
+ * must be either 'globalextra' or 'prototypeextra'.
+ *
+ * If any fields of the answer html are to be preloaded, these should be specified
+ * in the answer preload with json of the form '{"": "",...}'
+ * where fieldValueList is a list of all the values to be assigned to the fields
+ * with the given name, in document order.
+ *
+ * To accommodate the possibility of dynamic HTML, any leftover preload values,
+ * that is, values that cannot be positioned within the HTML either because
+ * there is no field of the required name or because, in the case of a list,
+ * there are insufficient elements, are assigned to the data['leftovers']
+ * attribute of the outer html div, as a sub-object of the original object.
+ * This outer div can be located as the 'closest' (in a jQuery sense)
+ * div.qtype-coderunner-html-outer-div. The author-supplied HTML must include
+ * JavaScript to make use of the 'leftovers'.
+ *
+ * As a special case of the serialisation, if all values in the serialisation
+ * are either empty strings or a list of empty strings, the serialisation is
+ * itself the empty string.
+ *
+ * @module coderunner/ui_html
+ * @copyright Richard Lobb, 2022, The University of Canterbury
+ * @copyright James Napier, 2022, The University of Canterbury
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Constructor=void 0,_jquery=_interopRequireDefault(_jquery),_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates);const escapeHtml=text=>{const map={"&":"&","<":"<",">":">",'"':""","'":"'"};return text.replace(/[&<>"']/g,(function(m){return map[m]}))},combinedOutput=(response,maxLen)=>{const limit=s=>s.length<=maxLen?s:s.substr(0,maxLen)+"... (truncated)";return response.cmpinfo+limit(response.output)+limit(response.stderr)},invertSerial=current=>"1"==current[0]?[""]:["1"],overwriteValues=(defaults,prescribed)=>{let overwritten={...defaults};if(prescribed)for(const[key,value]of Object.entries(defaults))overwritten[key]=prescribed[key]||value;return overwritten};_exports.Constructor=class{constructor(textAreaId,width,height,uiParams){const DEF_UI_PARAMS={scratchpad_name:"",button_name:"",prefix_name:"",help_text:"",run_lang:uiParams.lang,html_output:!1,disable_scratchpad:!1,wrapper_src:null};this.textArea=document.getElementById(textAreaId),this.textAreaId=textAreaId,this.height=height,this.readOnly=this.textArea.readonly,this.fail=!1,this.lang=uiParams.lang,uiParams.num_rows=this.textArea.readOnly,this.uiParams=overwriteValues(DEF_UI_PARAMS,uiParams),this.runWrapper=null;const wrapperSrc=this.uiParams.wrapper_src;wrapperSrc&&(this.runWrapper="globalextra"===wrapperSrc||"prototypeextra"===wrapperSrc?this.textArea.dataset[wrapperSrc]:null),this.outerDiv=null,this.scratchpadDiv=null,this.reload()}failed(){return this.fail}failMessage(){return this.failString}sync(){if(!this.context)return;const prefixAns=document.getElementById(this.context.prefix_ans.id),showHide=document.getElementById(this.context.show_hide.id);let serialisation={answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:[""]};this.answerTextarea&&(serialisation.answer_code=[this.answerTextarea.value]),this.testTextarea&&(serialisation.test_code=[this.testTextarea.value]),(el=>{const styles=window.getComputedStyle(el);return"none"===styles.display||"hidden"===styles.visibility})(showHide)||(serialisation.show_hide=["1"]),null!=prefixAns&&prefixAns.checked&&(serialisation.prefix_ans=["1"]),serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),Object.values(serialisation).some((val=>1===val.length&&val[0].length>0))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}getElement(){return this.outerDiv}async handleRunButtonClick(ajax,outputDisplayArea){outputDisplayArea=(0,_jquery.default)(outputDisplayArea),this.sync();const htmlOutput=this.uiParams.html_output,maxLen=this.uiParams["max-output-length"]||3e4,preloadString=(0,_jquery.default)(this.textArea).val(),serial=this.serialize(preloadString),params=this.uiParams.params,code=(answerCode=serial.answer_code,testCode=serial.test_code,prefixAns=serial.prefix_ans[0],(template=this.runWrapper)||(template="{{ ANSWER_CODE }}\n{{ SCRATCHPAD_CODE }}"),prefixAns||(answerCode=""),(template=template.replaceAll("{{ ANSWER_CODE }}",answerCode)).replaceAll("{{ SCRATCHPAD_CODE }}",testCode));var answerCode,testCode,prefixAns,template;outputDisplayArea.html(""),htmlOutput&&outputDisplayArea.hide(),outputDisplayArea.next("div.filter-ace-inline-html").remove(),ajax.call([{methodname:"qtype_coderunner_run_in_sandbox",args:{contextid:M.cfg.contextid,sourcecode:code,language:this.uiParams.run_lang,params:JSON.stringify(params)},done:function(responseJson){const response=JSON.parse(responseJson),error=(response=>{const ERROR_RESPONSES=[[1,0,"error_access_denied"],[2,0,"error_unknown_language"],[3,0,"error_access_denied"],[4,0,"error_submission_limit_reached"],[5,0,"error_sandbox_server_overload"],[0,11,""],[0,12,""],[0,13,"error_timeout"],[0,15,""],[0,17,"error_memory_limit"],[0,21,"error_sandbox_server_overload"],[0,30,"error_excessive_output"]];for(const row of ERROR_RESPONSES)if(row[0]==response.error&&(0!=response.error||response.result==row[1]))return row[2];return"error_unknown_runtime"})(response);if(""===error)if(htmlOutput&&15===response.result){outputDisplayArea.next("div.filter-ace-inline-html").remove();const html=(0,_jquery.default)("
"+response.output+"
");outputDisplayArea.after(html)}else{const text=combinedOutput(response,maxLen);outputDisplayArea.show(),""===text.trim()?outputDisplayArea.html('< No output! >'):outputDisplayArea.html(escapeHtml(text))}else{let extra=0==response.error?combinedOutput(response,maxLen):"";"error_unknown_runtime"===error&&(extra+=response.error?"(Sandbox error code "+response.error+")":"(Run result: "+response.result+")"),(async(langStringName,textarea,additionalText)=>{const message=await(0,_str.get_string)(langStringName,"filter_ace_inline");textarea.show(),textarea.html(escapeHtml("*** "+message+" ***\n"+additionalText))})(error,outputDisplayArea,extra)}},fail:function(error){alert(error.message)}}])}updateContext(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,prefix_name:this.uiParams.prefix_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",text:preload.answer_code[0],lang:this.lang,rows:this.uiParams.rows},test_code:{id:this.textAreaId+"_test-code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_output-displayarea"},jquery_escape:function(){return function(text,render){return _jquery.default.escapeSelector(render(text))}}}}serialize(preloadString){let serial;return preloadString&&(serial=JSON.parse(preloadString)),serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),serial}async reload(){const preloadString=this.textArea.value;let preload;try{preload=this.serialize(preloadString)}catch(error){return this.fail=!0,void(this.failString="scratchpad_ui_invalidserialisation")}this.updateContext(preload);try{const{html:html}=await _templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);document.createElement("div").innerHTML=html,document.getElementById(this.textAreaId).nextSibling.innerHTML=html,this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id));const runButton=document.getElementById(this.textAreaId+"_run-btn"),outputDisplayarea=document.getElementById(this.context.output_display.id);runButton&&runButton.addEventListener("click",(()=>this.handleRunButtonClick(_ajax.default,outputDisplayarea)))}catch(e){this.fail=!0,this.failString="UI template failed to load."}document.getElementById(this.textAreaId+"_wrapper").style.resize="none"}resize(){}hasFocus(){var _this$answerCodeUi,_this$testCodeUi;let focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}destroy(){var _this$outerDiv;this.sync(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}}));
+
+//# sourceMappingURL=ui_scratchpad.min.js.map
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js.map b/amd/build/ui_scratchpad.min.js.map
new file mode 100644
index 000000000..9573b2504
--- /dev/null
+++ b/amd/build/ui_scratchpad.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the html_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a div\n * containing the author-supplied HTML. The serialisation of that HTML,\n * which is what is essentially copied back into the textarea for submissions\n * as the answer, is a JSON object. The fields of that object are the names\n * of all author-supplied HTML elements with a class 'coderunner-ui-element';\n * all such objects are expected to have a 'name' attribute as well. The\n * associated field values are lists. Each list contains all the values, in\n * document order, of the results of calling the jquery val() method in turn\n * on each of the UI elements with that name.\n * This means that at least input, select and textarea\n * elements are supported. The author is responsible for checking the\n * compatibility of other elements with jquery's val() method.\n *\n * The HTML to use in the answer area must be provided as the contents of\n * either the globalextra field or the prototypeextra field in the question\n * authoring form. The choice of which is set by the html_src UI parameter, which\n * must be either 'globalextra' or 'prototypeextra'.\n *\n * If any fields of the answer html are to be preloaded, these should be specified\n * in the answer preload with json of the form '{\"\": \"\",...}'\n * where fieldValueList is a list of all the values to be assigned to the fields\n * with the given name, in document order.\n *\n * To accommodate the possibility of dynamic HTML, any leftover preload values,\n * that is, values that cannot be positioned within the HTML either because\n * there is no field of the required name or because, in the case of a list,\n * there are insufficient elements, are assigned to the data['leftovers']\n * attribute of the outer html div, as a sub-object of the original object.\n * This outer div can be located as the 'closest' (in a jQuery sense)\n * div.qtype-coderunner-html-outer-div. The author-supplied HTML must include\n * JavaScript to make use of the 'leftovers'.\n *\n * As a special case of the serialisation, if all values in the serialisation\n * are either empty strings or a list of empty strings, the serialisation is\n * itself the empty string.\n *\n * @module coderunner/ui_html\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport ajax from 'core/ajax';\nimport {get_string as getLangString} from 'core/str';\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\n\n\nconst RESULT_SUCCESS = 15; // Code for a correct Jobe run.\nconst DEFAULT_MAX_OUTPUT_LEN = 30000;\n\n\n/**\n * Escape text special HTML characters.\n * @param {string} text\n * @returns {string} text with various special chars replaced with equivalent\n * html entities. Newlines are replaced with .\n */\nconst escapeHtml = text => {\n const map = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return text.replace(/[&<>\"']/g, function(m) {\n return map[m];\n });\n};\n\n/**\n * Analyse the response for errors. There are two sorts of error: sandbox failures,\n * for which the field response.error is non-zero meaning the run didn't take\n * place at all and failures in the run\n * itself, such as compile errors, timeouts, runtime errors etc. The\n * various codes are documented in the CodeRunner file sandbox.php.\n * Some error returns, notably compilation error and runtime error, are not\n * treated as errors here, since the stdout + stderr should reveal what\n * happened anyway. More obscure errors are lumped together as 'Unknown\n * runtime error'.\n * @param {object} response The response from the web-service sandbox request.\n * @returns string The language string to use for an error message or '' if\n * no error message.\n */\nconst diagnose = response => {\n // Table of error conditions.\n // Each row is response.error, response.result, langstring\n // response.result is ignored if response.error is non-zero.\n // Any condition not in the table is deemed an \"Unknown runtime error\".\n const ERROR_RESPONSES = [\n [1, 0, 'error_access_denied'], // Sandbox AUTH_ERROR\n [2, 0, 'error_unknown_language'], // Sandbox WRONG_LANG_ID\n [3, 0, 'error_access_denied'], // Sandbox ACCESS_DENIED\n [4, 0, 'error_submission_limit_reached'], // Sandbox SUBMISSION_LIMIT_EXCEEDED\n [5, 0, 'error_sandbox_server_overload'], // Sandbox SERVER_OVERLOAD\n [0, 11, ''], // RESULT_COMPILATION_ERROR\n [0, 12, ''], // RESULT_RUNTIME_ERROR\n [0, 13, 'error_timeout'], // RESULT TIME_LIMIT\n [0, RESULT_SUCCESS, ''], // RESULT_SUCCESS\n [0, 17, 'error_memory_limit'], // RESULT_MEMORY_LIMIT\n [0, 21, 'error_sandbox_server_overload'], // RESULT_SERVER_OVERLOAD\n [0, 30, 'error_excessive_output'] // RESULT OUTPUT_LIMIT\n ];\n for (const row of ERROR_RESPONSES) {\n if (row[0] == response.error && (response.error != 0 || response.result == row[1])) {\n return row[2];\n }\n }\n return 'error_unknown_runtime';\n};\n\n/**\n * Get the specified language string using\n * AJAX and plug it into the given textarea\n * @param {string} langStringName The language string name.\n * @param {DOMnode} textarea The textarea into which the error message\n * should be plugged.\n * @param {string} additionalText Extra text to follow the result code.\n */\nconst setLangString = async(langStringName, textarea, additionalText) => {\n const message = await getLangString(langStringName, 'filter_ace_inline');\n textarea.show();\n textarea.html(escapeHtml(\"*** \" + message + \" ***\\n\" + additionalText));\n};\n\n/**\n * Concatenates the cmpinfo, stdout and stderr fields of the sandbox\n * response, truncating both stdout and stderr to a given maximum length\n * if necessary (in which case '... (truncated)' is appended.\n * @param {object} response Sandbox response object\n * @param {int} maxLen The maximum length of the trimmed stringlen.\n */\nconst combinedOutput = (response, maxLen) => {\n const limit = s => s.length <= maxLen ? s : s.substr(0, maxLen) + '... (truncated)';\n return response.cmpinfo + limit(response.output) + limit(response.stderr);\n};\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = current => current[0] == '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text.\n * @param {string} testCode text.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template) => {\n if (!template) {\n template = '{{ ANSWER_CODE }}\\n' +\n '{{ SCRATCHPAD_CODE }}';\n }\n if (!prefixAns) {\n answerCode = '';\n }\n template = template.replaceAll('{{ ANSWER_CODE }}', answerCode);\n template = template.replaceAll('{{ SCRATCHPAD_CODE }}', testCode);\n return template;\n};\n\n/**\n * Returns anew object containg default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the defualt value.\n * Does not add keys/values to the result if that key is not in defualts.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with defualt values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n\n/**\n * Is an element currently hidden?\n * @param {Element} el to check visibility of.\n * @returns {boolean} true if el is visible.\n */\nconst isVisible = (el) => {\n const styles = window.getComputedStyle(el);\n return styles.display === 'none' || styles.visibility === 'hidden';\n};\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n html_output: false,\n disable_scratchpad: false,\n wrapper_src: null\n };\n\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n\n this.lang = uiParams.lang;\n\n uiParams.num_rows = this.textArea.readOnly;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n\n // Find the run wrapper source location.\n this.runWrapper = null;\n const wrapperSrc = this.uiParams.wrapper_src;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n this.runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n // TODO: raise some sort of exception? Invalid, params.\n // Bad wrapper src provided by user...\n this.runWrapper = null;\n }\n }\n\n this.outerDiv = null; // TODO: Investigate...\n this.scratchpadDiv = null;\n this.reload(); // Draw my beautiful blobs.\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n\n let serialisation = {\n answer_code: [''],\n test_code: [''],\n show_hide: [''],\n prefix_ans: ['']\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (!isVisible(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked) {\n serialisation.prefix_ans = ['1'];\n }\n\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n async handleRunButtonClick(ajax, outputDisplayArea) {\n outputDisplayArea = $(outputDisplayArea);\n this.sync(); // Use up-to-date serialization.\n\n const htmlOutput = this.uiParams.html_output;\n const maxLen = this.uiParams['max-output-length'] || DEFAULT_MAX_OUTPUT_LEN;\n const preloadString = $(this.textArea).val();\n const serial = this.serialize(preloadString);\n const params = this.uiParams.params;\n const code = fillWrapper(\n serial.answer_code,\n serial.test_code,\n serial.prefix_ans[0],\n this.runWrapper\n );\n\n // Clear all output areas.\n outputDisplayArea.html('');\n if (htmlOutput) {\n outputDisplayArea.hide();\n }\n outputDisplayArea.next('div.filter-ace-inline-html').remove(); // TODO: Naming\n\n\n ajax.call([{\n methodname: 'qtype_coderunner_run_in_sandbox',\n args: {\n contextid: M.cfg.contextid, // Moodle context ID\n sourcecode: code,\n language: this.uiParams.run_lang,\n params: JSON.stringify(params) // Sandbox params\n },\n done: function(responseJson) {\n const response = JSON.parse(responseJson);\n const error = diagnose(response);\n if (error === '') {\n // If no errors or compilation error or runtime error\n if (!htmlOutput || response.result !== RESULT_SUCCESS) {\n // Either it's not HTML output or it is but we have compilation or runtime errors.\n const text = combinedOutput(response, maxLen);\n outputDisplayArea.show();\n if (text.trim() === '') {\n outputDisplayArea.html('< No output! >');\n } else {\n outputDisplayArea.html(escapeHtml(text));\n }\n } else { // Valid HTML output - just plug in the raw html to the DOM.\n // Repeat the deletion of previous output in case of multiple button clicks.\n outputDisplayArea.next('div.filter-ace-inline-html').remove();\n\n const html = $(\"
\" +\n response.output + \"
\");\n outputDisplayArea.after(html);\n }\n } else {\n // If an error occurs, display the language string in the\n // outputDisplayArea plus additional info.\n let extra = response.error == 0 ? combinedOutput(response, maxLen) : '';\n if (error === 'error_unknown_runtime') {\n extra += response.error ? '(Sandbox error code ' + response.error + ')' :\n '(Run result: ' + response.result + ')';\n }\n setLangString(error, outputDisplayArea, extra);\n }\n },\n fail: function(error) {\n alert(error.message);\n }\n }]);\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"prefix_name\": this.uiParams.prefix_name,\n \"help_text\": {\"text\": this.uiParams.help_text}, // TODO: context doesnt match...\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.uiParams.rows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_output-displayarea'\n },\n \"jquery_escape\": function() {\n return function(text, render) {\n return $.escapeSelector(render(text));\n };\n }\n };\n }\n\n serialize(preloadString) {\n const defaultSerial = {\n answer_code: [''],\n test_code: [''],\n show_hide: [''],\n prefix_ans: ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString) {\n serial = JSON.parse(preloadString);\n }\n serial = overwriteValues(defaultSerial, serial);\n return serial;\n }\n\n async reload() {\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.serialize(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n\n this.updateContext(preload);\n\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n\n const div = document.createElement('div');\n div.innerHTML = html;\n document.getElementById(this.textAreaId)\n .nextSibling\n .innerHTML = html;\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n const outputDisplayarea = document.getElementById(this.context.output_display.id);\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick(ajax, outputDisplayarea));\n }\n } catch (e) {\n this.fail = true;\n this.failString = \"UI template failed to load.\"; // TODO: Lang-string goes here.\n }\n\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n document.getElementById(this.textAreaId + '_wrapper').style.resize = 'none';\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["escapeHtml","text","map","replace","m","combinedOutput","response","maxLen","limit","s","length","substr","cmpinfo","output","stderr","invertSerial","current","overwriteValues","defaults","prescribed","overwritten","key","value","Object","entries","constructor","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","run_lang","lang","html_output","disable_scratchpad","wrapper_src","textArea","document","getElementById","readOnly","this","readonly","fail","num_rows","runWrapper","wrapperSrc","dataset","outerDiv","scratchpadDiv","reload","failed","failMessage","failString","sync","context","prefixAns","prefix_ans","id","showHide","show_hide","serialisation","answer_code","test_code","answerTextarea","testTextarea","el","styles","window","getComputedStyle","display","visibility","isVisible","checked","values","some","val","JSON","stringify","getElement","ajax","outputDisplayArea","htmlOutput","preloadString","serial","serialize","params","code","answerCode","testCode","template","replaceAll","html","hide","next","remove","call","methodname","args","contextid","M","cfg","sourcecode","language","done","responseJson","parse","error","ERROR_RESPONSES","row","result","diagnose","after","show","trim","extra","async","langStringName","textarea","additionalText","message","setLangString","alert","updateContext","preload","rows","render","$","escapeSelector","Templates","renderForPromise","createElement","innerHTML","nextSibling","answerCodeUi","testCodeUi","runButton","outputDisplayarea","output_display","addEventListener","handleRunButtonClick","e","style","resize","hasFocus","focused","_this$answerCodeUi","uiInstance","_this$testCodeUi","destroy"],"mappings":"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"}
\ No newline at end of file
From 9611ade125e7a53d479d6fe5cb6d7027a41cf0de Mon Sep 17 00:00:00 2001
From: Michelle Hsieh
Date: Thu, 26 Jan 2023 20:01:30 +1300
Subject: [PATCH 042/188] Fixed the UI display bug
---
amd/build/authorform.min.js | 2 +-
amd/build/authorform.min.js.map | 2 +-
amd/src/authorform.js | 16 +++++++++++-----
3 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/amd/build/authorform.min.js b/amd/build/authorform.min.js
index 9ef278644..20d22f825 100644
--- a/amd/build/authorform.min.js
+++ b/amd/build/authorform.min.js
@@ -5,6 +5,6 @@
* @copyright Richard Lobb, 2015, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/authorform",["jquery","qtype_coderunner/userinterfacewrapper","core/str"],(function($,ui,str){let currentQtype="";var JSON_TO_FORM_MAP={template:["#id_template","value",""],iscombinatortemplate:["#id_iscombinatortemplate","checked","",function(value){return"1"===value}],cputimelimitsecs:["#id_cputimelimitsecs","value",""],memlimitmb:["#id_memlimitmb","value",""],sandbox:["#id_sandbox","value","DEFAULT"],sandboxparams:["#id_sandboxparams","value",""],testsplitterre:["#id_testsplitterre","value","",function(splitter){return splitter.replace("\n","\\n")}],allowmultiplestdins:["#id_allowmultiplestdins","checked","",function(value){return"1"===value}],grader:["#id_grader","value","EqualityGrader"],resultcolumns:["#id_resultcolumns","value",""],language:["#id_language","value",""],acelang:["#id_acelang","value",""],uiplugin:["#id_uiplugin","value","ace"]};return{initEditForm:function(){var typeCombo=$("#id_coderunnertype"),prototypeDisplay=$("#id_isprototype"),template=$("#id_template"),evaluatePerStudent=$("#id_templateparamsevalpertry"),globalextra=$("#id_globalextra"),prototypeextra=$("#id_prototypeextra"),useace=$("#id_useace"),language=$("#id_language"),acelang=$("#id_acelang"),customise=$("#id_customise"),isCombinator=$("#id_iscombinatortemplate"),testSplitterRe=$("#id_testsplitterre"),allowMultipleStdins=$("#id_allowmultiplestdins"),customisationFieldSet=$("#id_customisationheader"),advancedCustomisation=$("#id_advancedcustomisationheader"),isCustomised=customise.prop("checked"),prototypeType=$("#id_prototypetype"),preloadHdr=$("#id_answerpreloadhdr"),courseId=$('input[name="courseid"]').prop("value"),questiontypeHelpDiv=$("#qtype-help"),precheck=$("select#id_precheck"),testtypedivs=$("div.testtype"),brokenQuestion=$("#id_broken_question"),badQuestionLoad=$("#id_bad_question_load"),uiplugin=$("#id_uiplugin"),uiparameters=$("#id_uiparameters");function setUi(taId,uiname){var lang,uiWrapper,ta=$(document.getElementById(taId)),currentLang=ta.attr("data-lang"),paramsJson=ta.attr("data-params"),params={};ta.attr("data-prototypeextra",prototypeextra.val()),ta.attr("data-globalextra",globalextra.val()),ta.attr("data-test0",$("#id_testcode_0").val());try{params=JSON.parse(paramsJson)}catch(err){}"none"===(uiname=uiname.toLowerCase())&&(uiname=""),"id_templateparams"==taId||"id_uiparameters"==taId?lang="":(lang=language.prop("value"),"id_template"!==taId&&acelang.prop("value")&&(lang=function(acelang){var langs,i;if(acelang.indexOf(",")<0)return acelang;for(langs=acelang.split(","),i=0;i0?langs[0]:""}(acelang.prop("value")))),(uiWrapper=ta.data("current-ui-wrapper"))&&uiWrapper.uiname===uiname&¤tLang==lang||(ta.attr("data-lang",lang),uiWrapper?(params.lang=lang,uiWrapper.loadUi(uiname,params)):uiWrapper=new ui.InterfaceWrapper(uiname,taId))}function setUis(){var uiname=uiplugin.val(),enableUi=!0;if("html"===uiname&&""!==uiparameters.val().trim())try{!1===JSON.parse(uiparameters.val()).enable_in_editor&&(enableUi=!1)}catch(error){alert("Invalid UI parameters.")}enableUi&&(setUi("id_answer",uiname),setUi("id_answerpreload",uiname))}function setCustomisationVisibility(isVisible){var display=isVisible?"block":"none";customisationFieldSet.css("display",display),advancedCustomisation.css("display",display),isVisible&&useace.prop("checked")&&setUi("id_template","ace")}function copyFieldsFromQuestionType(newType,response){var formspecifier,attrval,isCombinatorEnabled;for(var key in function(stateOn){var uiWrapper,taIds=["id_template","id_uiparameters"];if(useace.prop("checked"))for(var i=0;i3&&(attrval=(0,formspecifier[3])(attrval)),$(formspecifier[0]).prop(formspecifier[1],attrval);customise.prop("checked",!1),str.get_string("coderunner_question_type","qtype_coderunner").then((function(s){var title,coderunner_descr,html,resultHtml;questiontypeHelpDiv.html((title=newType,coderunner_descr=s,html=response.questiontext,resultHtml='
")}))):loadCustomisationFields(),set_testtype_visibilities(),useace.prop("checked")&&(setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")),loadUiParametersDescription(),customise.on("change",(function(){customise.prop("checked")?setCustomisationVisibility(!0):str.get_string("confirm_proceed","qtype_coderunner").then((function(s){window.confirm(s)?setCustomisationVisibility(!1):customise.prop("checked",!0)}))})),acelang.on("change",check_ace_lang),language.on("change",(function(){useace.prop("checked")&&setUi("id_template","ace"),check_ace_lang()})),typeCombo.on("change",(function(){customise.prop("checked")?str.get_string("question_type_changed","qtype_coderunner").then((function(s){window.confirm(s)&&loadCustomisationFields()})):loadCustomisationFields()})),useace.on("change",(function(){useace.prop("checked")?(setUi("id_template","ace"),setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")):(setUi("id_template",""),setUi("id_templateparams",""),setUi("id_uiparameters",""))})),evaluatePerStudent.on("change",(function(){evaluatePerStudent.is(":checked")&&langStringAlert("templateparamsusingsandbox")})),uiplugin.on("change",(function(){setUis(),loadUiParametersDescription()})),precheck.on("change",set_testtype_visibilities),prototypeType.on("change",(function(){"0"==prototypeType.prop("value")?prototypeDisplay.attr("hidden","1"):prototypeDisplay.removeAttr("hidden")})),new MutationObserver((function(){setUis()})).observe(preloadHdr.get(0),{attributes:!0}),$("button.replaceexpectedwithgot").click((function(){var gotPre=$(this).prev('pre[id^="id_got_"]'),testCaseId=gotPre.attr("id").replace("id_got_","");$("#id_expected_"+testCaseId).val(gotPre.text()),$("#id_fail_expected_"+testCaseId).html(gotPre.text()),$(".failrow_"+testCaseId).addClass("fixed"),$(this).prop("disabled",!0)})),$(".btn-primary").click((function(){typeCombo.prop("disabled",!1)}))}}}));
//# sourceMappingURL=authorform.min.js.map
\ No newline at end of file
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
index a44df6300..02b9de9e9 100644
--- a/amd/build/authorform.min.js.map
+++ b/amd/build/authorform.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n */\n function setUis() {\n var uiname = uiplugin.val();\n var enableUi = true;\n if (uiname === 'html' && uiparameters.val().trim() !== '') {\n try {\n var uiparams = JSON.parse(uiparameters.val());\n if (uiparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","enableUi","trim","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * The sample answer and answer preload have the data-params attribute which contains\n * the UI params in a JSON from the current question merged with the prototype.\n * Both of them are identical and are changed simultaneously; only checking\n * answer as state is identical.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n *\n */\n function setUis() {\n let uiname = uiplugin.val();\n let answer = $('#id_answer');\n let enableUi = true;\n if (uiname === 'html' && answer.attr('data-params') !== '') {\n try {\n let answerparams = JSON.parse(answer.attr('data-params'));\n if (answerparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","answer","enableUi","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","trim","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/authorform.js b/amd/src/authorform.js
index a64a48c04..66f7df7bf 100644
--- a/amd/src/authorform.js
+++ b/amd/src/authorform.js
@@ -151,17 +151,23 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
/**
* Set the correct Ui controller on both the sample answer and the answer preload.
+ * The sample answer and answer preload have the data-params attribute which contains
+ * the UI params in a JSON from the current question merged with the prototype.
+ * Both of them are identical and are changed simultaneously; only checking
+ * answer as state is identical.
* As a special case, we don't turn on the Ui controller in the answer
* and answer preload fields when using Html-Ui and the ui-parameter
* enable_in_editor is false.
+ *
*/
function setUis() {
- var uiname = uiplugin.val();
- var enableUi = true;
- if (uiname === 'html' && uiparameters.val().trim() !== '') {
+ let uiname = uiplugin.val();
+ let answer = $('#id_answer');
+ let enableUi = true;
+ if (uiname === 'html' && answer.attr('data-params') !== '') {
try {
- var uiparams = JSON.parse(uiparameters.val());
- if (uiparams.enable_in_editor === false) {
+ let answerparams = JSON.parse(answer.attr('data-params'));
+ if (answerparams.enable_in_editor === false) {
enableUi = false;
}
} catch (error) {
From 7680a44fd1662dcb19165fa07eccf47abd981267 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Fri, 27 Jan 2023 13:23:24 +1300
Subject: [PATCH 043/188] Tweaks to commenting.
---
question.php | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/question.php b/question.php
index 7912be98e..f9041dd08 100644
--- a/question.php
+++ b/question.php
@@ -324,9 +324,15 @@ public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
return new qbehaviour_adaptive_adapted_for_coderunner($qa, $preferredbehaviour);
}
+ /**
+ * What data may be included in the form submission when a student submits
+ * this question in its current state?
+ *
+ * @return array|string variable name => PARAM_... constant
+ */
public function get_expected_data() {
$expecteddata = array('answer' => PARAM_RAW,
- 'language' => PARAM_NOTAGS);
+ 'language' => PARAM_NOTAGS); // NOTAGS => any HTML is stripped.
if ($this->attachments != 0) {
$expecteddata['attachments'] = question_attempt::PARAM_FILES;
}
From 485ca1eaff37328af966cde2af0c3942937c3a17 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Sat, 28 Jan 2023 11:32:07 +1300
Subject: [PATCH 044/188] Add Michelle's hidden error message.
---
edit_coderunner_form.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/edit_coderunner_form.php b/edit_coderunner_form.php
index 754c12794..5d2bbf28d 100644
--- a/edit_coderunner_form.php
+++ b/edit_coderunner_form.php
@@ -496,7 +496,7 @@ private function make_questiontype_panel($mform) {
$hidemethod = method_exists($mform, 'hideIf') ? 'hideIf' : 'disabledIf';
$mform->addElement('header', 'questiontypeheader', get_string('type_header', 'qtype_coderunner'));
-
+
// Insert the (possible) bad question load message as a hidden field before broken question. JavaScript
// will be used to show it if non-empty.
$mform->addElement('hidden', 'badquestionload', '',
From aa32a8fe309f62c64337170982990d994906bf52 Mon Sep 17 00:00:00 2001
From: James Napier <61300251+jimbonothing64@users.noreply.github.com>
Date: Sat, 28 Jan 2023 11:35:20 +1300
Subject: [PATCH 045/188] Scratchpad UI (#162)
* init
* init
* final init...
* UI to serialization working
* serialization to UI working (both ways)
* code grunted
* before altering serialization
* fixed bug where garbled serialization caused crash
* change serialisation to use object
* html bug in reload method fixed
* object serialisation for both text areas
* added checkbox (very big!)
* fixed checkbox and button sizes
* fixed prefix-ans checkbox not remembering state
* added show/hide scratchpad serialization
* show/hide tickbox now shows/hides sp
* make grunt happy
* sp is now hidden when serialization tells it to be
* grunt
* run button runs hello world program
* run button runs serialized field
* run button now runs code as it should
* spacing fixed
* Added error messaging and trunction for run
* fixed language choice for run
* refactored UI html code
* fixed bug were new scratchpad not serializing
* fixed scratchpad show/hide
* fixed scratchpad show/hide (again)
* refactor function out of object
* add ui param for 'run' button
* added scratchpad name ui param
* sp html display now works
* added new params
* changed sb_ prefix to sp_ for ui params
* added ui param for changing prefix with ans text
* remove stray sb_ prefixes
* added run specific language param
* refactored combine code to be function not method
* added ui_params for wrapper
* trial version of wrapper/template
* added global template/wrapper option functionality
* wrapper functionality working
* Cleaned up comments
* fixed sandbox params
* bug fix: sp display area now clears in text and html mode
* bug fix: globalextra
* bug fix: \n bug in answer code leading to unterminated strings etc.
* change names in wrapper
extract \n bugfix to wrapper
* added ace editor to top
* added ace editor to scratchpad (serial to reload broken)
* Fixed serialization with ace
* Ace added using two UI wrappers, each containing an ACE UI
* Refactored reload method
* Refactored sub-UI creation
* Added syntax highlighting for ace
* bug fix: undefined ID for sub-textareas
* bug fix: fixed size and resizing of answer-box
* bug fix: sizing, weird flashing while loading
* bug fix: remove list from serialization
* grunted
* Serialization when empty now empty string
* remove get fields method
* removed blob names, replaced with scratchpad
* renamed blob ui files to scratchpad ui files
* changed dual blob filenames to scratchpad filenames
* implement hasFocus method
* add configure sandbox method
* fixed a tag to be html compliant
* added scratchpad ui acceptance tests
* added scratchpad ui acceptance tests for using run and serialization
* added scratchpad ui acceptance tests for using run and serialization
* acceptance tests for using run
* bug fix: serialization empty unless scratchpad shown
* bug fix: sandbox not init for test properly
* Added feedback when run program provides no output
* fixed validate on save breaking tests
* added serialisation tests
* fixed serialisation tests
* I see in answer box added (no pystring)
* I see in answer box added pystring
* Changed name of test to match others better
* Added wrapper tests
* Fixed serialisation tests
* Cleaned up scenario descriptions
* added I set ace field function
* fixed tests to work with Ace editors
* bug fix: ace now allowed for Scratchpad
* bug fix: test with wrong deffinition
* cleaned up comments
* bug fix: clicking label checks checkbox now
* invert prefix ans default state and serialization
* prefix ans functionality now matches inverted serialisation
* match tests with new defualt prefix on state of SP
* cleaned up serialization inversion
* uninvert serialisation
* uninvert serialisation
* uninvert serialisation
* updated default prefix text
* added help button
* simplified ui param names
* added scratchpad ui strings
* added language string support
* added language strings for sp
* changed test's UI param names to match new names
* cleaned up spacing, semicolons
* removed sp_ prefixes
* added langauge string support for buttons, help text ect
* spacing
* fixed wrong direction of scratchpad arrow
* clean up help popover
* switch show/hide to use bootstrap collapse
* added help_text UI param for setting help text
* modified wrapper_src to alow prototypeextra field use, disallowed wrapper entry
* fixed slow loading ace bug
* added spacing between precheck checkbox and label
* bug fix: scratchpad button oppening ALL scratchpads
* changed serialization to use lists
* fixed tab highlight being too big for space on scratchpad bar
* bug fix: scratchpad name param and lang string replacement
* bug fix: propper listy serialization for answer_code and test_code
* update tests to reflect changes to design
* update tests to test serialization missing fields
* ui can now handle serialization missing fields
* bug fix: ace not reading lang correctly
* added disable scratchpad ui param
* Eslint
* gurkin lint
* make use of mustache templates, jquerry removal
* re-add functionality to overwrite langauge strings with ui params
* switch to class notation
* es6ify
* es6
* commit before pull
* fix show/hide serialization
* update json parsing rules
* fixed UI params, show/hide arrow pointing wrong way
* fix test not waiting long enough
* added invert_prefix ui param, lang strings for ui params
* eslint
* added new tests for UI params, serialisation and demo readme
* bug fix: destroy ui method, add new tests, lint
* touched up readme, refactored code
* Update readme
* Add display area class, HTML, JSON and Text output support
---
ReadmeScratchpadUi.md | 137 +++++++
amd/build/ajaxquestionloader.min.js.map | 1 -
amd/build/authorform.min.js.map | 1 -
amd/build/graphelements.min.js.map | 1 -
amd/build/graphutil.min.js.map | 1 -
amd/build/multilanguagequestion.min.js.map | 1 -
amd/build/outputdisplayarea.min.js | 8 +
amd/build/resetbutton.min.js.map | 1 -
amd/build/showdiff.min.js.map | 1 -
amd/build/textareas.min.js.map | 1 -
amd/build/ui_ace.min.js.map | 1 -
amd/build/ui_ace_gapfiller.min.js.map | 1 -
amd/build/ui_gapfiller.min.js.map | 1 -
amd/build/ui_graph.min.js.map | 1 -
amd/build/ui_html.min.js.map | 1 -
amd/build/ui_scratchpad.min.js | 54 +++
amd/build/ui_table.min.js.map | 1 -
amd/build/userinterfacewrapper.min.js.map | 1 -
amd/src/outputdisplayarea.js | 203 ++++++++++
amd/src/ui_scratchpad.js | 407 ++++++++-------------
amd/src/ui_scratchpad.json | 4 +
lang/en/qtype_coderunner.php | 17 +-
samples/input_mpl_wrapper.py | 117 ++++++
templates/answer_textarea.mustache | 2 +-
templates/output_displayarea.mustache | 13 +-
templates/scratchpad.mustache | 31 +-
templates/scratchpad_controls.mustache | 9 +-
templates/scratchpad_ui.mustache | 4 +-
tests/behat/scratchpad_ui.feature | 99 ++++-
tests/behat/scratchpad_ui_params.feature | 189 +++++++++-
30 files changed, 983 insertions(+), 326 deletions(-)
create mode 100644 ReadmeScratchpadUi.md
delete mode 100644 amd/build/ajaxquestionloader.min.js.map
delete mode 100644 amd/build/authorform.min.js.map
delete mode 100644 amd/build/graphelements.min.js.map
delete mode 100644 amd/build/graphutil.min.js.map
delete mode 100644 amd/build/multilanguagequestion.min.js.map
create mode 100644 amd/build/outputdisplayarea.min.js
delete mode 100644 amd/build/resetbutton.min.js.map
delete mode 100644 amd/build/showdiff.min.js.map
delete mode 100644 amd/build/textareas.min.js.map
delete mode 100644 amd/build/ui_ace.min.js.map
delete mode 100644 amd/build/ui_ace_gapfiller.min.js.map
delete mode 100644 amd/build/ui_gapfiller.min.js.map
delete mode 100644 amd/build/ui_graph.min.js.map
delete mode 100644 amd/build/ui_html.min.js.map
create mode 100644 amd/build/ui_scratchpad.min.js
delete mode 100644 amd/build/ui_table.min.js.map
delete mode 100644 amd/build/userinterfacewrapper.min.js.map
create mode 100644 amd/src/outputdisplayarea.js
create mode 100644 samples/input_mpl_wrapper.py
diff --git a/ReadmeScratchpadUi.md b/ReadmeScratchpadUi.md
new file mode 100644
index 000000000..3815cef33
--- /dev/null
+++ b/ReadmeScratchpadUi.md
@@ -0,0 +1,137 @@
+## Scratchpad UI
+The **Scratchpad UI** is an extension of the **Ace UI**:
+- The Scratchpad UI is designed to allow the execution of code in the CodeRunner question in a manner similar to an IDE.
+- The Scratchpad UI contains two editor boxes, one on top of another, allowing users to enter and edit code in both.
+
+By default, only the top editor is visible and the **Scratchpad Area**, which contains the bottom editor, is hidden.
+Clicking the **Scratchpad Button** shows the **Scratchpad Area**.
+This includes a second editor, a **Run button** and a **Prefix with Answer** checkbox and an **Output Display Area**.
+Additionally, there is a **Help Button** that provides information about how to use the Scratchpad.
+
+It's possible to run code 'in-browser' by clicking the **Run Button**, _without_ making a submission via the **Check Button**:
+- If **Prefix with Answer** is not checked, only the code in the **Scratchpad Editor** is run -- allowing for a rough working spot to quickly check the result of code.
+- Otherwise, when **Prefix with Answer** is checked, the code in the **Scratchpad Editor** is appended to the code in the first editor before being run.
+
+The Run Button has some limitations when using its default configuration:
+- Does not support programs that use STDIN (by default);
+- Only supports textual STDOUT (by default).
+
+Note: *These features can be supported, see wrappers...*
+
+### Serialisation
+The UI state serialises to JSON, with fields:
+- `answer_code`: `[""]` A list containing a string with answer code from the first editor;
+- `test_code`: `[""]` A list containing a string with containing answer code from the second editor;
+- `show_hide`: `["1"]` when scratchpad is visible, otherwise `[""]`;
+- `prefix_ans`: `["1"]` when **Prefix with Answer** is checked, otherwise `[""]`.
+
+A special case is the default serialisation: `{"answer_code":[""],"test_code":[""],"show_hide":["1"],"prefix_ans":["1"]}` is converted to `""` (an empty string).
+
+The UI will also accept (and convert) JSON with only the field `answer_code` and strings to a valid serialisation.
+A valid serialisation is one with all four specified fields. All other serialisations will be rejected by the interface.
+
+### UI Parameters
+
+- `scratchpad_name`: display name of the scratchpad, used to hide/un-hide the scratchpad.
+- `button_name`: run button text.
+- `prefix_name`: prefix with answer check-box label text.
+- `help_text`: help text to show.
+- `run_lang`: language used to run code when the run button is clicked, this should be the language your wrapper is written in (if applicable).
+- `wrapper_src`: location of wrapper code to be used by the run button, if applicable:
+ - setting to `globalextra` will use text in global extra field,
+ - `prototypeextra` will use the prototype extra field.
+- `html_output`: when true, the output from run will be displayed as raw HTML instead of text.
+- `disable_scratchpad`: disable the scratchpad, effectively revert back to Ace UI from student perspective.
+- `invert_prefix`: inverts meaning of prefix_ans serialisation -- `'1'` means un-ticked, vice versa. This can be used to swap the default state.
+- `params` : **THESE ARE NOT WELL DOCUMENTED**
+
+
+### Advanced Customization: Wrappers
+A wrapper can be used to wrap code before it is run using the sandbox.
+Some tasks that require this include: running languages installed on Jobe but not supported by coderunner; reading standard input during runs; or displaying Matplotlib graphs.
+
+
+You can insert the answer code and scratchpad code into the wrapper using `{{ ANSWER_CODE }}` and `{{ SCRATCHPAD_CODE }}` respectively.
+If the **Prefix with Answer** checkbox is unchecked `{{ ANSWER_CODE }}` will be replaced with an empty string `''`.
+The default configuration uses the following wrapper:
+
+
+```
+{{ ANSWER_CODE }}
+{{ SCRATCHPAD_CODE }}
+```
+
+Wrappers can be in a different language to their question; you can set the Run language, using `run_lang`.
+This changes the language the sandbox service uses to run the wrapper.
+This would be invisible to the student answering the question.
+Below is an example of a C program being wrapped using Python (see the multi-language question for further inspiration):
+ ```
+ import subprocess
+
+student_answer = """{{ ANSWER_CODE }}"""
+test_code = """{{ SCRATCHPAD_CODE }}"""
+all_code = student_answer + '\n' + test_code
+ filename = '__tester__.c
+ with open(filename, "w") as src:
+ print(all_code, file=src)
+
+cflags = "-std=c99 -Wall -Werror"
+return_code = subprocess.call("gcc {0} -o __tester__ __tester__.c".format(cflags).split())
+if return_code != 0:
+ raise Exception("** Compilation failed. Testing aborted **")
+exec_command = ["./__tester__"]
+
+ output = subprocess.check_output(exec_command, universal_newlines=True)
+print(output)
+ ```
+To expand: it is possible to wrap the scratchpad code inside a main function or use a modified wrapper to run unsupported code.
+
+The `html_output` parameter, in conjunction with a wrapper, can be used to display graphical/non-textual output in the output display area. Using HTML output, it is possible to insert images and input boxes.
+
+Example of a wrapper to display `Matplotlib` graphs in the output display area:
+```
+import subprocess, base64, html, os, tempfile
+
+
+def make_data_uri(filename):
+ with open(filename, "br") as fin:
+ contents = fin.read()
+ contents_b64 = base64.b64encode(contents).decode("utf8")
+ return "data:image/png;base64,{}".format(contents_b64)
+
+
+code = r"""{{ ANSWER_CODE }}
+{{ SCRATCHPAD_CODE }}
+"""
+
+prefix = """import os, tempfile
+os.environ["MPLCONFIGDIR"] = tempfile.mkdtemp()
+import matplotlib as _mpl
+_mpl.use("Agg")
+"""
+
+suffix = """
+figs = _mpl.pyplot.get_fignums()
+for i, fig in enumerate(figs):
+ _mpl.pyplot.figure(fig)
+ filename = f'image{i}.png'
+ _mpl.pyplot.savefig(filename, bbox_inches='tight')
+"""
+
+prog_to_exec = prefix + code + suffix
+
+with open('prog.py', 'w') as outfile:
+ outfile.write(prog_to_exec)
+
+result = subprocess.run(['python3', 'prog.py'], capture_output=True, text=True)
+print('
')
+
+for fname in os.listdir():
+ if fname.endswith('png'):
+ print(f'')
+```
+Note: `html_output` *must* be set to`true` for the image to be displayed.
\ No newline at end of file
diff --git a/amd/build/ajaxquestionloader.min.js.map b/amd/build/ajaxquestionloader.min.js.map
deleted file mode 100644
index a39d8300d..000000000
--- a/amd/build/ajaxquestionloader.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"ajaxquestionloader.min.js","sources":["../src/ajaxquestionloader.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 * JavaScript for filling in the question text with the contents of one\n * of the question's support files. Intended primarily for program contest\n * problems, where the support file is a single domjudge or ICPC problem zip,\n * with the problem spec within it.\n *\n * @module qtype_coderunner/ajaxquestionloader\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function ($) {\n /**\n * Append to the question text div in the question a data-URL containing\n * the contents of the question specification file (usu. a pdf).\n * @param {int} qid The question ID in the database.\n * @param {string} divId The ID of the question text
element.\n * @param {string} questionFilename The name of the problem spec file within\n * the problem zip file.\n */\n function loadQuestionText(qid, divId, questionFilename) {\n var questionTextDiv = $('#' + divId),\n errorDiv = '
Failed to load problem spec
';\n if (questionTextDiv.length != 1) {\n questionTextDiv.append(errorDiv);\n return;\n }\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/problemspec.php',\n {\n questionid: qid,\n sesskey: M.cfg.sesskey,\n filename: questionFilename\n },\n function (response) {\n if (response.filecontentsb64) {\n\n questionTextDiv.append(\n '
');\n } else {\n questionTextDiv.append(errorDiv);\n }\n\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred.\n questionTextDiv.append(errorDiv);\n });\n }\n\n return {\n loadQuestionText: loadQuestionText\n };\n});\n"],"names":["define","$","loadQuestionText","qid","divId","questionFilename","questionTextDiv","errorDiv","length","getJSON","M","cfg","wwwroot","questionid","sesskey","filename","response","filecontentsb64","append","fail"],"mappings":";;;;;;;;;;AA2BAA,6CAAO,CAAC,WAAW,SAAUC,SAuClB,CACHC,0BA/BsBC,IAAKC,MAAOC,sBAC9BC,gBAAkBL,EAAE,IAAMG,OAC1BG,SAAW,2DACe,GAA1BD,gBAAgBE,OAIpBP,EAAEQ,QAAQC,EAAEC,IAAIC,QAAU,4CAClB,CACIC,WAAYV,IACZW,QAASJ,EAAEC,IAAIG,QACfC,SAAUV,mBAEd,SAAUW,UACFA,SAASC,gBAETX,gBAAgBY,OACd,sDACAF,SAASC,gBAAkB,4BAE7BX,gBAAgBY,OAAOX,aAIrCY,MAAK,WAEHb,gBAAgBY,OAAOX,aAtBvBD,gBAAgBY,OAAOX"}
\ No newline at end of file
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
deleted file mode 100644
index a44df6300..000000000
--- a/amd/build/authorform.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n */\n function setUis() {\n var uiname = uiplugin.val();\n var enableUi = true;\n if (uiname === 'html' && uiparameters.val().trim() !== '') {\n try {\n var uiparams = JSON.parse(uiparameters.val());\n if (uiparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","enableUi","trim","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/graphelements.min.js.map b/amd/build/graphelements.min.js.map
deleted file mode 100644
index 8487227c5..000000000
--- a/amd/build/graphelements.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"graphelements.min.js","sources":["../src/graphelements.js"],"sourcesContent":["/******************************************************************************\n *\n * A module for use by ui_graph, defining classes Node, Link, SelfLink,\n * StartLink and TemporaryLink\n *\n * @module qtype_coderunner/graphelements\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// This code is a modified version of Finite State Machine Designer\n// (http://madebyevan.com/fsm/)\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(['qtype_coderunner/graphutil'], function(util) {\n\n /**\n * Define a class Node that represents a node in a graph\n * @param {object} parent The Graph to which this node belongs.\n * @param {int} x The x-coordinate of the node.\n * @param {int} y The y-coordinate of the node.\n *\n */\n function Node(parent, x, y) {\n this.parent = parent; // The ui_graph instance.\n this.x = x;\n this.y = y;\n this.mouseOffsetX = 0;\n this.mouseOffsetY = 0;\n this.isAcceptState = false;\n this.textBox = new TextBox('', this);\n this.caretPosition = 0;\n }\n\n // At the start of a drag, record our position relative to the mouse.\n Node.prototype.setMouseStart = function(mouseX, mouseY) {\n this.mouseOffsetX = this.x - mouseX;\n this.mouseOffsetY = this.y - mouseY;\n };\n\n Node.prototype.setAnchorPoint = function(x, y) {\n this.x = x + this.mouseOffsetX;\n this.y = y + this.mouseOffsetY;\n };\n\n // Given a new mouse position during a drag, move to the appropriate\n // new position.\n Node.prototype.trackMouse = function(mouseX, mouseY) {\n this.x = this.mouseOffsetX + mouseX;\n this.y = this.mouseOffsetY + mouseY;\n };\n\n Node.prototype.draw = function(c) {\n // Draw the circle.\n c.beginPath();\n c.arc(this.x, this.y, this.parent.nodeRadius(), 0, 2 * Math.PI, false);\n c.stroke();\n\n // Draw the text.\n this.textBox.draw(this.x, this.y, null, this);\n\n // Draw a double circle for an accept state.\n if(this.isAcceptState) {\n c.beginPath();\n c.arc(this.x, this.y, this.parent.nodeRadius() - 6, 0, 2 * Math.PI, false);\n c.stroke();\n }\n };\n\n Node.prototype.closestPointOnCircle = function(x, y) {\n var dx = x - this.x;\n var dy = y - this.y;\n var scale = Math.sqrt(dx * dx + dy * dy);\n return {\n 'x': this.x + dx * this.parent.nodeRadius() / scale,\n 'y': this.y + dy * this.parent.nodeRadius() / scale,\n };\n };\n\n Node.prototype.containsPoint = function(x, y) {\n return (x - this.x) * (x - this.x) + (y - this.y) * (y - this.y) < this.parent.nodeRadius() * this.parent.nodeRadius();\n };\n\n // Method of a Node that, given a list of all links in a graph, returns\n // a list of any nodes that contain a link to this node (excluding StartLinks\n // and SelfLinks).\n Node.prototype.neighbours = function(links) {\n var neighbours = [], link;\n for (var i = 0; i < links.length; i++) {\n link = links[i];\n if (link instanceof Link) { // Exclude SelfLinks and StartLinks.\n if (link.nodeA === this && !neighbours.includes(link.nodeB)) {\n neighbours.push(link.nodeB);\n } else if (link.nodeB === this && !neighbours.includes(link.nodeA)) {\n neighbours.push(link.nodeA);\n }\n }\n }\n return neighbours;\n };\n\n // Method of Node that traverses a graph defined by a given set of links\n // starting at 'this' node and updating the visited list for each new\n // node. Returns the updated visited list, which (for the root call)\n // is a list of all nodes connected to the given start node.\n Node.prototype.traverseGraph = function(links, visited) {\n var neighbours,\n neighbour;\n if (!visited.includes(this)) {\n visited.push(this);\n neighbours = this.neighbours(links);\n for (var i = 0; i < neighbours.length; i++) {\n neighbour = neighbours[i];\n if (!visited.includes(neighbour)) {\n neighbour.traverseGraph(links, visited);\n }\n }\n }\n return visited;\n };\n\n /**\n * Define a class Link that represents a connection between two nodes.\n * @param {object} parent The graph to which this link belongs.\n * @param {object} a The node at one end of the link.\n * @param {object} b The node at the other end of the link.\n *\n */\n function Link(parent, a, b) {\n this.parent = parent; // The parent ui_digraph instance.\n this.nodeA = a;\n this.nodeB = b;\n this.textBox = new TextBox('', this);\n this.lineAngleAdjust = 0; // Value to add to textAngle when link is straight line.\n this.caretPosition = 0;\n\n // Make anchor point relative to the locations of nodeA and nodeB.\n this.parallelPart = 0.5; // Percentage from nodeA to nodeB.\n this.perpendicularPart = 0; // Pixels from line between nodeA and nodeB.\n }\n\n Link.prototype.getAnchorPoint = function() {\n var dx = this.nodeB.x - this.nodeA.x;\n var dy = this.nodeB.y - this.nodeA.y;\n var scale = Math.sqrt(dx * dx + dy * dy);\n return {\n 'x': this.nodeA.x + dx * this.parallelPart - dy * this.perpendicularPart / scale,\n 'y': this.nodeA.y + dy * this.parallelPart + dx * this.perpendicularPart / scale\n };\n };\n\n Link.prototype.setAnchorPoint = function(x, y) {\n var dx = this.nodeB.x - this.nodeA.x;\n var dy = this.nodeB.y - this.nodeA.y;\n var scale = Math.sqrt(dx * dx + dy * dy);\n this.parallelPart = (dx * (x - this.nodeA.x) + dy * (y - this.nodeA.y)) / (scale * scale);\n this.perpendicularPart = (dx * (y - this.nodeA.y) - dy * (x - this.nodeA.x)) / scale;\n // Snap to a straight line.\n if(this.parallelPart > 0 && this.parallelPart < 1 && Math.abs(this.perpendicularPart) < this.parent.SNAP_TO_PADDING) {\n this.lineAngleAdjust = (this.perpendicularPart < 0) * Math.PI;\n this.perpendicularPart = 0;\n }\n };\n\n Link.prototype.getEndPointsAndCircle = function() {\n if(this.perpendicularPart === 0) {\n var midX = (this.nodeA.x + this.nodeB.x) / 2;\n var midY = (this.nodeA.y + this.nodeB.y) / 2;\n var start = this.nodeA.closestPointOnCircle(midX, midY);\n var end = this.nodeB.closestPointOnCircle(midX, midY);\n return {\n 'hasCircle': false,\n 'startX': start.x,\n 'startY': start.y,\n 'endX': end.x,\n 'endY': end.y,\n };\n }\n var anchor = this.getAnchorPoint();\n var circle = util.circleFromThreePoints(this.nodeA.x, this.nodeA.y, this.nodeB.x, this.nodeB.y, anchor.x, anchor.y);\n var isReversed = (this.perpendicularPart > 0);\n var reverseScale = isReversed ? 1 : -1;\n var rRatio = reverseScale * this.parent.nodeRadius() / circle.radius;\n var startAngle = Math.atan2(this.nodeA.y - circle.y, this.nodeA.x - circle.x) - rRatio;\n var endAngle = Math.atan2(this.nodeB.y - circle.y, this.nodeB.x - circle.x) + rRatio;\n var startX = circle.x + circle.radius * Math.cos(startAngle);\n var startY = circle.y + circle.radius * Math.sin(startAngle);\n var endX = circle.x + circle.radius * Math.cos(endAngle);\n var endY = circle.y + circle.radius * Math.sin(endAngle);\n return {\n 'hasCircle': true,\n 'startX': startX,\n 'startY': startY,\n 'endX': endX,\n 'endY': endY,\n 'startAngle': startAngle,\n 'endAngle': endAngle,\n 'circleX': circle.x,\n 'circleY': circle.y,\n 'circleRadius': circle.radius,\n 'reverseScale': reverseScale,\n 'isReversed': isReversed,\n };\n };\n\n Link.prototype.draw = function(c) {\n var linkInfo = this.getEndPointsAndCircle(), textX, textY, textAngle, relDist;\n // Draw arc.\n c.beginPath();\n if(linkInfo.hasCircle) {\n c.arc(linkInfo.circleX,\n linkInfo.circleY,\n linkInfo.circleRadius,\n linkInfo.startAngle,\n linkInfo.endAngle,\n linkInfo.isReversed);\n } else {\n c.moveTo(linkInfo.startX, linkInfo.startY);\n c.lineTo(linkInfo.endX, linkInfo.endY);\n }\n c.stroke();\n // Draw the head of the arrow.\n if(linkInfo.hasCircle) {\n this.parent.arrowIfReqd(c,\n linkInfo.endX,\n linkInfo.endY,\n linkInfo.endAngle - linkInfo.reverseScale * (Math.PI / 2));\n } else {\n this.parent.arrowIfReqd(c,\n linkInfo.endX,\n linkInfo.endY,\n Math.atan2(linkInfo.endY - linkInfo.startY, linkInfo.endX - linkInfo.startX));\n }\n // Draw the text.\n relDist = this.textBox.relDist;\n if(linkInfo.hasCircle) {\n var startAngle = linkInfo.startAngle;\n var endAngle = linkInfo.endAngle;\n if (endAngle < startAngle) {\n endAngle += Math.PI * 2;\n }\n\n textAngle = ((1 - relDist) * startAngle + relDist * endAngle);\n if (linkInfo.isReversed){\n textAngle += (1 - relDist) * (2 * Math.PI); // Reflect text across the line between the link points\n }\n textX = linkInfo.circleX + linkInfo.circleRadius * Math.cos(textAngle);\n textY = linkInfo.circleY + linkInfo.circleRadius * Math.sin(textAngle);\n this.textBox.draw(textX, textY, textAngle, this);\n } else {\n textX = ((1 - relDist) * linkInfo.startX + relDist * linkInfo.endX);\n textY = ((1 - relDist) * linkInfo.startY + relDist * linkInfo.endY);\n textAngle = Math.atan2(linkInfo.endX - linkInfo.startX, linkInfo.startY - linkInfo.endY);\n this.textBox.draw(textX, textY, textAngle + this.lineAngleAdjust, this);\n }\n };\n\n Link.prototype.containsPoint = function(x, y) {\n var linkInfo = this.getEndPointsAndCircle(), dx, dy, distance;\n if(linkInfo.hasCircle) {\n dx = x - linkInfo.circleX;\n dy = y - linkInfo.circleY;\n distance = Math.sqrt(dx * dx + dy * dy) - linkInfo.circleRadius;\n if(Math.abs(distance) < this.parent.HIT_TARGET_PADDING) {\n var angle = Math.atan2(dy, dx);\n var startAngle = linkInfo.startAngle;\n var endAngle = linkInfo.endAngle;\n if(linkInfo.isReversed) {\n var temp = startAngle;\n startAngle = endAngle;\n endAngle = temp;\n }\n if(endAngle < startAngle) {\n endAngle += Math.PI * 2;\n }\n if(angle < startAngle) {\n angle += Math.PI * 2;\n } else if(angle > endAngle) {\n angle -= Math.PI * 2;\n }\n return (angle > startAngle && angle < endAngle);\n }\n } else {\n dx = linkInfo.endX - linkInfo.startX;\n dy = linkInfo.endY - linkInfo.startY;\n var length = Math.sqrt(dx * dx + dy * dy);\n var percent = (dx * (x - linkInfo.startX) + dy * (y - linkInfo.startY)) / (length * length);\n distance = (dx * (y - linkInfo.startY) - dy * (x - linkInfo.startX)) / length;\n return (percent > 0 && percent < 1 && Math.abs(distance) < this.parent.HIT_TARGET_PADDING);\n }\n return false;\n };\n\n /**\n * Define a class SelfLink that represents a connection from a node back\n * to itself.\n * @param {object} parent The graph to which this link belongs.\n * @param {object} node The node the link emerges from and returns to.\n * @param {object} mouse The current position of the mouse that's defining\n * the position of the self-link.\n */\n function SelfLink(parent, node, mouse) {\n this.parent = parent;\n this.node = node;\n this.anchorAngle = 0;\n this.mouseOffsetAngle = 0;\n this.textBox = new TextBox('', this);\n\n if(mouse) {\n this.setAnchorPoint(mouse.x, mouse.y);\n }\n }\n\n SelfLink.prototype.setMouseStart = function(x, y) {\n this.mouseStartX = x;\n this.mouseStartY = y;\n };\n\n SelfLink.prototype.setAnchorPoint = function(x, y) {\n this.anchorAngle = Math.atan2(y - this.node.y, x - this.node.x) + this.mouseOffsetAngle;\n // Snap to 90 degrees.\n var snap = Math.round(this.anchorAngle / (Math.PI / 2)) * (Math.PI / 2);\n if(Math.abs(this.anchorAngle - snap) < 0.1) {\n this.anchorAngle = snap;\n }\n // Keep in the range -pi to pi so our containsPoint() function always works.\n if(this.anchorAngle < -Math.PI) {\n this.anchorAngle += 2 * Math.PI;\n }\n if(this.anchorAngle > Math.PI) {\n this.anchorAngle -= 2 * Math.PI;\n }\n };\n\n SelfLink.prototype.getEndPointsAndCircle = function() {\n var circleX = this.node.x + 1.5 * this.parent.nodeRadius() * Math.cos(this.anchorAngle);\n var circleY = this.node.y + 1.5 * this.parent.nodeRadius() * Math.sin(this.anchorAngle);\n var circleRadius = 0.75 * this.parent.nodeRadius();\n var startAngle = this.anchorAngle - Math.PI * 0.8;\n var endAngle = this.anchorAngle + Math.PI * 0.8;\n var startX = circleX + circleRadius * Math.cos(startAngle);\n var startY = circleY + circleRadius * Math.sin(startAngle);\n var endX = circleX + circleRadius * Math.cos(endAngle);\n var endY = circleY + circleRadius * Math.sin(endAngle);\n return {\n 'hasCircle': true,\n 'startX': startX,\n 'startY': startY,\n 'endX': endX,\n 'endY': endY,\n 'startAngle': startAngle,\n 'endAngle': endAngle,\n 'circleX': circleX,\n 'circleY': circleY,\n 'circleRadius': circleRadius\n };\n };\n\n SelfLink.prototype.draw = function(c) {\n var linkInfo = this.getEndPointsAndCircle();\n // Draw arc.\n c.beginPath();\n c.arc(linkInfo.circleX, linkInfo.circleY, linkInfo.circleRadius, linkInfo.startAngle, linkInfo.endAngle, false);\n c.stroke();\n // Draw the text on the loop.\n var relDist = this.textBox.relDist;\n var textAngle = linkInfo.startAngle * (1 - relDist) + linkInfo.endAngle * relDist;\n var textX = linkInfo.circleX + linkInfo.circleRadius * Math.cos(textAngle);\n var textY = linkInfo.circleY + linkInfo.circleRadius * Math.sin(textAngle);\n this.textBox.draw(textX, textY, textAngle, this);\n // Draw the head of the arrow.\n this.parent.arrowIfReqd(c, linkInfo.endX, linkInfo.endY, linkInfo.endAngle + Math.PI * 0.4);\n };\n\n SelfLink.prototype.containsPoint = function(x, y) {\n var linkInfo = this.getEndPointsAndCircle();\n var dx = x - linkInfo.circleX;\n var dy = y - linkInfo.circleY;\n var distance = Math.sqrt(dx * dx + dy * dy) - linkInfo.circleRadius;\n return (Math.abs(distance) < this.parent.HIT_TARGET_PADDING);\n };\n\n /**\n * Define a class StartLink that represents a start link in a finite\n * state machine. Not useful in general digraphs.\n * @param {object} parent The graph to which this link belongs.\n * @param {node} node The node that the link leads into.\n * @param {object} start The point at the open end of the link.\n */\n function StartLink(parent, node, start) {\n this.parent = parent;\n this.node = node;\n this.deltaX = 0;\n this.deltaY = 0;\n\n if(start) {\n this.setAnchorPoint(start.x, start.y);\n }\n }\n\n StartLink.prototype.setAnchorPoint = function(x, y) {\n this.deltaX = x - this.node.x;\n this.deltaY = y - this.node.y;\n\n if(Math.abs(this.deltaX) < this.parent.SNAP_TO_PADDING) {\n this.deltaX = 0;\n }\n\n if(Math.abs(this.deltaY) < this.parent.SNAP_TO_PADDING) {\n this.deltaY = 0;\n }\n };\n\n StartLink.prototype.getEndPoints = function() {\n var startX = this.node.x + this.deltaX;\n var startY = this.node.y + this.deltaY;\n var end = this.node.closestPointOnCircle(startX, startY);\n return {\n 'startX': startX,\n 'startY': startY,\n 'endX': end.x,\n 'endY': end.y,\n };\n };\n\n StartLink.prototype.draw = function(c) {\n var endPoints = this.getEndPoints();\n\n // Draw the line.\n c.beginPath();\n c.moveTo(endPoints.startX, endPoints.startY);\n c.lineTo(endPoints.endX, endPoints.endY);\n c.stroke();\n\n // Draw the head of the arrow.\n this.parent.arrowIfReqd(c, endPoints.endX, endPoints.endY, Math.atan2(-this.deltaY, -this.deltaX));\n };\n\n StartLink.prototype.containsPoint = function(x, y) {\n var endPoints = this.getEndPoints();\n var dx = endPoints.endX - endPoints.startX;\n var dy = endPoints.endY - endPoints.startY;\n var length = Math.sqrt(dx * dx + dy * dy);\n var percent = (dx * (x - endPoints.startX) + dy * (y - endPoints.startY)) / (length * length);\n var distance = (dx * (y - endPoints.startY) - dy * (x - endPoints.startX)) / length;\n return (percent > 0 && percent < 1 && Math.abs(distance) < this.parent.HIT_TARGET_PADDING);\n };\n\n /**\n * Define a class TemporaryLink that represents a link that's in the\n * process of being created.\n * @param {object} parent The graph to which this link belongs.\n * @param {object} from The node the link starts at.\n * @param {object} to The node the link goes to.\n */\n function TemporaryLink(parent, from, to) {\n this.parent = parent;\n this.from = from;\n this.to = to;\n }\n\n TemporaryLink.prototype.draw = function(c) {\n // Draw the line.\n c.beginPath();\n c.moveTo(this.to.x, this.to.y);\n c.lineTo(this.from.x, this.from.y);\n c.stroke();\n\n // Draw the head of the arrow.\n this.parent.arrowIfReqd(c, this.to.x, this.to.y, Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x));\n };\n\n /**\n * Define a class Button for a pseudo-menu button.\n * @param {object} parent The graph to which this button belongs.\n * @param {int} topX The x coordinate of the top left corner of the menu text.\n * @param {int} topY The y coordinate of the top left corner of the menu text.\n * @param {string} text The button label text.\n */\n function Button(parent, topX, topY, text) {\n this.BUTTON_WIDTH = 60;\n this.BUTTON_HEIGHT = 25;\n this.TEXT_OFFSET_X = 30;\n this.TEXT_OFFSET_Y = 17;\n this.topX = topX;\n this.topY = topY;\n this.parent = parent;\n this.text = text;\n this.highLighted = false;\n }\n\n Button.prototype.containsPoint = function(x, y) {\n return util.isInside({x: x, y: y},\n {x: this.topX, y: this.topY, width: this.BUTTON_WIDTH, height: this.BUTTON_HEIGHT});\n };\n\n Button.prototype.draw = function(c) {\n if (this.highLighted) {\n c.fillStyle = '#FFFFFF';\n } else {\n c.fillStyle = '#F0F0F0';\n }\n c.fillRect(this.topX, this.topY,\n this.BUTTON_WIDTH, this.BUTTON_HEIGHT);\n c.lineWidth = 0.5;\n c.strokeStyle = '#000000';\n c.strokeRect(this.topX, this.topY,\n this.BUTTON_WIDTH, this.BUTTON_HEIGHT);\n\n c.font = '12pt Arial';\n c.fillStyle = '#000000';\n c.textAlign = \"center\";\n c.fillText(this.text, this.topX + this.TEXT_OFFSET_X, this.topY + this.TEXT_OFFSET_Y);\n c.textAlign = \"left\";\n };\n\n Button.prototype.onClick = function() {\n\n };\n\n /**\n * Define a class HelpBox for the help box and its pseudo-menu button.\n * @param {object} parent The graph to which this help box belongs.\n * @param {int} topX The x coordinate of the top left corner of the help box.\n * @param {int} topY The y coordinate of the top left corner of the help box.\n */\n function HelpBox(parent, topX, topY) {\n Button.call(this, parent, topX, topY, \"Help\");\n this.helpOpen = false;\n this.LINE_HEIGHT = 18;\n this.HELP_INDENT = 5;\n }\n\n HelpBox.prototype = new Button();\n\n HelpBox.prototype.draw = function(c) {\n var lines, i, y, helpText;\n\n Button.prototype.draw.call(this, c);\n\n if (this.helpOpen) {\n helpText = this.parent.helpText;\n c.font = '12pt Arial';\n lines = helpText.split('\\n');\n y = this.topY + this.BUTTON_HEIGHT;\n for (i = 0; i < lines.length; i += 1) {\n y += this.LINE_HEIGHT;\n c.fillText(lines[i], this.topX + this.HELP_INDENT, y);\n }\n }\n };\n\n HelpBox.prototype.onClick = function() {\n this.helpOpen = ! this.helpOpen;\n this.parent.draw();\n };\n\n\n /**\n * Define a class TextBox for a possibly editable text box that might\n * be contained in another element.\n * @param {string} text The text to put in the text box.\n * @param {object} parent The graph to which the text box belongs.\n **/\n function TextBox(text, parent) {\n this.text = text;\n this.parent = parent;\n this.caretPosition = text.length;\n this.relDist = 0.5;\n this.offset = parent.parent.textOffset();\n this.dragged = false;\n this.boundingBox = {};\n }\n\n // Inserts a given character into the TextBox at its current caretPosition\n TextBox.prototype.insertChar = function(char) {\n this.text = this.text.slice(0, this.caretPosition) + char + this.text.slice(this.caretPosition);\n this.caretRight();\n };\n\n // Deletes the character in the TextBox that is located behind the current caretPosition\n TextBox.prototype.deleteChar = function() {\n if (this.caretPosition > 0){\n this.text = this.text.slice(0, this.caretPosition - 1) + this.text.slice(this.caretPosition);\n this.caretLeft();\n }\n };\n\n // Moves the TextBox's caret left one character if possible\n TextBox.prototype.caretLeft = function() {\n if (this.caretPosition > 0) {\n this.caretPosition --;\n }\n };\n\n // Moves the TextBox's caret right one character if possible\n TextBox.prototype.caretRight = function() {\n if (this.caretPosition < this.text.length) {\n this.caretPosition ++;\n }\n };\n\n TextBox.prototype.containsPoint = function(x, y) {\n var point = {x: x, y: y};\n return util.isInside(point, this.boundingBox);\n };\n\n TextBox.prototype.setMouseStart = function(x, y) {\n // At the start of a drag, record our position relative to the mouse.\n this.mouseOffsetX = this.position.x - x;\n this.mouseOffsetY = this.position.y - y;\n };\n\n TextBox.prototype.setAnchorPoint = function(x, y) {\n x += (this.mouseOffsetX || 0);\n y += (this.mouseOffsetY || 0);\n var linkInfo = this.parent.getEndPointsAndCircle();\n var relDist, offset;\n //Calculate the relative distance of the dragged text along its parent link\n if (linkInfo.hasCircle){\n var textAngle = Math.atan2(y-linkInfo.circleY, x-linkInfo.circleX);\n // Ensure textAngle is either between start and end angle, or more than end angle\n if (textAngle < linkInfo.startAngle) {\n textAngle += Math.PI * 2;\n }\n if (linkInfo.endAngle < linkInfo.startAngle) {\n linkInfo.endAngle += Math.PI * 2;\n }\n // Calculate relDist from angle (inverse of angle-from-relDist calculation in Link.prototype.draw)\n if (linkInfo.isReversed){\n relDist = (textAngle - linkInfo.startAngle - Math.PI*2) / (linkInfo.endAngle - linkInfo.startAngle - Math.PI*2);\n }else{\n relDist = (textAngle - linkInfo.startAngle) / (linkInfo.endAngle - linkInfo.startAngle);\n }\n offset = util.vectorMagnitude({x: x-linkInfo.circleX, y: y-linkInfo.circleY}) - linkInfo.circleRadius;\n }\n else {\n // Calculate relative position of the mouse projected onto the link.\n var textVector = {x: x - linkInfo.startX,\n y: y - linkInfo.startY};\n var linkVector = {x: linkInfo.endX - linkInfo.startX,\n y: linkInfo.endY - linkInfo.startY};\n var projection = util.scalarProjection(textVector, linkVector);\n relDist = projection / util.vectorMagnitude(linkVector);\n // Calculate offset (closest distance) of the mouse position from the link\n offset = Math.sqrt(Math.pow(util.vectorMagnitude(textVector), 2)- Math.pow(projection, 2));\n // If the mouse is on the opposite side of the link from the default text position, negate the offset\n var ccw = util.isCCW(textVector, linkVector);\n var reversed = (this.parent.lineAngleAdjust != 0);\n if ((!ccw && reversed) || (ccw && !reversed)){\n offset *= -1;\n }\n }\n if (relDist > 0 && relDist < 1){ //Ensure text isn't dragged past end of the link\n this.relDist = relDist;\n this.offset = Math.round(offset);\n this.dragged = true;\n }\n };\n\n TextBox.prototype.draw = function(x, y, angleOrNull, parentObject) {\n var graph = parentObject.parent,\n c = graph.getCanvas().getContext('2d');\n\n c.font = graph.fontSize() + 'px Arial';\n //Text before and after caret are drawn separately to expand Latex shortcuts at the caret position\n var beforeCaretText = util.convertLatexShortcuts(this.text.slice(0, this.caretPosition));\n var afterCaretText = util.convertLatexShortcuts(this.text.slice(this.caretPosition));\n var width = c.measureText(beforeCaretText + afterCaretText).width;\n var dy = Math.round(graph.fontSize() / 2);\n\n // Position the text appropriately if it is part of a link\n if(angleOrNull !== null) {\n var cos = Math.cos(angleOrNull);\n var sin = Math.sin(angleOrNull);\n\n //Add text offset in the direction of the text angle\n x += this.offset * cos;\n y += this.offset * sin;\n\n // Position text intelligently if text has not been manually moved\n if (!this.dragged){\n var cornerPointX = (width / 2) * (cos > 0 ? 1 : -1);\n var cornerPointY = (dy / 2) * (sin > 0 ? 1 : -1);\n var slide = sin * Math.pow(Math.abs(sin), 40) * cornerPointX - cos * Math.pow(Math.abs(cos), 10) * cornerPointY;\n x += cornerPointX - sin * slide;\n y += cornerPointY + cos * slide;\n }\n this.position = {x: Math.round(x), y: Math.round(y)}; //Record the position where text is anchored to\n }\n\n x -= width / 2; // Center the text.\n\n //Round the coordinates so they fall on a pixel\n x = Math.round(x);\n y = Math.round(y);\n\n // Draw text and caret\n if('advancedFillText' in c) {\n c.advancedFillText(this.text, this.text, x + width / 2, y, angleOrNull);\n } else {\n // Draw translucent white rectangle behind text\n var prevStyle = c.fillStyle;\n c.fillStyle = \"rgba(255, 255, 255, 0.7)\";\n c.fillRect(x, y-dy, width, dy*2);\n c.fillStyle = prevStyle;\n\n // Draw text\n dy = Math.round(graph.fontSize() / 3); // Don't understand this.\n c.fillText(beforeCaretText, x, y + dy);\n var caretX = x + c.measureText(beforeCaretText).width;\n c.fillText(afterCaretText, caretX, y + dy);\n\n // Draw caret\n dy = Math.round(graph.fontSize() / 2);\n if(parentObject == graph.selectedObject && graph.caretVisible && graph.hasFocus() && document.hasFocus()) {\n c.beginPath();\n c.moveTo(caretX, y - dy);\n c.lineTo(caretX, y + dy);\n c.stroke();\n }\n }\n this.boundingBox = {x: x, y: y - dy, height: dy * 2, width: width};\n };\n\n\n return {\n Node: Node,\n Link: Link,\n SelfLink: SelfLink,\n TemporaryLink: TemporaryLink,\n StartLink: StartLink,\n Button: Button,\n HelpBox: HelpBox,\n TextBox: TextBox\n };\n});\n"],"names":["define","util","Node","parent","x","y","mouseOffsetX","mouseOffsetY","isAcceptState","textBox","TextBox","this","caretPosition","Link","a","b","nodeA","nodeB","lineAngleAdjust","parallelPart","perpendicularPart","SelfLink","node","mouse","anchorAngle","mouseOffsetAngle","setAnchorPoint","StartLink","start","deltaX","deltaY","TemporaryLink","from","to","Button","topX","topY","text","BUTTON_WIDTH","BUTTON_HEIGHT","TEXT_OFFSET_X","TEXT_OFFSET_Y","highLighted","HelpBox","call","helpOpen","LINE_HEIGHT","HELP_INDENT","length","relDist","offset","textOffset","dragged","boundingBox","prototype","setMouseStart","mouseX","mouseY","trackMouse","draw","c","beginPath","arc","nodeRadius","Math","PI","stroke","closestPointOnCircle","dx","dy","scale","sqrt","containsPoint","neighbours","links","link","i","includes","push","traverseGraph","visited","neighbour","getAnchorPoint","abs","SNAP_TO_PADDING","getEndPointsAndCircle","midX","midY","end","anchor","circle","circleFromThreePoints","isReversed","reverseScale","rRatio","radius","startAngle","atan2","endAngle","cos","sin","textX","textY","textAngle","linkInfo","hasCircle","circleX","circleY","circleRadius","moveTo","startX","startY","lineTo","endX","endY","arrowIfReqd","distance","percent","HIT_TARGET_PADDING","angle","temp","mouseStartX","mouseStartY","snap","round","getEndPoints","endPoints","isInside","width","height","fillStyle","fillRect","lineWidth","strokeStyle","strokeRect","font","textAlign","fillText","onClick","lines","helpText","split","insertChar","char","slice","caretRight","deleteChar","caretLeft","point","position","vectorMagnitude","textVector","linkVector","projection","scalarProjection","pow","ccw","isCCW","reversed","angleOrNull","parentObject","graph","getCanvas","getContext","fontSize","beforeCaretText","convertLatexShortcuts","afterCaretText","measureText","cornerPointX","cornerPointY","slide","advancedFillText","prevStyle","caretX","selectedObject","caretVisible","hasFocus","document"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/graphutil.min.js.map b/amd/build/graphutil.min.js.map
deleted file mode 100644
index 23ac65002..000000000
--- a/amd/build/graphutil.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"graphutil.min.js","sources":["../src/graphutil.js"],"sourcesContent":["/***********************************************************************\n *\n * Utility functions/data/constants for the ui_graph module.\n *\n ***********************************************************************/\n// Most of this code is taken from Finite State Machine Designer:\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(function() {\n /**\n * Contstructor for the Util class.\n */\n function Util() {\n\n this.greekLetterNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',\n 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda',\n 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',\n 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega' ];\n }\n\n Util.prototype.convertLatexShortcuts = function(text) {\n // Html greek characters.\n for(var i = 0; i < this.greekLetterNames.length; i++) {\n var name = this.greekLetterNames[i];\n text = text.replace(new RegExp('\\\\\\\\' + name, 'g'), String.fromCharCode(913 + i + (i > 16)));\n text = text.replace(new RegExp('\\\\\\\\' + name.toLowerCase(), 'g'), String.fromCharCode(945 + i + (i > 16)));\n }\n\n // Subscripts.\n for(var i = 0; i < 10; i++) {\n text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));\n }\n text = text.replace(new RegExp('_a', 'g'), String.fromCharCode(8336));\n return text;\n };\n\n Util.prototype.drawArrow = function(c, x, y, angle) {\n // Draw an arrow head on the graphics context c at (x, y) with given angle.\n\n var dx = Math.cos(angle);\n var dy = Math.sin(angle);\n c.beginPath();\n c.moveTo(x, y);\n c.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);\n c.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);\n c.fill();\n };\n\n Util.prototype.det = function(a, b, c, d, e, f, g, h, i) {\n // Determinant of given matrix elements.\n return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g;\n };\n\n Util.prototype.vectorMagnitude = function(v){\n // Returns magnitude (length) of a vector v\n return Math.sqrt(v.x * v.x + v.y * v.y);\n };\n\n Util.prototype.scalarProjection = function(a, b) {\n // Returns scalar projection of vector a onto vector b\n return (a.x * b.x + a.y * b.y) / this.vectorMagnitude(b);\n };\n\n Util.prototype.isCCW = function(a, b) {\n // Returns true iff vector b is in a counter-clockwise orientation relative to a\n return (a.x * b.y) - (b.x * a.y) > 0;\n };\n\n Util.prototype.circleFromThreePoints = function(x1, y1, x2, y2, x3, y3) {\n // Return {x, y, radius} of circle through (x1, y1), (x2, y2), (x3, y3).\n var a = this.det(x1, y1, 1, x2, y2, 1, x3, y3, 1);\n var bx = -this.det(x1 * x1 + y1 * y1, y1, 1, x2 * x2 + y2 * y2, y2, 1, x3 * x3 + y3 * y3, y3, 1);\n var by = this.det(x1 * x1 + y1 * y1, x1, 1, x2 * x2 + y2 * y2, x2, 1, x3 * x3 + y3 * y3, x3, 1);\n var c = -this.det(x1 * x1 + y1 * y1, x1, y1, x2 * x2 + y2 * y2, x2, y2, x3 * x3 + y3 * y3, x3, y3);\n return {\n 'x': -bx / (2 * a),\n 'y': -by / (2 * a),\n 'radius': Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))\n };\n };\n\n Util.prototype.isInside = function(pos, rect) {\n // True iff given point pos is inside rectangle.\n return pos.x > rect.x && pos.x < rect.x + rect.width && pos.y < rect.y + rect.height && pos.y > rect.y;\n };\n\n Util.prototype.crossBrowserKey = function(e) {\n // Return which key was pressed, given the event, in a browser-independent way.\n e = e || window.event;\n return e.which || e.keyCode;\n };\n\n\n Util.prototype.crossBrowserRelativeMousePos = function(e) {\n // Earlier complex version was breaking in Moodle 4, so replaced\n // with this much simpler version that should work with all modern\n // browsers.\n const rect = e.target.getBoundingClientRect();\n const x = e.clientX - rect.left; //x position within the element.\n const y = e.clientY - rect.top; //y position within the element.\n return {'x': x, 'y': y};\n };\n\n return new Util();\n});\n"],"names":["define","Util","greekLetterNames","prototype","convertLatexShortcuts","text","i","this","length","name","replace","RegExp","String","fromCharCode","toLowerCase","drawArrow","c","x","y","angle","dx","Math","cos","dy","sin","beginPath","moveTo","lineTo","fill","det","a","b","d","e","f","g","h","vectorMagnitude","v","sqrt","scalarProjection","isCCW","circleFromThreePoints","x1","y1","x2","y2","x3","y3","bx","by","abs","isInside","pos","rect","width","height","crossBrowserKey","window","event","which","keyCode","crossBrowserRelativeMousePos","target","getBoundingClientRect","clientX","left","clientY","top"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/multilanguagequestion.min.js.map b/amd/build/multilanguagequestion.min.js.map
deleted file mode 100644
index 820d8aa0e..000000000
--- a/amd/build/multilanguagequestion.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"multilanguagequestion.min.js","sources":["../src/multilanguagequestion.js"],"sourcesContent":["/******************************************************************************\n *\n * This module simply handles changes in the Language selection dropdown for\n * multilanguage questions as seen by students. It switches the Ace language\n * accordingly.\n * It should only be loaded in conjunction with the ui_ace module.\n *\n * @module qtype_coderunner/multilanguagequestion\n * @copyright Richard Lobb, 2018, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * TODO: is there a race problem here??\n */\n\ndefine(['jquery'], function($) {\n /**\n * Initialise the language selector dropdown when the document is ready.\n * @param {string} taId The ID of the student answer box text area.\n */\n function initLangSelector(taId) {\n $().ready(function() {\n initLangSelectorWhenReady(taId);\n });\n }\n\n /**\n * Initialise the language selector. Called by initLanSelector when the\n * document is ready so the name is a bit of a misnomer.\n * @param {string} taId The ID of the student answer box text area.\n */\n function initLangSelectorWhenReady(taId) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n selector = $(\".coderunner-lang-select\");\n\n /**\n * Set the language for the Ace editor.\n */\n function setAceLang() {\n // Set the language for the Ace plugin (or any other plugin if it\n // has a setLanguage method).\n var lang = selector.val(),\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on reqd ta.\n\n if (uiWrapper && uiWrapper.uiInstance && typeof uiWrapper.uiInstance.setLanguage === 'function') {\n // TODO: define setLanguage as a required method of all UI plugins.\n uiWrapper.uiInstance.setLanguage(lang);\n }\n }\n\n selector.on('change', setAceLang);\n }\n\n return {'initLangSelector' : initLangSelector};\n});"],"names":["define","$","taId","ready","ta","document","getElementById","selector","setAceLang","lang","val","uiWrapper","data","uiInstance","setLanguage","on","initLangSelectorWhenReady"],"mappings":";;;;;;;;;;;;;AAcAA,gDAAO,CAAC,WAAW,SAASC,SAsCjB,2BAjCmBC,MACtBD,IAAIE,OAAM,qBAUqBD,UAC3BE,GAAKH,EAAEI,SAASC,eAAeJ,OAC/BK,SAAWN,EAAE,oCAKRO,iBAGDC,KAAOF,SAASG,MAChBC,UAAYP,GAAGQ,KAAK,sBAEpBD,WAAaA,UAAUE,YAA0D,mBAArCF,UAAUE,WAAWC,aAEjEH,UAAUE,WAAWC,YAAYL,MAIzCF,SAASQ,GAAG,SAAUP,YA5BlBQ,CAA0Bd"}
\ No newline at end of file
diff --git a/amd/build/outputdisplayarea.min.js b/amd/build/outputdisplayarea.min.js
new file mode 100644
index 000000000..102f26059
--- /dev/null
+++ b/amd/build/outputdisplayarea.min.js
@@ -0,0 +1,8 @@
+define("qtype_coderunner/outputdisplayarea",["exports","jquery","core/ajax","core/str"],(function(_exports,_jquery,_ajax,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+/**
+ * @module coderunner/outputdisplayarea
+ * @copyright James Napier, 2023, The University of Canterbury
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.OutputDisplayArea=void 0,_jquery=_interopRequireDefault(_jquery),_ajax=_interopRequireDefault(_ajax);_exports.OutputDisplayArea=class{constructor(displayAreaId,outputMode){this.displayAreaId=displayAreaId,this.displayArea=document.getElementById(displayAreaId),this.textDisplay=document.getElementById(displayAreaId+"-text"),this.htmlDisplay=document.getElementById(displayAreaId+"-html"),this.imageDisplay=document.getElementById(displayAreaId+"-images"),this.mode=outputMode,this.stdIn=[]}clearDisplay(){this.textDisplay.innerHTML="",this.htmlDisplay.innerHTML="",this.imageDisplay.innerHTML=""}displayText(response){const output=response.output,error=response.stderr;this.textDisplay.innerText=output+error}displayHtml(response){const output=response.output,error=response.stderr;this.textDisplay.innerHTML=output+error}displayNoOutput(response){const isNoOutput=""===response.output&&""===response.stderr;return isNoOutput&&(this.textDisplay.innerHTML='< No output! >'),isNoOutput}display(response){if(!this.displayNoOutput(response))if("json"===this.mode){const json=JSON.parse(response);this.displayJson(json)}else if("html"===this.mode)this.displayHtml(response);else{if("text"!==this.mode)throw Error('Invalid outputMode given: "'.concat(this.mode,'"'));{const text=response;this.displayText(text)}}}async handleRunButtonClick(code,lang,sandboxParams){this.clearDisplay(),_ajax.default.call([{methodname:"qtype_coderunner_run_in_sandbox",args:{contextid:M.cfg.contextid,sourcecode:code,language:lang,params:JSON.stringify(sandboxParams)},done:responseJson=>{const response=JSON.parse(responseJson);this.display(response)},fail:error=>{alert(error.message)}}])}displayJson(json){var result=json,text=result.stdout;42!==result.returncode&&(text+=result.stderr),13==result.returncode&&(text+="\n*** Timeout error ***\n");var base64,numImages=0;if(result.files)for(var prop in(0,_jquery.default)(this.imageDisplay).empty(),result.files){var image=(base64=result.files[prop],(0,_jquery.default)('')));(0,_jquery.default)(this.imageDisplay).append(image),numImages+=1}if(""===text.trim()&&42!==result.returncode?0==numImages&&(0,_jquery.default)(this.textDisplay).html('< No output! >'):(0,_jquery.default)(this.textDisplay).text(text),42===result.returncode){var inputId="".concat(this.displayAreaId,"-input-field");(0,_jquery.default)(this.textDisplay).html((0,_jquery.default)(this.textDisplay).html()+''));var inputEl=(0,_jquery.default)(document.getElementById(inputId));inputEl.focus(),inputEl.on("keyup",(e=>{if(13===e.keyCode){const line=inputEl.val();inputEl.remove(),(0,_jquery.default)(this.textDisplay).html((0,_jquery.default)(this.textDisplay).html()+line),this.handleRunButtonClick()}}))}}}}));
+
+//# sourceMappingURL=outputdisplayarea.min.js.map
\ No newline at end of file
diff --git a/amd/build/resetbutton.min.js.map b/amd/build/resetbutton.min.js.map
deleted file mode 100644
index 6ca89e48d..000000000
--- a/amd/build/resetbutton.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"resetbutton.min.js","sources":["../src/resetbutton.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 * This AMD module provides the functionality for the \"Reset\"\n * button that is shown just below the student answer field if the question is\n * defined to have preloaded text.\n * If clicked, the button reloads the student answer field with the original\n * preloaded text (after a Confirm dialogue, of course).\n *\n * @module qtype_coderunner/resetbutton\n * @copyright Richard Lobb, 2016, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Initialise the Reset button.\n * @param {string} buttonId The ID of the button.\n * @param {string} answerId The ID of the answer box whose contents are to\n * be reset.\n * @param {string} confirmText The language string to display when asking\n * the user to confirm the reset.\n */\n function initResetButton(buttonId, answerId, confirmText) {\n var resetButton = $('[id=\"' + buttonId + '\"]'),\n studentAnswer = $('[id=\"' + answerId + '\"]'),\n uiWrapper;\n\n resetButton.on(\"click\", function() {\n if (window.behattesting || window.confirm(confirmText)) {\n var reloadText = resetButton.attr('data-reload-text');\n uiWrapper = studentAnswer.data('current-ui-wrapper');\n if (uiWrapper && uiWrapper.uiInstance) {\n // If the textarea has a UI wrapper, and it's active.\n uiWrapper.stop();\n studentAnswer.val(reloadText);\n uiWrapper.restart();\n } else {\n studentAnswer.val(reloadText);\n }\n }\n });\n }\n\n return { \"initResetButton\": initResetButton };\n});\n"],"names":["define","$","buttonId","answerId","confirmText","uiWrapper","resetButton","studentAnswer","on","window","behattesting","confirm","reloadText","attr","data","uiInstance","stop","val","restart"],"mappings":";;;;;;;;;;;AA4BAA,sCAAO,CAAC,WAAW,SAASC,SA+BjB,0BArBkBC,SAAUC,SAAUC,iBAGrCC,UAFAC,YAAcL,EAAE,QAAUC,SAAW,MACrCK,cAAgBN,EAAE,QAAUE,SAAW,MAG3CG,YAAYE,GAAG,SAAS,cAChBC,OAAOC,cAAgBD,OAAOE,QAAQP,aAAc,KAChDQ,WAAaN,YAAYO,KAAK,qBAClCR,UAAYE,cAAcO,KAAK,wBACdT,UAAUU,YAEvBV,UAAUW,OACVT,cAAcU,IAAIL,YAClBP,UAAUa,WAEVX,cAAcU,IAAIL"}
\ No newline at end of file
diff --git a/amd/build/showdiff.min.js.map b/amd/build/showdiff.min.js.map
deleted file mode 100644
index a80d3f760..000000000
--- a/amd/build/showdiff.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"showdiff.min.js","sources":["../src/showdiff.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 * This AMD module provides the functionality for the \"Show differences\"\n * button that is shown in the student's result page if their answer\n * isn't right and an \"exact-match\" (or near equivalent) grader is being used.\n *\n * @module qtype_coderunner/showdiff\n * @copyright Richard Lobb, 2016, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n var NLCHAR = '\\u21A9'; // Unicode \"leftwards arrow with hook\" to show newlines.\n\n /**\n * Given two lists of items, items1 and item2, return the length matrix\n * M defined as M[i][j] = max subsequence length of the two item lists\n * items1[0:i], items2[0:j]\n * @param {array} items1 The first list of items.\n * @param {array} items2 The second list of items.\n * @return {array} The length matrix.\n */\n function lcsLengths(items1, items2) {\n\n var n1 = items1.length,\n n2 = items2.length,\n lengths, i, j,\n has_fill = typeof [1].fill === 'function';\n\n lengths = [];\n\n for (i = 0; i <= n1; i += 1) {\n lengths[i] = new Array(n2 + 1);\n if (has_fill) {\n lengths[i].fill(0);\n } else {\n // Bloody IE.\n for (j = 0; j < n2 + 1; j++) {\n lengths[i][j] = 0;\n }\n }\n }\n for (i = 0; i < n1; i += 1) {\n for (j = 0; j < n2; j += 1) {\n if (items1[i] == items2[j]) {\n lengths[i + 1][j + 1] = 1 + lengths[i][j];\n } else {\n lengths[i + 1][j + 1] = Math.max(lengths[i][j + 1], lengths[i + 1][j]);\n }\n }\n }\n return lengths;\n }\n\n /**\n * Given two lists of items, items1 and item2, return the longest common\n * subsequence.\n * @param {array} items1 The first list of items.\n * @param {array} items2 The second list of items.\n * @return {array} The longest common subsequence.\n */\n function lcss(items1, items2) {\n\n var M, i, j, n, result, length;\n M = lcsLengths(items1, items2);\n length = M[items1.length][items2.length];\n result = [];\n i = items1.length;\n j = items2.length;\n n = length - 1;\n while (n >= 0) {\n if (items1[i - 1] == items2[j - 1]) {\n result[n] = items1[i - 1];\n n -= 1;\n i -= 1;\n j -= 1;\n } else if (M[i - 1][j] == M[i][j]) {\n i -= 1;\n } else {\n j -= 1;\n }\n }\n return result;\n }\n\n /**\n * Process the given token list and a subsequence of it, concatenating\n * tokens and wrapping all items not in the subsequence with\n * del tags (or whatever strings are specified by startDel, endDel).\n * @param {array} tokens A list of tokens in the original string.\n * @param {array} subSeq A subsequence of the tokens array.\n * @param {string} startDel An optional string that denotes the start of\n * a sequence of tokens to be deleted. Default ''\n * @param {string} endDel An optional string to mark the end of a sequence\n * of deleted tokens. Default ''.\n * @return {string} The concatenated sequence of tokens with start and\n * end delete tokens inserted to mark where the tokens from the first\n * parameter are not present in the second.\n */\n function insertDels(tokens, subSeq, startDel, endDel) {\n var html = \"\",\n deleting = false,\n i,\n ssi = 0;\n if (startDel === undefined) {\n startDel = '';\n }\n if (endDel === undefined) {\n endDel = '';\n }\n for (i = 0; i < tokens.length; i += 1) {\n if (ssi < subSeq.length && tokens[i] == subSeq[ssi]) {\n if (deleting) {\n html += endDel;\n deleting = false;\n }\n ssi += 1;\n } else {\n if (!deleting) {\n html += startDel;\n deleting = true;\n }\n }\n\n html += tokens[i];\n }\n if (deleting) {\n html += endDel;\n }\n return html;\n }\n\n /**\n * @param {string} elem An HTML element\n * @return {string} The HTML element type (i.e. its tag name) in lower case\n */\n function elType(elem) {\n return elem.tagName.toLowerCase();\n }\n\n /**\n * Return the sequence of tokens from the given HTML element.\n * A token is either a single character or an HTML entity (&.*;)\n * Extra 'leftward-arrow-with-hook' characters (\\u21A9) are added\n * at the ends of lines.\n * @param {string} element The HTML element whose contents are to be tokenised.\n * @return {array} The list of tokens extracted from the element.\n */\n function getTokens(element) {\n var isPre = elType(element) === 'pre',\n text = element.innerHTML,\n seq,\n i = 0;\n\n /**\n * Extract and return the next token starting at text[i]. Update i.\n * Precondition: i < text.length.\n */\n function nextToken() {\n var token, match;\n if (text[i] != '&') {\n token = text[i];\n i = i + 1;\n } else {\n match = text.substring(i, text.length).match(/(^&[a-zA-Z]+;)|(^[0-9]+;)|(^[xX][0-9a-fA-F]+;)/);\n if (match === null) {\n token = text[i];\n i = i + 1;\n } else {\n token = match[0];\n i = i + token.length;\n }\n }\n return token;\n }\n\n if (isPre) {\n text = text.replace(/\\n/g, NLCHAR + '\\n');\n }\n text = text.replace(/( )/g, NLCHAR + '$1');\n seq = [];\n i = 0;\n while (i < text.length) {\n seq.push(nextToken());\n }\n\n return seq;\n }\n\n /**\n * Given (references to) two HTML elements, extract the innerHTML\n * of both, find the longest common subsequence of chars and wrap text not\n * in that subsequence in del elements.\n * elements within the innerHTML are preceded by a\n * Unicode \"leftwards arrow with hook\" ('\\u21A9') so that line break changes\n * can be highlighted.\n * @param {string} firstEl The first HTML element to be processed.\n * @param {string} secondEl The second HTML element to be processed.\n */\n function showDifferences(firstEl, secondEl) {\n var openDelTag = '',\n closeDelTag = '',\n seq1,\n seq2,\n css;\n\n seq1 = getTokens(firstEl);\n seq2 = getTokens(secondEl);\n css = lcss(seq1, seq2);\n firstEl.innerHTML = insertDels(seq1, css, openDelTag, closeDelTag);\n secondEl.innerHTML = insertDels(seq2, css, openDelTag, closeDelTag);\n }\n\n /**\n * Given (references to) two DOM elements, delete all and \n * tags from the innerHTML of both. Also remove the \"leftwards arrows with\n * hooks\".\n * @param {string} firstEl The first HTML element to be processed.\n * @param {string} secondEl The second HTML element to be processed.\n */\n function hideDifferences(firstEl, secondEl) {\n var replPat = new RegExp('(?del[^>]*>)|(' + NLCHAR + ')', 'g');\n firstEl.innerHTML = firstEl.innerHTML.replace(replPat, '');\n secondEl.innerHTML = secondEl.innerHTML.replace(replPat, '');\n }\n\n /**\n * Now the API for applying diffs to rows in a CodeRunner\n * results table. Defines a class with methods initDiffButton and\n * processAllRows.\n * @param {array} tableRows The list of rows from the CodeRunner results table.\n * @param {int} gotCol The column number of the 'Got' column in the table.\n * @param {int} expectedCol The column number of the 'Expected' column.\n * @param {function} f The function to apply to the (expected, got) pair.\n */\n function processAllRows(tableRows, gotCol, expectedCol, f) {\n var row,\n cells,\n expected,\n got;\n\n for (var i = 0; i < tableRows.length; i++) {\n row = tableRows[i];\n cells = row.getElementsByTagName('td');\n expected = cells[expectedCol].children[0];\n got = cells[gotCol].children[0];\n f(expected, got);\n }\n }\n\n /**\n * Initialise the Show Differences button.\n * @param {string} buttonId The ID of the Show Differences button.\n * @param {string} showValue The text in the button initially.\n * @param {string} hideValue The text in the button when differences are showing.\n * @param {string} expectedString The column header denoting the 'Expected' column.\n * @param {string} gotString The column header denoting the 'Got' column.\n */\n function initDiffButton(buttonId, showValue, hideValue, expectedString, gotString) {\n var diffButton = $('[id=\"' + buttonId + '\"]'),\n table,\n tableRows,\n thEls,\n columnCount=0,\n gotCol=-1,\n expectedCol=-1;\n\n table = diffButton.closest('div.coderunner-test-results');\n thEls = table.find('thead tr').children();\n tableRows = table.find('tbody tr');\n\n // Find 'Expected' and 'Got' columns.\n thEls.each(function() {\n if ($(this).html() === gotString) {\n gotCol = columnCount;\n } else if ($(this).html() === expectedString) {\n expectedCol = columnCount;\n }\n columnCount += 1;\n });\n\n if (gotCol !== -1 && expectedCol !== -1) {\n diffButton.on(\"click\", function() {\n if (diffButton.prop('value') === showValue) {\n processAllRows(tableRows.toArray(), gotCol, expectedCol, showDifferences);\n diffButton.prop('value', hideValue);\n } else {\n processAllRows(tableRows.toArray(), gotCol, expectedCol, hideDifferences);\n diffButton.prop('value', showValue);\n }\n });\n } else {\n diffButton.enabled = false;\n diffButton.hide();\n }\n }\n\n return { \"initDiffButton\": initDiffButton };\n});\n"],"names":["define","$","lcss","items1","items2","M","i","j","n","result","length","lengths","n1","n2","has_fill","fill","Array","Math","max","lcsLengths","insertDels","tokens","subSeq","startDel","endDel","html","deleting","ssi","undefined","getTokens","element","seq","token","match","isPre","tagName","toLowerCase","text","innerHTML","replace","NLCHAR","push","substring","showDifferences","firstEl","secondEl","seq1","seq2","css","hideDifferences","replPat","RegExp","processAllRows","tableRows","gotCol","expectedCol","f","cells","getElementsByTagName","children","buttonId","showValue","hideValue","expectedString","gotString","table","thEls","diffButton","columnCount","closest","find","each","this","on","prop","toArray","enabled","hide"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/textareas.min.js.map b/amd/build/textareas.min.js.map
deleted file mode 100644
index 617c95e64..000000000
--- a/amd/build/textareas.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"textareas.min.js","sources":["../src/textareas.js"],"sourcesContent":["/**\n * 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/**\n * JavaScript for handling textareas and form actions in CodeRunner question\n * editing forms and student question answering forms.\n *\n * @module qtype_coderunner/textareas\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Function to initialise all code-input text-areas in a page.\n * Used by the form editor but can't be used for question text areas as\n * renderer.php is called once for each question in a quiz, and there is\n * no communication between the questions.\n */\n function setupAllTAs() {\n $('textarea.edit_code').each(initTextArea);\n }\n\n /**\n * Initialise a particular text area (TA), given its ID (which may contain\n * colons, so can't use jQuery selector).\n * @param {string} taId The ID of the textarea html element.\n */\n function initQuestionTA(taId) {\n $(document.getElementById(taId)).each(initTextArea);\n }\n\n\n /**\n * Set up to expand or collapse the question author's answer when the user clicks\n * on the show/hide answer button.\n * @param {string} linkId The ID of the link that the user clicks.\n * @param {sting} divId The ID of the div to be shown or hidden.\n */\n function setupShowHideAnswer(linkId, divId) {\n let link = $(document.getElementById(linkId)),\n div = $(document.getElementById(divId)),\n hdr = link.children()[0], // The
element.\n hdrText = hdr.innerText; // Its body.\n link.click(() => {\n if (div.is(\":visible\")) {\n div.hide();\n hdr.innerText = hdrText.replace(\"\\u25BE\", \"\\u25B8\");\n } else {\n div.show();\n div.trigger('mousemove'); // So user sees contents.\n hdr.innerText = hdrText.replace(\"\\u25B8\", \"\\u25BE\");\n }\n });\n }\n\n /**\n * Set up the JavaScript to handle a text area 'this'.\n * It just does rudimentary autoindent on return and replaces tabs with\n * 4 spaces always.\n * For info on key handling browser inconsistencies see\n * http://unixpapa.com/js/key.html\n * If the AceEditor is handling a text area, this code is unused as\n * the actual textarea is hidden by Ace.\n */\n function initTextArea() {\n var TAB = 9,\n ENTER = 13,\n ESC = 27,\n KEY_M = 77;\n\n $(this).data('clickInProgress', false);\n $(this).data('capturingTab', true);\n\n $(this).on('mousedown', function() {\n /*\n * Event order seems to be (\\ is where the mouse button is pressed, / released):\n * Chrome: \\ mousedown, mouseup, focusin / click.\n * Firefox/IE: \\ mousedown, focusin / mouseup, click.\n */\n $(this).data('clickInProgress', true);\n });\n\n $(this).on('focusin', function() {\n /*\n * At first, pressing TAB moves focus.\n */\n $(this).data('capturingTab', $(this).data('clickInProgress'));\n });\n\n $(this).on('click', function() {\n $(this).data('clickInProgress', false);\n });\n\n $(this).on('keydown', function(e) {\n /*\n * Don't autoindent when behat testing in progress.\n */\n if (window.hasOwnProperty('behattesting') && window.behattesting) { return; }\n\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode == TAB && $(this).data('capturingTab')) {\n /*\n * Ignore SHIFT/TAB. Insert 4 spaces on TAB.\n */\n if (e.shiftKey || insertString(this, \" \")) {\n e.preventDefault();\n }\n }\n else if (e.keyCode === ENTER && this.selectionStart !== undefined) {\n /*\n * Handle autoindent only on non-IE.\n */\n var before = this.value.substring(0, this.selectionStart);\n var eol = before.lastIndexOf(\"\\n\");\n var line = before.substring(eol + 1); // Take from eol to end.\n var indent = \"\";\n for (var i = 0; i < line.length && line.charAt(i) === ' '; i++) {\n indent = indent + \" \";\n }\n if (insertString(this, \"\\n\" + indent)) {\n e.preventDefault();\n }\n /*\n * Once the user has started typing, TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n else if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n /*\n * CTRL + M toggles TAB capturing mode.\n * This is the short-cut recommended by\n * https:www.w3.org/TR/wai-aria-practices/#richtext.\n */\n $(this).data('capturingTab', !$(this).data('capturingTab'));\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n /*\n * ESC always stops capturing TAB.\n */\n $(this).data('capturingTab', false);\n }\n else if (!(e.ctrlKey || e.altKey)) {\n /*\n * Once the user has started typing (not modifier keys) TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n }\n });\n }\n\n /**\n * Insert into the given textarea ta the given string sToInsert.\n * @param {html_element} ta The textarea to be updated.\n * @param {string} sToInsert The string to be inserted at the current selection\n * point.\n */\n function insertString(ta, sToInsert) {\n if (ta.selectionStart !== undefined) { // Firefox etc.\n var before = ta.value.substring(0, ta.selectionStart);\n var selSave = ta.selectionEnd;\n var after = ta.value.substring(ta.selectionEnd, ta.value.length);\n\n /**\n * Update the text field.\n */\n var tmp = ta.scrollTop; // Inhibit annoying auto-scroll.\n ta.value = before + sToInsert + after;\n var pos = selSave + sToInsert.length;\n ta.selectionStart = pos;\n ta.selectionEnd = pos;\n ta.scrollTop = tmp;\n return true;\n\n }\n else if (document.selection && document.selection.createRange) { // IE.\n /*\n * TODO: check if this still works. OLD CODE.\n */\n var r = document.selection.createRange();\n var dr = r.duplicate();\n dr.moveToElementText(ta);\n dr.setEndPoint(\"EndToEnd\", r);\n r.text = sToInsert;\n return true;\n }\n /*\n * Other browsers we can't handle.\n */\n else {\n return false;\n }\n }\n\n return {\n setupAllTAs: setupAllTAs,\n initQuestionTA: initQuestionTA,\n setupShowHideAnswer: setupShowHideAnswer,\n };\n});\n"],"names":["define","$","initTextArea","this","data","on","e","window","hasOwnProperty","behattesting","undefined","which","keyCode","shiftKey","insertString","preventDefault","selectionStart","before","value","substring","eol","lastIndexOf","line","indent","i","length","charAt","ctrlKey","altKey","ta","sToInsert","selSave","selectionEnd","after","tmp","scrollTop","pos","document","selection","createRange","r","dr","duplicate","moveToElementText","setEndPoint","text","setupAllTAs","each","initQuestionTA","taId","getElementById","setupShowHideAnswer","linkId","divId","link","div","hdr","children","hdrText","innerText","click","is","hide","replace","show","trigger"],"mappings":";;;;;;;;AA2BAA,oCAAO,CAAC,WAAW,SAASC,YAsDfC,eAMLD,EAAEE,MAAMC,KAAK,mBAAmB,GAChCH,EAAEE,MAAMC,KAAK,gBAAgB,GAE7BH,EAAEE,MAAME,GAAG,aAAa,WAMpBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,WAIlBJ,EAAEE,MAAMC,KAAK,eAAgBH,EAAEE,MAAMC,KAAK,uBAG9CH,EAAEE,MAAME,GAAG,SAAS,WAChBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,SAASC,QAIvBC,OAAOC,eAAe,iBAAmBD,OAAOE,mBAEpCC,IAAZJ,EAAEK,OAAmC,IAAZL,EAAEK,UAlCzB,GAmCEL,EAAEM,SAAkBX,EAAEE,MAAMC,KAAK,iBAI7BE,EAAEO,UAAYC,aAAaX,KAAM,UACjCG,EAAES,sBAGL,GA1CD,KA0CKT,EAAEM,cAA6CF,IAAxBP,KAAKa,eAA8B,SAI3DC,OAASd,KAAKe,MAAMC,UAAU,EAAGhB,KAAKa,gBACtCI,IAAMH,OAAOI,YAAY,MACzBC,KAAOL,OAAOE,UAAUC,IAAM,GAC9BG,OAAS,GACJC,EAAI,EAAGA,EAAIF,KAAKG,QAA6B,MAAnBH,KAAKI,OAAOF,GAAYA,IACvDD,QAAkB,IAElBT,aAAaX,KAAM,KAAOoB,SAC1BjB,EAAES,iBAKNd,EAAEE,MAAMC,KAAK,gBAAgB,QAzD7B,KA2DKE,EAAEM,SAAqBN,EAAEqB,UAAYrB,EAAEsB,QAM5C3B,EAAEE,MAAMC,KAAK,gBAAiBH,EAAEE,MAAMC,KAAK,iBAC3CE,EAAES,kBAnEJ,KAqEOT,EAAEM,QAIPX,EAAEE,MAAMC,KAAK,gBAAgB,GAEtBE,EAAEqB,SAAWrB,EAAEsB,QAItB3B,EAAEE,MAAMC,KAAK,gBAAgB,eAYpCU,aAAae,GAAIC,mBACIpB,IAAtBmB,GAAGb,eAA8B,KAC7BC,OAASY,GAAGX,MAAMC,UAAU,EAAGU,GAAGb,gBAClCe,QAAUF,GAAGG,aACbC,MAAQJ,GAAGX,MAAMC,UAAUU,GAAGG,aAAcH,GAAGX,MAAMO,QAKrDS,IAAML,GAAGM,UACbN,GAAGX,MAAQD,OAASa,UAAYG,UAC5BG,IAAML,QAAUD,UAAUL,cAC9BI,GAAGb,eAAiBoB,IACpBP,GAAGG,aAAeI,IAClBP,GAAGM,UAAYD,KACR,EAGN,GAAIG,SAASC,WAAaD,SAASC,UAAUC,YAAa,KAIvDC,EAAIH,SAASC,UAAUC,cACvBE,GAAKD,EAAEE,mBACXD,GAAGE,kBAAkBd,IACrBY,GAAGG,YAAY,WAAYJ,GAC3BA,EAAEK,KAAOf,WACF,SAMA,QAIR,CACHgB,uBAjLA7C,EAAE,sBAAsB8C,KAAK7C,eAkL7B8C,wBA1KoBC,MACpBhD,EAAEoC,SAASa,eAAeD,OAAOF,KAAK7C,eA0KtCiD,6BAhKyBC,OAAQC,WAC7BC,KAAOrD,EAAEoC,SAASa,eAAeE,SACjCG,IAAMtD,EAAEoC,SAASa,eAAeG,QAChCG,IAAMF,KAAKG,WAAW,GACtBC,QAAUF,IAAIG,UAClBL,KAAKM,OAAM,KACHL,IAAIM,GAAG,aACPN,IAAIO,OACJN,IAAIG,UAAYD,QAAQK,QAAQ,IAAU,OAE1CR,IAAIS,OACJT,IAAIU,QAAQ,aACZT,IAAIG,UAAYD,QAAQK,QAAQ,IAAU"}
\ No newline at end of file
diff --git a/amd/build/ui_ace.min.js.map b/amd/build/ui_ace.min.js.map
deleted file mode 100644
index 9e67f3290..000000000
--- a/amd/build/ui_ace.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"ui_ace.min.js","sources":["../src/ui_ace.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 * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n session.setValue(this.textarea.val());\n\n this.fixSlowLoad();\n\n // Set theme if available (not currently enabled).\n if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n }\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // Nothing to do ... always sync'd\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 0;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n // Sometimes Ace editors do not load until the mouse is moved. To fix this,\n // 'move' the mouse using JQuerry when the editor div enters the viewport.\n AceWrapper.prototype.fixSlowLoad = function () {\n const observer = new IntersectionObserver( () => {\n $(document).trigger('mousemove');\n });\n const editNode = this.editNode.get(0); // Non-JQuerry node.\n observer.observe(editNode);\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","getSession","setValue","val","fixSlowLoad","theme","setTheme","matchMedia","matches","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","failed","failMessage","sync","syncIntervalSecs","language","session","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","observer","IntersectionObserver","trigger","observe","getValue","container","addEventListener","e","undefined","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_ace_gapfiller.min.js.map b/amd/build/ui_ace_gapfiller.min.js.map
deleted file mode 100644
index 4b640e3ad..000000000
--- a/amd/build/ui_ace_gapfiller.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"ui_ace_gapfiller.min.js","sources":["../src/ui_ace_gapfiller.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 util.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 * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","matchMedia","matches","setTheme","theme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_gapfiller.min.js.map b/amd/build/ui_gapfiller.min.js.map
deleted file mode 100644
index 5dcfd71fe..000000000
--- a/amd/build/ui_gapfiller.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"ui_gapfiller.min.js","sources":["../src/ui_gapfiller.js"],"sourcesContent":["/**\n * 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 util.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/**\n * Implementation of the gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer box with a div\n * consisting of pre-formatted text supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra). HTML\n * entry or textarea elements are then inserted at\n * specified points. It is intended primarily for use with coding questions\n * where the answerbox presents the students with code that has smallish bits\n * missing.\n *\n * The locations within the globalextra text at which the input elements are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * for an HTML input element\n *\n * or\n *\n * {[ rows, columns ]}\n *\n * for a textarea element\n *\n * where size, rows and column are integer literals. These respectively\n * inject an HTML input element or a textarea element of the\n * specified size.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the input element insertion tags are by default '{[' and\n * ']}', but can be changed by an optional ui parameter gap_filler_delimiters,\n * which must be a 2-element array of strings. For example:\n *\n * {\"gap_filler_delimiters\": [\"{{\", \"}}\"]}\n *\n * Note that the double-brace delimiters in that example are the same as those\n * used by Twig, so using them instead of the default would prevent you from\n * ever adding Twig expansion (e.g. for randomisation) to the question.\n *\n * @module qtype_coderunner/ui_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n /**\n * Constructor for UI. Source html comes from data-globalextra by default,\n * else from whatever source is specified by the uiParams parameter.\n * @param {string} textareaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\n function GapfillerUi(textareaId, width, height, uiParams) {\n var html;\n this.textArea = $(document.getElementById(textareaId));\n this.readOnly = this.textArea.prop('readonly');\n this.uiParams = uiParams;\n this.fail = false;\n this.htmlDiv = null;\n this.source = uiParams.ui_source || 'globalextra';\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for HTML in ui_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n html = this.textArea.attr('data-globalextra');\n } else {\n html = this.textArea.attr('data-test0');\n }\n this.html = html.replace('<', '<');\n this.reload();\n }\n\n GapfillerUi.prototype.failed = function() {\n return this.fail; // Currently always true. See reload function.\n };\n\n /**\n * Copy the serialised version of the HTML UI area to the TextArea.\n */\n GapfillerUi.prototype.sync = function() {\n var\n serialisation = [], // A list of field values.\n empty = true;\n\n this.getFields().each(function() {\n var name, value;\n name = $(this).attr('name');\n if (name !== 'cr_gapfiller_field') {\n alert('Unexpected UI element found in answer box');\n } else {\n value = $(this).val();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n });\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n GapfillerUi.prototype.getElement = function() {\n return this.htmlDiv;\n };\n\n GapfillerUi.prototype.getFields = function() {\n return $(this.htmlDiv).find('.coderunner-ui-element');\n };\n\n /**\n * Set the value of the jQuery field to the given value.\n * If the field is a radio button or a checkbox,\n * the checked attribute is set. Otherwise the field's\n * val() function is called to set the value.\n * @param {object} field The JQuery field elemetn whose value is to be set.\n * @param {string} value The value to be used.\n */\n GapfillerUi.prototype.setField = function(field, value) {\n if (field.attr('type') === 'checkbox' || field.attr('type') === 'radio') {\n field.prop('checked', field.val() === value);\n } else {\n field.val(value);\n }\n };\n\n /**\n * Process the supplied HTML, HTML-escaping existing HTML\n * and inserting the input and textarea elements\n * at the marked locations.\n */\n GapfillerUi.prototype.markedUpHtml = function() {\n\n /**\n * Prefix any regular expression special chars in s with a backslash.\n * @param {string} s The string whose special values are to be escaped.\n * @return {string} The escaped string.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n var sepLeft = reEscape('{['),\n sepRight = reEscape(']}'),\n splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+, *\\\\d+)) *' + sepRight),\n bits = this.html.split(splitter),\n result = '
' + bits[0],\n i;\n\n for (i = 1; i < bits.length; i += 2) {\n result += this.markUp(bits[i]);\n if (i + 1 < bits.length) {\n result += bits[i + 1];\n }\n }\n\n result = result + '
';\n return result;\n };\n\n\n /**\n * Return the HTML element to insert given the tag contents, which\n * should be either a single integer (size of input element) or\n * two integers separated by a comma (rows and cols of textarea).\n * @param {string} tagContents The text between the delimiters of a gap specifier.\n * @return {string} The HTML for an input or textarea element build\n * according to the given tagContents.\n */\n GapfillerUi.prototype.markUp = function(tagContents) {\n var numbers, result='';\n\n /**\n * The function to handle an 'input' tag.\n * @param {int} size The size of the input element to return.\n * @return {string} The html for a text area to the given specs.\n */\n function input(size) {\n return '';\n }\n\n /**\n * The function to handle a 'textarea' tag.\n * @param {int} rows The number of rows of text required.\n * @param {int} cols The number of columns of text required.\n * @return The HTML for a textarea to the given specs.\n */\n function textarea(rows, cols) {\n return '';\n }\n\n numbers = tagContents.split(',');\n if (numbers.length == 1) {\n result = input(parseInt(numbers[0]));\n } else {\n result = textarea(parseInt(numbers[0]), parseInt(numbers[1]));\n }\n\n return result;\n };\n\n /**\n * Reload the HTML fields from the given serialisation.\n * Unlike other plugins, we don't actually fail the load if, for example\n * the number of fields doesn't match the number of values in the\n * serialisation. We simply set any excess fields for which data\n * in unavailable to '???' or discard extra values. This ensures\n * that at least the unfilled content is presented to the question author\n * when the number of fields is altered during editing.\n */\n GapfillerUi.prototype.reload = function() {\n var\n content = $(this.textArea).val(), // JSON-encoded HTML element settings.\n value,\n values,\n i,\n fields,\n outerDiv = \"
\");\n if (content) {\n try {\n values = JSON.parse(content);\n fields = this.getFields();\n for (i = 0; i < fields.length; i++) {\n value = i < values.length ? values[i] : '???';\n this.setField($(fields[i]), value);\n }\n } catch(e) {\n /**\n * Just ignore errors\n */\n }\n }\n };\n\n GapfillerUi.prototype.resize = function() {}; // Nothing to see here. Move along please.\n\n GapfillerUi.prototype.hasFocus = function() {\n var focused = false;\n this.getFields().each(function() {\n if (this === document.activeElement) {\n focused = true;\n }\n });\n return focused;\n };\n\n /**\n * Destroy the GapFiller UI and serialise the result into the original text area.\n */\n GapfillerUi.prototype.destroy = function() {\n this.sync();\n $(this.htmlDiv).remove();\n this.htmlDiv = null;\n };\n\n return {\n Constructor: GapfillerUi\n };\n});\n"],"names":["define","$","GapfillerUi","textareaId","width","height","uiParams","html","textArea","document","getElementById","readOnly","this","prop","fail","htmlDiv","source","ui_source","alert","attr","replace","reload","prototype","failed","sync","serialisation","empty","getFields","each","value","val","push","JSON","stringify","getElement","find","setField","field","markedUpHtml","reEscape","s","c","result","i","length","j","sepLeft","sepRight","splitter","RegExp","bits","split","markUp","tagContents","numbers","rows","cols","parseInt","values","fields","content","parse","e","resize","hasFocus","focused","activeElement","destroy","remove","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_graph.min.js.map b/amd/build/ui_graph.min.js.map
deleted file mode 100644
index 707291cfd..000000000
--- a/amd/build/ui_graph.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"ui_graph.min.js","sources":["../src/ui_graph.js"],"sourcesContent":["/**\n * This file is part of Moodle - http:moodle.org/\n *\n * Much of this code is from Finite State Machine Designer:\n */\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n/**\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 util.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/**\n * JavaScript to interface to the Graph editor, which is used both in\n * the author editing page and by the student question submission page.\n *\n * @module qtype_coderunner/ui_graph\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/graphutil', 'qtype_coderunner/graphelements'], function($, util, elements) {\n\n /**\n * Constructor for a GraphCanvas object, which is a wrapper for a Graph's HTML canvas\n * object.\n * @param {object} parent The Graph that owns this object.\n * @param {string} canvasId The ID of the HTML canvas to be wrapped by this object.\n * @param {int} w The required width of the wrapper.\n * @param {int} h The required height of the wrapper.\n */\n function GraphCanvas(parent, canvasId, w, h) {\n this.HANDLE_SIZE = 10;\n\n this.parent = parent;\n this.canvas = $(document.createElement(\"canvas\"));\n this.canvas.attr({\n id: canvasId,\n class: \"coderunner_graphcanvas\",\n tabindex: 1 // So canvas can get focus.\n });\n this.canvas.css({'background-color': 'white'});\n\n this.canvas.on('mousedown', function(e) {\n return parent.mousedown(e);\n });\n\n this.canvas.on('mouseup', function(e) {\n return parent.mouseup(e);\n });\n\n this.canvas.on('dblclick', function(e) {\n return parent.dblclick(e);\n });\n\n this.canvas.on('keydown', function(e) {\n return parent.keydown(e);\n });\n\n this.canvas.on('mousemove', function(e) {\n return parent.mousemove(e);\n });\n\n this.canvas.on('keypress', function(e) {\n return parent.keypress(e);\n });\n\n /**\n * Resize this object to then given dimensions.\n * @param {int} w Required width.\n * @param {int} h Required height.\n */\n this.resize = function(w, h) {\n this.canvas.attr(\"width\", w);\n this.canvas.attr(\"height\", h);\n };\n\n this.resize(w, h);\n }\n\n /**\n * Constructor for the Graph object.\n * This is the ui component for a graph-drawing coderunner question.\n *\n * Relevant ui parameters:\n *\n * isfsm. True if the graph is of a Finite State Machine.\n * If true, the graph can contain an incoming edge from nowhere\n * (the start edge). Default: true.\n * isdirected. True if edges are directed. Default: true.\n * noderadius. The radius of a node, in pixels. Default: 26.\n * fontsize. The font size used for node and edge labels. Default: 20 points.\n * textoffset. An offset in pixels used to determine how far from the link\n * a label is initially positioned. Default 5. Largely defunct\n * now that link text can be dragged.\n * helpmenutext. A string to be used in lieu of the default Help info, if supplied.\n * No default.\n * locknodepositions. True to prevent the user from moving nodes. Useful when the\n * answer box is preloaded with a graph that the student has to\n * annotate by changing node or edge labels or by\n * adding/removing edges. Note, though that nodes can still be\n * added and deleted. See locknodeset.\n * locknodeset. True to prevent the user from adding or deleting nodes, or\n * toggling node types to/from acceptors.\n * locknodelabels: True to prevent the user from editing node labels. This\n * will also prevent any new nodes having non-empty labels.\n * lockedgepositions. True to prevent the user from dragging edges to change\n * their curvature. Possibly useful if the answer box is\n * preloaded with a graph that the student has to annotate by\n * changing node or edge labels or by adding/removing edges.\n * Also ensures that edges added by a student are straight, e.g.\n * to draw a polygon on a set of given points. Note, though that\n * edges can still be added and deleted. See lockedgeset.\n * lockedgeset. True to prevent the user from adding or deleting edges.\n * lockedgelabels: True to prevent the user from editing edge labels. This\n * also prevents any new edges from having labels.\n * @param {string} textareaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\n function Graph(textareaId, width, height, uiParams) {\n /**\n * Constructor.\n */\n var save_this = this;\n\n this.SNAP_TO_PADDING = 6;\n this.DUPLICATE_LINK_OFFSET = 16; // Pixels offset for a duplicate link\n this.HIT_TARGET_PADDING = 6; // Pixels.\n this.DEFAULT_NODE_RADIUS = 26; // Pixels. UI parameter noderadius can override this.\n this.DEFAULT_FONT_SIZE = 20; // px. UI parameter fontsize can override this.\n this.DEFAULT_TEXT_OFFSET = 5; // Link label tweak. UI params can override.\n this.DEFAULT_LINK_LABEL_REL_DIST = 0.5; // Relative distance along link to place labels\n this.MAX_VERSIONS = 30; // Maximum number of versions saved for undo/redo\n\n this.canvasId = 'graphcanvas_' + textareaId;\n this.textArea = $(document.getElementById(textareaId));\n this.helpText = ''; // Obtained by JSON - see below.\n this.readOnly = this.textArea.prop('readonly');\n this.uiParams = uiParams;\n this.graphCanvas = new GraphCanvas(this, this.canvasId, width, height);\n this.caretVisible = true;\n this.caretTimer = 0; // Need global so we can kill a running timer.\n this.originalClick = null;\n this.nodes = [];\n this.links = [];\n this.selectedObject = null; // Either a elements.Link or a elements.Node or a elements.Button.\n this.currentLink = null;\n this.movingObject = false;\n this.fail = false; // Will be set true if reload fails (can't deserialise).\n this.failString = null; // Language string key for fail error message.\n this.versions = [];\n this.versionIndex = -1; //Index of current state of graph in versions list\n\n this.helpBox = new elements.HelpBox(this, 0, 0); // Button that opens a help text box\n this.clearButton = new elements.Button(this, 60, 0, \"Clear\"); // Button that clears the canvas\n this.clearButton.onClick = function() {\n if (confirm(\"Are you sure you want to clear the diagram?\")) {\n this.parent.clear();\n }\n };\n this.buttons = [this.helpBox, this.clearButton];\n\n /**\n * Legacy support for locknodes and lockedges.\n */\n if ('locknodes' in uiParams) {\n uiParams.locknodepositions = uiParams.locknodes;\n }\n if ('lockedges' in uiParams) {\n uiParams.lockedgepositions = uiParams.lockedges;\n }\n\n if ('helpmenutext' in uiParams) {\n this.helpText = uiParams.helpmenutext;\n } else {\n require(['core/str'], function(str) {\n /**\n * Get help text via AJAX.\n */\n var helpPresent = str.get_string('graphhelp', 'qtype_coderunner');\n $.when(helpPresent).done(function(graphhelp) {\n save_this.helpText = graphhelp;\n });\n });\n }\n this.reload();\n if (!this.fail) {\n this.draw();\n }\n }\n\n Graph.prototype.failed = function() {\n return this.fail;\n };\n\n Graph.prototype.failMessage = function() {\n return this.failString;\n };\n\n Graph.prototype.getElement = function() {\n return this.getCanvas();\n };\n\n Graph.prototype.hasFocus = function() {\n return document.activeElement == this.getCanvas();\n };\n\n Graph.prototype.getCanvas = function() {\n var canvas = this.graphCanvas.canvas[0];\n return canvas;\n };\n\n Graph.prototype.nodeRadius = function() {\n return this.uiParams.noderadius ? this.uiParams.noderadius : this.DEFAULT_NODE_RADIUS;\n };\n\n Graph.prototype.fontSize = function() {\n return this.uiParams.fontsize ? this.uiParams.fontsize : this.DEFAULT_FONT_SIZE;\n };\n\n Graph.prototype.isFsm = function() {\n return this.uiParams.isfsm !== undefined ? this.uiParams.isfsm : true;\n };\n\n\n Graph.prototype.textOffset = function() {\n return this.uiParams.textoffset ? this.uiParams.textoffset : this.DEFAULT_TEXT_OFFSET;\n };\n\n /**\n * Draw an arrow head if this is a directed graph. Otherwise do nothing.\n * @param {object} c The graphic context.\n * @param {int} x The x location of the arrow head.\n * @param {int} y The y location of the arrow head.\n * @param {float} angle The angle of the arrow.\n */\n Graph.prototype.arrowIfReqd = function(c, x, y, angle) {\n if (this.uiParams.isdirected === undefined || this.uiParams.isdirected) {\n util.drawArrow(c, x, y, angle);\n }\n };\n\n /**\n * Copy the serialised version of the graph to the TextArea.\n */\n Graph.prototype.sync = function() {\n /**\n * Nothing to do ... always sync'd.\n */\n };\n\n /**\n * Disable autosync, too.\n */\n Graph.prototype.syncIntervalSecs = function() {\n return 0;\n };\n\n Graph.prototype.keypress = function(e) {\n var key = util.crossBrowserKey(e);\n\n if (this.readOnly) {\n return;\n }\n\n if(key >= 0x20 &&\n key <= 0x7E &&\n !e.metaKey &&\n !e.altKey &&\n !e.ctrlKey &&\n key !== 37 && //Don't register arrow keys\n key !== 39 &&\n this.selectedObject !== null &&\n this.canEditText()) {\n if (this.selectedObject.justMoved) {\n this.saveVersion();\n }\n this.selectedObject.justMoved = false;\n this.selectedObject.textBox.insertChar(String.fromCharCode(key));\n this.resetCaret();\n this.draw();\n\n /**\n * Don't let keys do their actions (like space scrolls down the page).\n */\n return false;\n } else if(key === 8 || key === 0x20 || key === 9) {\n /**\n * Disable scrolling on backspace, tab and space.\n */\n return false;\n }\n };\n\n Graph.prototype.mousedown = function(e) {\n var mouse = util.crossBrowserRelativeMousePos(e);\n\n if (this.readOnly) {\n return;\n }\n\n this.selectedObject = this.selectObject(mouse.x, mouse.y);\n this.movingObject = false;\n this.movingGraph = false;\n this.movingText = false;\n this.originalClick = mouse;\n\n this.saveVersion();\n\n if (this.selectedObject !== this.helpBox){\n this.helpBox.helpOpen = false;\n }\n\n if(this.selectedObject !== null) {\n if(this.selectedObject instanceof elements.Button){\n this.selectedObject.onClick();\n } else if(e.shiftKey && this.selectedObject instanceof elements.Node) {\n if (!this.uiParams.lockedgeset) {\n this.currentLink = new elements.SelfLink(this, this.selectedObject, mouse);\n }\n } else if (e.altKey && this.selectedObject instanceof elements.Node) {\n /**\n * Moving an entire connected graph component.\n */\n if (!this.uiParams.locknodepositions) {\n this.movingGraph = true;\n this.movingNodes = this.selectedObject.traverseGraph(this.links, []);\n for (var i = 0; i < this.movingNodes.length; i++) {\n this.movingNodes[i].setMouseStart(mouse.x, mouse.y);\n }\n }\n } else if (this.selectedObject instanceof elements.TextBox){\n if (!this.uiParams.lockedgelabels) {\n this.movingText = true;\n this.selectedObject.setMouseStart(mouse.x, mouse.y);\n this.selectedObject = this.selectedObject.parent;\n }\n } else if (!(this.uiParams.locknodepositions && this.selectedObject instanceof elements.Node) &&\n !(this.uiParams.lockedgepositions && this.selectedObject instanceof elements.Link)){\n this.movingObject = true;\n if(this.selectedObject.setMouseStart) {\n this.selectedObject.setMouseStart(mouse.x, mouse.y);\n }\n }\n this.selectedObject.justMoved = true;\n this.resetCaret();\n } else if(e.shiftKey && this.isFsm()) {\n this.currentLink = new elements.TemporaryLink(this, mouse, mouse);\n }\n\n this.draw();\n\n if(this.hasFocus()) {\n /**\n * Disable drag-and-drop only if the canvas is already focused.\n */\n return false;\n } else {\n /**\n * Otherwise, let the browser switch the focus away from wherever it was.\n */\n this.resetCaret();\n return true;\n }\n };\n\n /**\n * Return true if currently selected object has text that we are allowed\n * to edit.\n */\n Graph.prototype.canEditText = function() {\n var isNode = this.selectedObject instanceof elements.Node,\n isLink = (this.selectedObject instanceof elements.Link ||\n this.selectedObject instanceof elements.SelfLink);\n return 'textBox' in this.selectedObject &&\n ((isNode && !this.uiParams.locknodelabels) ||\n (isLink && !this.uiParams.lockedgelabels));\n };\n\n Graph.prototype.keydown = function(e) {\n var key = util.crossBrowserKey(e), i, nodeDeleted=false;\n\n if (this.readOnly) {\n return;\n }\n\n if(key === 8) { // Backspace key.\n if(this.selectedObject !== null && this.canEditText()) {\n this.selectedObject.textBox.deleteChar();\n this.resetCaret();\n this.draw();\n }\n /**\n * Backspace is a shortcut for the back button, but do NOT want to change pages.\n */\n return false;\n } else if(key === 46 && this.selectedObject !== null) { // Delete key\n this.saveVersion();\n for (i = 0; i < this.nodes.length; i++) {\n if (this.nodes[i] === this.selectedObject && !this.uiParams.locknodeset) {\n this.nodes.splice(i--, 1);\n nodeDeleted = true;\n }\n }\n for (i = 0; i < this.links.length; i++) {\n if((this.links[i] === this.selectedObject && !this.uiParams.lockedgeset) ||\n nodeDeleted && (\n this.links[i].node === this.selectedObject ||\n this.links[i].nodeA === this.selectedObject ||\n this.links[i].nodeB === this.selectedObject)) {\n this.links.splice(i--, 1);\n }\n }\n this.selectedObject = null;\n this.draw();\n } else if(key === 13) { // Enter key.\n if(this.selectedObject !== null) {\n /**\n * Deselect the object.\n */\n this.selectedObject = null;\n this.draw();\n }\n } else if(key === 37) { // Left arrow key\n if(this.selectedObject !== null && this.canEditText()) {\n this.selectedObject.textBox.caretLeft();\n this.resetCaret();\n this.draw();\n }\n } else if(key === 39) { // Right arrow key\n if(this.selectedObject !== null && this.canEditText()) {\n this.selectedObject.textBox.caretRight();\n this.resetCaret();\n this.draw();\n }\n } else if ((e.keyCode == 90 && e.ctrlKey && e.shiftKey) || (e.keyCode == 89 && e.ctrlKey)) { //CTRL+SHIFT+z or CTRL+y\n this.redo();\n } else if (e.keyCode == 90 && e.ctrlKey) { //CTRL+z\n this.undo();\n }\n };\n\n Graph.prototype.dblclick = function(e) {\n var mouse = util.crossBrowserRelativeMousePos(e);\n\n if (this.readOnly || this.uiParams.locknodeset) {\n return;\n }\n\n this.selectedObject = this.selectObject(mouse.x, mouse.y);\n\n this.saveVersion();\n\n if(this.selectedObject === null) {\n this.selectedObject = new elements.Node(this, mouse.x, mouse.y);\n this.nodes.push(this.selectedObject);\n this.selectedObject.justMoved = true;\n this.resetCaret();\n this.draw();\n } else {\n if(this.selectedObject instanceof elements.Node && this.isFsm()) {\n this.selectedObject.isAcceptState = !this.selectedObject.isAcceptState;\n this.draw();\n }\n }\n };\n\n Graph.prototype.resize = function(w, h) {\n this.graphCanvas.resize(w, h);\n this.draw();\n };\n\n Graph.prototype.mousemove = function(e) {\n var mouse = util.crossBrowserRelativeMousePos(e),\n closestPoint;\n\n if (this.readOnly) {\n return;\n }\n\n for (i = 0; i < this.buttons.length; i++){\n if (this.buttons[i].containsPoint(mouse.x, mouse.y)){\n this.buttons[i].highLighted = true;\n }else{\n this.buttons[i].highLighted = false;\n }\n this.draw();\n }\n\n if(this.currentLink !== null) {\n var targetNode = this.selectObject(mouse.x, mouse.y);\n if(!(targetNode instanceof elements.Node)) {\n targetNode = null;\n }\n\n if(this.selectedObject === null) {\n if(targetNode !== null) {\n this.currentLink = new elements.StartLink(this, targetNode, this.originalClick);\n } else {\n this.currentLink = new elements.TemporaryLink(this, this.originalClick, mouse);\n }\n } else {\n if(targetNode === this.selectedObject) {\n this.currentLink = new elements.SelfLink(this, this.selectedObject, mouse);\n } else if(targetNode !== null) {\n this.currentLink = new elements.Link(this, this.selectedObject, targetNode);\n } else {\n closestPoint = this.selectedObject.closestPointOnCircle(mouse.x, mouse.y);\n this.currentLink = new elements.TemporaryLink(this, closestPoint, mouse);\n }\n }\n this.draw();\n }\n if (this.movingGraph) {\n var nodes = this.movingNodes;\n for (var i = 0; i < nodes.length; i++) {\n nodes[i].trackMouse(mouse.x, mouse.y);\n this.snapNode(nodes[i]);\n }\n this.draw();\n } else if(this.movingText){\n this.selectedObject.textBox.setAnchorPoint(mouse.x, mouse.y);\n this.draw();\n } else if(this.movingObject) {\n this.selectedObject.setAnchorPoint(mouse.x, mouse.y);\n if(this.selectedObject instanceof elements.Node) {\n this.snapNode(this.selectedObject);\n }\n this.draw();\n }\n };\n\n Graph.prototype.mouseup = function() {\n\n if (this.readOnly) {\n return;\n }\n\n this.movingObject = false;\n this.movingGraph = false;\n this.movingText = false;\n\n if(this.currentLink !== null) {\n if(!(this.currentLink instanceof elements.TemporaryLink)) {\n this.selectedObject = this.currentLink;\n this.addLink(this.currentLink);\n this.resetCaret();\n }\n this.currentLink = null;\n this.draw();\n }\n };\n\n Graph.prototype.selectObject = function(x, y) {\n for (i = 0; i < this.buttons.length; i++){\n if (this.buttons[i].containsPoint(x, y)){\n return this.buttons[i];\n }\n }\n var i;\n for(i = 0; i < this.nodes.length; i++) {\n if(this.nodes[i].containsPoint(x, y)) {\n return this.nodes[i];\n }\n }\n for(i = 0; i < this.links.length; i++) {\n if(this.links[i].containsPoint(x, y)) {\n return this.links[i];\n }else if ('textBox' in this.links[i] && this.links[i].textBox.containsPoint(x, y)){\n return this.links[i].textBox;\n }\n }\n return null;\n };\n\n Graph.prototype.snapNode = function(node) {\n for(var i = 0; i < this.nodes.length; i++) {\n if(this.nodes[i] === node){\n continue;\n }\n\n if(Math.abs(node.x - this.nodes[i].x) < this.SNAP_TO_PADDING) {\n node.x = this.nodes[i].x;\n }\n\n if(Math.abs(node.y - this.nodes[i].y) < this.SNAP_TO_PADDING) {\n node.y = this.nodes[i].y;\n }\n }\n };\n\n /**\n * Add a new link (always 'this.currentLink') to the set of links.\n * If the link connects two nodes already linked, the angle of the new link\n * is tweaked so it is distinguishable from the existing links.\n * @param {object} newLink The link to be added.\n */\n Graph.prototype.addLink = function(newLink) {\n var maxPerpRHS = null;\n for (var i = 0; i < this.links.length; i++) {\n var link = this.links[i];\n if (link.nodeA === newLink.nodeA && link.nodeB === newLink.nodeB) {\n if (maxPerpRHS === null || link.perpendicularPart > maxPerpRHS) {\n maxPerpRHS = link.perpendicularPart;\n }\n }\n if (link.nodeA === newLink.nodeB && link.nodeB === newLink.nodeA) {\n if (maxPerpRHS === null || -link.perpendicularPart > maxPerpRHS ) {\n maxPerpRHS = -link.perpendicularPart;\n }\n }\n }\n if (maxPerpRHS !== null) {\n newLink.perpendicularPart = maxPerpRHS + this.DUPLICATE_LINK_OFFSET;\n }\n this.links.push(newLink);\n };\n\n Graph.prototype.reload = function() {\n var content = $(this.textArea).val();\n if (content) {\n try {\n /**\n * Load up the student's previous answer if non-empty.\n */\n var backup = JSON.parse(content), i;\n\n for(i = 0; i < backup.nodes.length; i++) {\n var backupNode = backup.nodes[i];\n var backupNodeLayout = backup.nodeGeometry[i];\n var node = new elements.Node(this, backupNodeLayout[0], backupNodeLayout[1]);\n node.isAcceptState = backupNode[1];\n node.textBox = new elements.TextBox(backupNode[0].toString(), node);\n this.nodes.push(node);\n }\n\n for(i = 0; i < backup.edges.length; i++) {\n var backupLink = backup.edges[i];\n var backupLinkLayout = backup.edgeGeometry[i];\n var link = null;\n if(backupLink[0] === backupLink[1]) {\n /**\n * Self link has two identical nodes.\n */\n link = new elements.SelfLink(this, this.nodes[backupLink[0]]);\n link.anchorAngle = backupLinkLayout.anchorAngle;\n link.textBox = new elements.TextBox(backupLink[2].toString(), link);\n if (backupLink.length > 3) {\n link.textBox.setAnchorPoint(backupLink[3].x, backupLink[3].y);\n }\n } else if(backupLink[0] === -1) {\n link = new elements.StartLink(this, this.nodes[backupLink[1]]);\n link.deltaX = backupLinkLayout.deltaX;\n link.deltaY = backupLinkLayout.deltaY;\n } else {\n link = new elements.Link(this, this.nodes[backupLink[0]], this.nodes[backupLink[1]]);\n link.parallelPart = backupLinkLayout.parallelPart;\n link.perpendicularPart = backupLinkLayout.perpendicularPart;\n link.lineAngleAdjust = backupLinkLayout.lineAngleAdjust;\n link.textBox = new elements.TextBox(backupLink[2].toString(), link);\n if (backupLink.length > 3) {\n link.textBox.setAnchorPoint(backupLink[3].x, backupLink[3].y);\n }\n }\n if(link !== null) {\n this.links.push(link);\n }\n }\n } catch(e) {\n this.fail = true;\n this.failString = 'graph_ui_invalidserialisation';\n }\n }\n };\n\n Graph.prototype.save = function() {\n\n var backup = {\n 'edgeGeometry': [],\n 'nodeGeometry': [],\n 'nodes': [],\n 'edges': [],\n };\n var i;\n\n if(!JSON || (this.textArea.val().trim() === '' && this.nodes.length === 0)) {\n return; // Don't save if we have an empty textbox and no graphic content.\n }\n\n for(i = 0; i < this.nodes.length; i++) {\n var node = this.nodes[i];\n\n var nodeData = [node.textBox.text, node.isAcceptState];\n var nodeLayout = [node.x, node.y];\n\n backup.nodeGeometry.push(nodeLayout);\n backup.nodes.push(nodeData);\n }\n\n for(i = 0; i < this.links.length; i++) {\n var link = this.links[i],\n linkData = null,\n linkLayout = null;\n\n if(link instanceof elements.SelfLink) {\n linkLayout = {\n 'anchorAngle': link.anchorAngle,\n };\n linkData = [this.nodes.indexOf(link.node), this.nodes.indexOf(link.node), link.textBox.text];\n if (link.textBox.dragged) {\n linkData.push(link.textBox.position);\n }\n } else if(link instanceof elements.StartLink) {\n linkLayout = {\n 'deltaX': link.deltaX,\n 'deltaY': link.deltaY\n };\n linkData = [-1, this.nodes.indexOf(link.node), \"\"];\n } else if(link instanceof elements.Link) {\n linkLayout = {\n 'lineAngleAdjust': link.lineAngleAdjust,\n 'parallelPart': link.parallelPart,\n 'perpendicularPart': link.perpendicularPart,\n };\n linkData = [this.nodes.indexOf(link.nodeA), this.nodes.indexOf(link.nodeB), link.textBox.text];\n if (link.textBox.dragged) {\n linkData.push(link.textBox.position);\n }\n }\n if (linkData !== null && linkLayout !== null) {\n backup.edges.push(linkData);\n backup.edgeGeometry.push(linkLayout);\n }\n }\n this.textArea.val(JSON.stringify(backup));\n };\n\n Graph.prototype.saveVersion = function () {\n var curState = this.textArea.val();\n if (this.versions.length == 0 || curState.localeCompare(this.versions[this.versionIndex]) != 0){\n this.versionIndex++;\n while (this.versionIndex < this.versions.length){ //Clear newer versions that have been overwritten by this save\n this.versions.pop();\n }\n this.versions.push(curState);\n if (this.versions.length > this.MAX_VERSIONS){ //Limit the size of this.versions\n this.versions.shift();\n this.versionIndex--;\n }\n }\n };\n\n Graph.prototype.undo = function () {\n this.saveVersion();\n if (this.versionIndex > 0){\n this.versionIndex--;\n this.textArea.val(this.versions[this.versionIndex]);\n /**\n * Clear graph nodes and links\n */\n this.nodes = [];\n this.links = [];\n /**\n * Reload graph from serialisation\n */\n this.reload();\n this.draw();\n }\n };\n\n Graph.prototype.redo = function() {\n if (this.versionIndex < this.versions.length - 1){\n this.versionIndex++;\n this.textArea.val(this.versions[this.versionIndex]);\n /**\n * Clear graph nodes and links\n */\n this.nodes = [];\n this.links = [];\n /**\n * Reload graph from serialisation\n */\n this.reload();\n this.draw();\n }\n };\n\n Graph.prototype.clear = function () {\n this.saveVersion();\n this.nodes = [];\n this.links = [];\n this.save();\n this.draw();\n };\n\n Graph.prototype.destroy = function () {\n clearInterval(this.caretTimer); // Stop the caret timer.\n this.graphCanvas.canvas.off(); // Stop all events.\n this.graphCanvas.canvas.remove();\n };\n\n Graph.prototype.resetCaret = function () {\n var t = this; // For embedded function to access this.\n\n clearInterval(this.caretTimer);\n this.caretTimer = setInterval(function() {\n t.caretVisible = !t.caretVisible;\n t.draw();\n }, 500);\n this.caretVisible = true;\n };\n\n Graph.prototype.draw = function () {\n var canvas = this.getCanvas(),\n c = canvas.getContext('2d'),\n i;\n\n c.clearRect(0, 0, this.getCanvas().width, this.getCanvas().height);\n c.save();\n c.translate(0.5, 0.5);\n\n for (i = 0; i < this.buttons.length; i++){\n this.buttons[i].draw(c);\n }\n\n if (!this.helpBox.helpOpen) { // Only proceed if help info not showing.\n\n for(i = 0; i < this.nodes.length; i++) {\n c.lineWidth = 1;\n c.fillStyle = c.strokeStyle = (this.nodes[i] === this.selectedObject) ? 'blue' : 'black';\n this.nodes[i].draw(c);\n }\n for(i = 0; i < this.links.length; i++) {\n c.lineWidth = 1;\n c.fillStyle = c.strokeStyle = (this.links[i] === this.selectedObject\n || this.links[i].textBox === this.selectedObject) ? 'blue' : 'black';\n this.links[i].draw(c);\n }\n if(this.currentLink !== null) {\n c.lineWidth = 1;\n c.fillStyle = c.strokeStyle = 'black';\n this.currentLink.draw(c);\n }\n }\n\n c.restore();\n this.save();\n };\n\n return {\n Constructor: Graph\n };\n});\n"],"names":["define","$","util","elements","GraphCanvas","parent","canvasId","w","h","HANDLE_SIZE","canvas","document","createElement","attr","id","class","tabindex","css","on","e","mousedown","mouseup","dblclick","keydown","mousemove","keypress","resize","Graph","textareaId","width","height","uiParams","save_this","this","SNAP_TO_PADDING","DUPLICATE_LINK_OFFSET","HIT_TARGET_PADDING","DEFAULT_NODE_RADIUS","DEFAULT_FONT_SIZE","DEFAULT_TEXT_OFFSET","DEFAULT_LINK_LABEL_REL_DIST","MAX_VERSIONS","textArea","getElementById","helpText","readOnly","prop","graphCanvas","caretVisible","caretTimer","originalClick","nodes","links","selectedObject","currentLink","movingObject","fail","failString","versions","versionIndex","helpBox","HelpBox","clearButton","Button","onClick","confirm","clear","buttons","locknodepositions","locknodes","lockedgepositions","lockedges","helpmenutext","require","str","helpPresent","get_string","when","done","graphhelp","reload","draw","prototype","failed","failMessage","getElement","getCanvas","hasFocus","activeElement","nodeRadius","noderadius","fontSize","fontsize","isFsm","undefined","isfsm","textOffset","textoffset","arrowIfReqd","c","x","y","angle","isdirected","drawArrow","sync","syncIntervalSecs","key","crossBrowserKey","metaKey","altKey","ctrlKey","canEditText","justMoved","saveVersion","textBox","insertChar","String","fromCharCode","resetCaret","mouse","crossBrowserRelativeMousePos","selectObject","movingGraph","movingText","helpOpen","shiftKey","Node","lockedgeset","SelfLink","movingNodes","traverseGraph","i","length","setMouseStart","TextBox","lockedgelabels","Link","TemporaryLink","isNode","isLink","locknodelabels","nodeDeleted","deleteChar","locknodeset","splice","node","nodeA","nodeB","caretLeft","caretRight","keyCode","redo","undo","push","isAcceptState","closestPoint","containsPoint","highLighted","targetNode","StartLink","closestPointOnCircle","trackMouse","snapNode","setAnchorPoint","addLink","Math","abs","newLink","maxPerpRHS","link","perpendicularPart","content","val","backup","JSON","parse","backupNode","backupNodeLayout","nodeGeometry","toString","edges","backupLink","backupLinkLayout","edgeGeometry","anchorAngle","deltaX","deltaY","parallelPart","lineAngleAdjust","save","trim","nodeData","text","nodeLayout","linkData","linkLayout","indexOf","dragged","position","stringify","curState","localeCompare","pop","shift","destroy","clearInterval","off","remove","t","setInterval","getContext","clearRect","translate","lineWidth","fillStyle","strokeStyle","restore","Constructor"],"mappings":";;;;;;;;AAqDAA,mCAAO,CAAC,SAAU,6BAA8B,mCAAmC,SAASC,EAAGC,KAAMC,mBAUxFC,YAAYC,OAAQC,SAAUC,EAAGC,QACjCC,YAAc,QAEdJ,OAASA,YACTK,OAAST,EAAEU,SAASC,cAAc,gBAClCF,OAAOG,KAAK,CACbC,GAAYR,SACZS,MAAY,yBACZC,SAAY,SAEXN,OAAOO,IAAI,oBAAqB,eAEhCP,OAAOQ,GAAG,aAAa,SAASC,UAC1Bd,OAAOe,UAAUD,WAGvBT,OAAOQ,GAAG,WAAW,SAASC,UACxBd,OAAOgB,QAAQF,WAGrBT,OAAOQ,GAAG,YAAY,SAASC,UACzBd,OAAOiB,SAASH,WAGtBT,OAAOQ,GAAG,WAAW,SAASC,UACxBd,OAAOkB,QAAQJ,WAGrBT,OAAOQ,GAAG,aAAa,SAASC,UAC1Bd,OAAOmB,UAAUL,WAGvBT,OAAOQ,GAAG,YAAY,SAASC,UACzBd,OAAOoB,SAASN,WAQtBO,OAAS,SAASnB,EAAGC,QACjBE,OAAOG,KAAK,QAASN,QACrBG,OAAOG,KAAK,SAAUL,SAG1BkB,OAAOnB,EAAGC,YA4CVmB,MAAMC,WAAYC,MAAOC,OAAQC,cAIlCC,UAAYC,UAEXC,gBAAkB,OAClBC,sBAAwB,QACxBC,mBAAqB,OACrBC,oBAAsB,QACtBC,kBAAoB,QACpBC,oBAAsB,OACtBC,4BAA8B,QAC9BC,aAAe,QAEfnC,SAAW,eAAiBsB,gBAC5Bc,SAAWzC,EAAEU,SAASgC,eAAef,kBACrCgB,SAAW,QACXC,SAAWZ,KAAKS,SAASI,KAAK,iBAC9Bf,SAAWA,cACXgB,YAAc,IAAI3C,YAAY6B,KAAOA,KAAK3B,SAAUuB,MAAOC,aAC3DkB,cAAe,OACfC,WAAa,OACbC,cAAgB,UAChBC,MAAQ,QACRC,MAAQ,QACRC,eAAiB,UACjBC,YAAc,UACdC,cAAe,OACfC,MAAO,OACPC,WAAa,UACbC,SAAW,QACXC,cAAgB,OAEhBC,QAAU,IAAIzD,SAAS0D,QAAQ5B,KAAM,EAAG,QACxC6B,YAAc,IAAI3D,SAAS4D,OAAO9B,KAAM,GAAI,EAAG,cAC/C6B,YAAYE,QAAU,WACrBC,QAAQ,qDACH5D,OAAO6D,cAGbC,QAAU,CAAClC,KAAK2B,QAAS3B,KAAK6B,aAK/B,cAAe/B,WACfA,SAASqC,kBAAoBrC,SAASsC,WAEtC,cAAetC,WACfA,SAASuC,kBAAoBvC,SAASwC,WAGtC,iBAAkBxC,cACba,SAAWb,SAASyC,aAE3BC,QAAQ,CAAC,aAAa,SAASC,SAIrBC,YAAcD,IAAIE,WAAW,YAAa,oBAC9C3E,EAAE4E,KAAKF,aAAaG,MAAK,SAASC,WAC9B/C,UAAUY,SAAWmC,qBAI5BC,SACA/C,KAAKuB,WACDyB,cAIbtD,MAAMuD,UAAUC,OAAS,kBACdlD,KAAKuB,MAGhB7B,MAAMuD,UAAUE,YAAc,kBACnBnD,KAAKwB,YAGhB9B,MAAMuD,UAAUG,WAAa,kBAClBpD,KAAKqD,aAGhB3D,MAAMuD,UAAUK,SAAW,kBAChB5E,SAAS6E,eAAiBvD,KAAKqD,aAG1C3D,MAAMuD,UAAUI,UAAY,kBACXrD,KAAKc,YAAYrC,OAAO,IAIzCiB,MAAMuD,UAAUO,WAAa,kBAClBxD,KAAKF,SAAS2D,WAAazD,KAAKF,SAAS2D,WAAazD,KAAKI,qBAGtEV,MAAMuD,UAAUS,SAAW,kBAChB1D,KAAKF,SAAS6D,SAAW3D,KAAKF,SAAS6D,SAAW3D,KAAKK,mBAGlEX,MAAMuD,UAAUW,MAAQ,uBACWC,IAAxB7D,KAAKF,SAASgE,OAAsB9D,KAAKF,SAASgE,OAI7DpE,MAAMuD,UAAUc,WAAa,kBAClB/D,KAAKF,SAASkE,WAAahE,KAAKF,SAASkE,WAAahE,KAAKM,qBAUtEZ,MAAMuD,UAAUgB,YAAc,SAASC,EAAGC,EAAGC,EAAGC,aACXR,IAA7B7D,KAAKF,SAASwE,YAA4BtE,KAAKF,SAASwE,aACxDrG,KAAKsG,UAAUL,EAAGC,EAAGC,EAAGC,QAOhC3E,MAAMuD,UAAUuB,KAAO,aASvB9E,MAAMuD,UAAUwB,iBAAmB,kBACxB,GAGX/E,MAAMuD,UAAUzD,SAAW,SAASN,OAC5BwF,IAAMzG,KAAK0G,gBAAgBzF,OAE3Bc,KAAKY,gBAIN8D,KAAO,IACAA,KAAO,MACNxF,EAAE0F,UACF1F,EAAE2F,SACF3F,EAAE4F,SACK,KAARJ,KACQ,KAARA,KACwB,OAAxB1E,KAAKoB,gBACLpB,KAAK+E,eACP/E,KAAKoB,eAAe4D,gBACfC,mBAEJ7D,eAAe4D,WAAY,OAC3B5D,eAAe8D,QAAQC,WAAWC,OAAOC,aAAaX,WACtDY,kBACAtC,QAKE,GACO,IAAR0B,KAAqB,KAARA,KAAwB,IAARA,UAAhC,GAQXhF,MAAMuD,UAAU9D,UAAY,SAASD,OAC7BqG,MAAQtH,KAAKuH,6BAA6BtG,OAE1Cc,KAAKY,kBAIJQ,eAAiBpB,KAAKyF,aAAaF,MAAMpB,EAAGoB,MAAMnB,QAClD9C,cAAe,OACfoE,aAAc,OACdC,YAAa,OACb1E,cAAgBsE,WAEhBN,cAEDjF,KAAKoB,iBAAmBpB,KAAK2B,eACxBA,QAAQiE,UAAW,GAGD,OAAxB5F,KAAKoB,eAAyB,IAC1BpB,KAAKoB,0BAA0BlD,SAAS4D,YACnCV,eAAeW,eACjB,GAAG7C,EAAE2G,UAAY7F,KAAKoB,0BAA0BlD,SAAS4H,KACtD9F,KAAKF,SAASiG,mBACV1E,YAAc,IAAInD,SAAS8H,SAAShG,KAAMA,KAAKoB,eAAgBmE,aAErE,GAAIrG,EAAE2F,QAAU7E,KAAKoB,0BAA0BlD,SAAS4H,UAItD9F,KAAKF,SAASqC,kBAAmB,MAC7BuD,aAAc,OACdO,YAAcjG,KAAKoB,eAAe8E,cAAclG,KAAKmB,MAAO,QAC5D,IAAIgF,EAAI,EAAGA,EAAInG,KAAKiG,YAAYG,OAAQD,SACpCF,YAAYE,GAAGE,cAAcd,MAAMpB,EAAGoB,MAAMnB,SAGlDpE,KAAKoB,0BAA0BlD,SAASoI,QAC1CtG,KAAKF,SAASyG,sBACVZ,YAAa,OACbvE,eAAeiF,cAAcd,MAAMpB,EAAGoB,MAAMnB,QAC5ChD,eAAiBpB,KAAKoB,eAAehD,QAErC4B,KAAKF,SAASqC,mBAAqBnC,KAAKoB,0BAA0BlD,SAAS4H,MAC3E9F,KAAKF,SAASuC,mBAAqBrC,KAAKoB,0BAA0BlD,SAASsI,YAC/ElF,cAAe,EACjBtB,KAAKoB,eAAeiF,oBACdjF,eAAeiF,cAAcd,MAAMpB,EAAGoB,MAAMnB,SAGpDhD,eAAe4D,WAAY,OAC3BM,kBACCpG,EAAE2G,UAAY7F,KAAK4D,eACpBvC,YAAc,IAAInD,SAASuI,cAAczG,KAAMuF,MAAOA,oBAG1DvC,QAEFhD,KAAKsD,kBASCgC,cACE,KAQf5F,MAAMuD,UAAU8B,YAAc,eACtB2B,OAAS1G,KAAKoB,0BAA0BlD,SAAS4H,KACjDa,OAAU3G,KAAKoB,0BAA0BlD,SAASsI,MAC9CxG,KAAKoB,0BAA0BlD,SAAS8H,eACzC,YAAahG,KAAKoB,iBAChBsF,SAAW1G,KAAKF,SAAS8G,gBACzBD,SAAW3G,KAAKF,SAASyG,iBAGtC7G,MAAMuD,UAAU3D,QAAU,SAASJ,OACIiH,EAA/BzB,IAAMzG,KAAK0G,gBAAgBzF,GAAO2H,aAAY,MAE9C7G,KAAKY,aAIE,IAAR8D,WAC4B,OAAxB1E,KAAKoB,gBAA2BpB,KAAK+E,qBAC/B3D,eAAe8D,QAAQ4B,kBACvBxB,kBACAtC,SAKF,EACJ,GAAW,KAAR0B,KAAsC,OAAxB1E,KAAKoB,eAAyB,UAC7C6D,cACAkB,EAAI,EAAGA,EAAInG,KAAKkB,MAAMkF,OAAQD,IAC3BnG,KAAKkB,MAAMiF,KAAOnG,KAAKoB,gBAAmBpB,KAAKF,SAASiH,mBACnD7F,MAAM8F,OAAOb,IAAK,GACvBU,aAAc,OAGjBV,EAAI,EAAGA,EAAInG,KAAKmB,MAAMiF,OAAQD,KAC3BnG,KAAKmB,MAAMgF,KAAOnG,KAAKoB,iBAAmBpB,KAAKF,SAASiG,aACxDc,cACG7G,KAAKmB,MAAMgF,GAAGc,OAASjH,KAAKoB,gBAC5BpB,KAAKmB,MAAMgF,GAAGe,QAAUlH,KAAKoB,gBAC7BpB,KAAKmB,MAAMgF,GAAGgB,QAAUnH,KAAKoB,uBAC3BD,MAAM6F,OAAOb,IAAK,QAG1B/E,eAAiB,UACjB4B,YACS,KAAR0B,IACqB,OAAxB1E,KAAKoB,sBAICA,eAAiB,UACjB4B,QAEK,KAAR0B,IACqB,OAAxB1E,KAAKoB,gBAA2BpB,KAAK+E,qBAC/B3D,eAAe8D,QAAQkC,iBACvB9B,kBACAtC,QAEK,KAAR0B,IACqB,OAAxB1E,KAAKoB,gBAA2BpB,KAAK+E,qBAC/B3D,eAAe8D,QAAQmC,kBACvB/B,kBACAtC,QAEY,IAAb9D,EAAEoI,SAAiBpI,EAAE4F,SAAW5F,EAAE2G,UAA2B,IAAb3G,EAAEoI,SAAiBpI,EAAE4F,aACxEyC,OACe,IAAbrI,EAAEoI,SAAiBpI,EAAE4F,cACvB0C,SAIb9H,MAAMuD,UAAU5D,SAAW,SAASH,OAC5BqG,MAAQtH,KAAKuH,6BAA6BtG,GAE1Cc,KAAKY,UAAYZ,KAAKF,SAASiH,mBAI9B3F,eAAiBpB,KAAKyF,aAAaF,MAAMpB,EAAGoB,MAAMnB,QAElDa,cAEsB,OAAxBjF,KAAKoB,qBACKA,eAAiB,IAAIlD,SAAS4H,KAAK9F,KAAMuF,MAAMpB,EAAGoB,MAAMnB,QACxDlD,MAAMuG,KAAKzH,KAAKoB,qBAChBA,eAAe4D,WAAY,OAC3BM,kBACAtC,QAENhD,KAAKoB,0BAA0BlD,SAAS4H,MAAQ9F,KAAK4D,eAC/CxC,eAAesG,eAAiB1H,KAAKoB,eAAesG,mBACpD1E,UAKjBtD,MAAMuD,UAAUxD,OAAS,SAASnB,EAAGC,QAC5BuC,YAAYrB,OAAOnB,EAAGC,QACtByE,QAGTtD,MAAMuD,UAAU1D,UAAY,SAASL,OAE7ByI,aADApC,MAAQtH,KAAKuH,6BAA6BtG,OAG1Cc,KAAKY,cAIJuF,EAAI,EAAGA,EAAInG,KAAKkC,QAAQkE,OAAQD,IAC7BnG,KAAKkC,QAAQiE,GAAGyB,cAAcrC,MAAMpB,EAAGoB,MAAMnB,QACxClC,QAAQiE,GAAG0B,aAAc,OAEzB3F,QAAQiE,GAAG0B,aAAc,OAE7B7E,UAGe,OAArBhD,KAAKqB,YAAsB,KACtByG,WAAa9H,KAAKyF,aAAaF,MAAMpB,EAAGoB,MAAMnB,GAC7C0D,sBAAsB5J,SAAS4H,OAChCgC,WAAa,MAGU,OAAxB9H,KAAKoB,oBAEKC,YADS,OAAfyG,WACoB,IAAI5J,SAAS6J,UAAU/H,KAAM8H,WAAY9H,KAAKiB,eAE9C,IAAI/C,SAASuI,cAAczG,KAAMA,KAAKiB,cAAesE,OAGzEuC,aAAe9H,KAAKoB,oBACdC,YAAc,IAAInD,SAAS8H,SAAShG,KAAMA,KAAKoB,eAAgBmE,OAC/C,OAAfuC,gBACDzG,YAAc,IAAInD,SAASsI,KAAKxG,KAAMA,KAAKoB,eAAgB0G,aAEhEH,aAAe3H,KAAKoB,eAAe4G,qBAAqBzC,MAAMpB,EAAGoB,MAAMnB,QAClE/C,YAAc,IAAInD,SAASuI,cAAczG,KAAM2H,aAAcpC,aAGrEvC,UAELhD,KAAK0F,YAAa,SACdxE,MAAQlB,KAAKiG,YACRE,EAAI,EAAGA,EAAIjF,MAAMkF,OAAQD,IAC7BjF,MAAMiF,GAAG8B,WAAW1C,MAAMpB,EAAGoB,MAAMnB,QAC9B8D,SAAShH,MAAMiF,SAEpBnD,YACChD,KAAK2F,iBACNvE,eAAe8D,QAAQiD,eAAe5C,MAAMpB,EAAGoB,MAAMnB,QACrDpB,QACChD,KAAKsB,oBACNF,eAAe+G,eAAe5C,MAAMpB,EAAGoB,MAAMnB,GAC/CpE,KAAKoB,0BAA0BlD,SAAS4H,WAClCoC,SAASlI,KAAKoB,qBAElB4B,UAIbtD,MAAMuD,UAAU7D,QAAU,WAElBY,KAAKY,gBAIJU,cAAe,OACfoE,aAAc,OACdC,YAAa,EAEM,OAArB3F,KAAKqB,cACCrB,KAAKqB,uBAAuBnD,SAASuI,qBACjCrF,eAAiBpB,KAAKqB,iBACtB+G,QAAQpI,KAAKqB,kBACbiE,mBAEJjE,YAAc,UACd2B,UAIbtD,MAAMuD,UAAUwC,aAAe,SAAStB,EAAGC,OAClC+B,EAAI,EAAGA,EAAInG,KAAKkC,QAAQkE,OAAQD,OAC7BnG,KAAKkC,QAAQiE,GAAGyB,cAAczD,EAAGC,UAC1BpE,KAAKkC,QAAQiE,OAGxBA,MACAA,EAAI,EAAGA,EAAInG,KAAKkB,MAAMkF,OAAQD,OAC3BnG,KAAKkB,MAAMiF,GAAGyB,cAAczD,EAAGC,UACvBpE,KAAKkB,MAAMiF,OAGtBA,EAAI,EAAGA,EAAInG,KAAKmB,MAAMiF,OAAQD,IAAK,IAChCnG,KAAKmB,MAAMgF,GAAGyB,cAAczD,EAAGC,UACvBpE,KAAKmB,MAAMgF,GAChB,GAAI,YAAanG,KAAKmB,MAAMgF,IAAMnG,KAAKmB,MAAMgF,GAAGjB,QAAQ0C,cAAczD,EAAGC,UACpEpE,KAAKmB,MAAMgF,GAAGjB,eAGtB,MAGXxF,MAAMuD,UAAUiF,SAAW,SAASjB,UAC5B,IAAId,EAAI,EAAGA,EAAInG,KAAKkB,MAAMkF,OAAQD,IAC/BnG,KAAKkB,MAAMiF,KAAOc,OAIlBoB,KAAKC,IAAIrB,KAAK9C,EAAInE,KAAKkB,MAAMiF,GAAGhC,GAAKnE,KAAKC,kBACzCgH,KAAK9C,EAAInE,KAAKkB,MAAMiF,GAAGhC,GAGxBkE,KAAKC,IAAIrB,KAAK7C,EAAIpE,KAAKkB,MAAMiF,GAAG/B,GAAKpE,KAAKC,kBACzCgH,KAAK7C,EAAIpE,KAAKkB,MAAMiF,GAAG/B,KAWnC1E,MAAMuD,UAAUmF,QAAU,SAASG,iBAC3BC,WAAa,KACRrC,EAAI,EAAGA,EAAInG,KAAKmB,MAAMiF,OAAQD,IAAK,KACpCsC,KAAOzI,KAAKmB,MAAMgF,GAClBsC,KAAKvB,QAAUqB,QAAQrB,OAASuB,KAAKtB,QAAUoB,QAAQpB,QACpC,OAAfqB,YAAuBC,KAAKC,kBAAoBF,cAChDA,WAAaC,KAAKC,mBAGtBD,KAAKvB,QAAUqB,QAAQpB,OAASsB,KAAKtB,QAAUoB,QAAQrB,QACpC,OAAfsB,aAAwBC,KAAKC,kBAAoBF,cACjDA,YAAcC,KAAKC,mBAIZ,OAAfF,aACAD,QAAQG,kBAAoBF,WAAaxI,KAAKE,4BAE7CiB,MAAMsG,KAAKc,UAGpB7I,MAAMuD,UAAUF,OAAS,eACjB4F,QAAU3K,EAAEgC,KAAKS,UAAUmI,SAC3BD,gBAKsCxC,EAA9B0C,OAASC,KAAKC,MAAMJ,aAEpBxC,EAAI,EAAGA,EAAI0C,OAAO3H,MAAMkF,OAAQD,IAAK,KACjC6C,WAAaH,OAAO3H,MAAMiF,GAC1B8C,iBAAmBJ,OAAOK,aAAa/C,GACvCc,KAAO,IAAI/I,SAAS4H,KAAK9F,KAAMiJ,iBAAiB,GAAIA,iBAAiB,IACzEhC,KAAKS,cAAgBsB,WAAW,GAChC/B,KAAK/B,QAAU,IAAIhH,SAASoI,QAAQ0C,WAAW,GAAGG,WAAYlC,WACzD/F,MAAMuG,KAAKR,UAGhBd,EAAI,EAAGA,EAAI0C,OAAOO,MAAMhD,OAAQD,IAAK,KACjCkD,WAAaR,OAAOO,MAAMjD,GAC1BmD,iBAAmBT,OAAOU,aAAapD,GACvCsC,KAAO,KACRY,WAAW,KAAOA,WAAW,KAI5BZ,KAAO,IAAIvK,SAAS8H,SAAShG,KAAMA,KAAKkB,MAAMmI,WAAW,MACpDG,YAAcF,iBAAiBE,YACpCf,KAAKvD,QAAU,IAAIhH,SAASoI,QAAQ+C,WAAW,GAAGF,WAAYV,MAC1DY,WAAWjD,OAAS,GACpBqC,KAAKvD,QAAQiD,eAAekB,WAAW,GAAGlF,EAAGkF,WAAW,GAAGjF,KAEtC,IAAnBiF,WAAW,KACjBZ,KAAO,IAAIvK,SAAS6J,UAAU/H,KAAMA,KAAKkB,MAAMmI,WAAW,MACrDI,OAASH,iBAAiBG,OAC/BhB,KAAKiB,OAASJ,iBAAiBI,UAE/BjB,KAAO,IAAIvK,SAASsI,KAAKxG,KAAMA,KAAKkB,MAAMmI,WAAW,IAAKrJ,KAAKkB,MAAMmI,WAAW,MAC3EM,aAAeL,iBAAiBK,aACrClB,KAAKC,kBAAoBY,iBAAiBZ,kBAC1CD,KAAKmB,gBAAkBN,iBAAiBM,gBACxCnB,KAAKvD,QAAU,IAAIhH,SAASoI,QAAQ+C,WAAW,GAAGF,WAAYV,MAC1DY,WAAWjD,OAAS,GACpBqC,KAAKvD,QAAQiD,eAAekB,WAAW,GAAGlF,EAAGkF,WAAW,GAAGjF,IAGvD,OAATqE,WACMtH,MAAMsG,KAAKgB,OAG1B,MAAMvJ,QACCqC,MAAO,OACPC,WAAa,kCAK9B9B,MAAMuD,UAAU4G,KAAO,eAQf1D,EANA0C,OAAS,cACO,gBACA,SACP,SACA,OAITC,OAAwC,KAA/B9I,KAAKS,SAASmI,MAAMkB,QAAuC,IAAtB9J,KAAKkB,MAAMkF,aAIzDD,EAAI,EAAGA,EAAInG,KAAKkB,MAAMkF,OAAQD,IAAK,KAC/Bc,KAAOjH,KAAKkB,MAAMiF,GAElB4D,SAAW,CAAC9C,KAAK/B,QAAQ8E,KAAM/C,KAAKS,eACpCuC,WAAa,CAAChD,KAAK9C,EAAG8C,KAAK7C,GAE/ByE,OAAOK,aAAazB,KAAKwC,YACzBpB,OAAO3H,MAAMuG,KAAKsC,cAGlB5D,EAAI,EAAGA,EAAInG,KAAKmB,MAAMiF,OAAQD,IAAK,KAC/BsC,KAAOzI,KAAKmB,MAAMgF,GAClB+D,SAAW,KACXC,WAAa,KAEd1B,gBAAgBvK,SAAS8H,UACxBmE,WAAa,aACM1B,KAAKe,aAExBU,SAAW,CAAClK,KAAKkB,MAAMkJ,QAAQ3B,KAAKxB,MAAOjH,KAAKkB,MAAMkJ,QAAQ3B,KAAKxB,MAAOwB,KAAKvD,QAAQ8E,MACnFvB,KAAKvD,QAAQmF,SACbH,SAASzC,KAAKgB,KAAKvD,QAAQoF,WAEzB7B,gBAAgBvK,SAAS6J,WAC/BoC,WAAa,QACC1B,KAAKgB,cACLhB,KAAKiB,QAEnBQ,SAAW,EAAE,EAAGlK,KAAKkB,MAAMkJ,QAAQ3B,KAAKxB,MAAO,KACzCwB,gBAAgBvK,SAASsI,OAC/B2D,WAAa,iBACU1B,KAAKmB,6BACRnB,KAAKkB,+BACAlB,KAAKC,mBAE9BwB,SAAW,CAAClK,KAAKkB,MAAMkJ,QAAQ3B,KAAKvB,OAAQlH,KAAKkB,MAAMkJ,QAAQ3B,KAAKtB,OAAQsB,KAAKvD,QAAQ8E,MACrFvB,KAAKvD,QAAQmF,SACbH,SAASzC,KAAKgB,KAAKvD,QAAQoF,WAGlB,OAAbJ,UAAoC,OAAfC,aACrBtB,OAAOO,MAAM3B,KAAKyC,UAClBrB,OAAOU,aAAa9B,KAAK0C,kBAG5B1J,SAASmI,IAAIE,KAAKyB,UAAU1B,WAGrCnJ,MAAMuD,UAAUgC,YAAc,eACtBuF,SAAWxK,KAAKS,SAASmI,SACD,GAAxB5I,KAAKyB,SAAS2E,QAA2E,GAA5DoE,SAASC,cAAczK,KAAKyB,SAASzB,KAAK0B,eAAoB,UACtFA,eACE1B,KAAK0B,aAAe1B,KAAKyB,SAAS2E,aAChC3E,SAASiJ,WAEbjJ,SAASgG,KAAK+C,UACfxK,KAAKyB,SAAS2E,OAASpG,KAAKQ,oBACvBiB,SAASkJ,aACTjJ,kBAKjBhC,MAAMuD,UAAUuE,KAAO,gBACdvC,cACDjF,KAAK0B,aAAe,SACfA,oBACAjB,SAASmI,IAAI5I,KAAKyB,SAASzB,KAAK0B,oBAIhCR,MAAQ,QACRC,MAAQ,QAIR4B,cACAC,SAIbtD,MAAMuD,UAAUsE,KAAO,WACfvH,KAAK0B,aAAe1B,KAAKyB,SAAS2E,OAAS,SACtC1E,oBACAjB,SAASmI,IAAI5I,KAAKyB,SAASzB,KAAK0B,oBAIhCR,MAAQ,QACRC,MAAQ,QAIR4B,cACAC,SAIbtD,MAAMuD,UAAUhB,MAAQ,gBACfgD,mBACA/D,MAAQ,QACRC,MAAQ,QACR0I,YACA7G,QAGTtD,MAAMuD,UAAU2H,QAAU,WACtBC,cAAc7K,KAAKgB,iBACdF,YAAYrC,OAAOqM,WACnBhK,YAAYrC,OAAOsM,UAG5BrL,MAAMuD,UAAUqC,WAAa,eACrB0F,EAAIhL,KAER6K,cAAc7K,KAAKgB,iBACdA,WAAaiK,aAAY,WAC1BD,EAAEjK,cAAgBiK,EAAEjK,aACpBiK,EAAEhI,SACH,UACEjC,cAAe,GAGxBrB,MAAMuD,UAAUD,KAAO,eAGfmD,EADAjC,EADSlE,KAAKqD,YACH6H,WAAW,UAG1BhH,EAAEiH,UAAU,EAAG,EAAGnL,KAAKqD,YAAYzD,MAAOI,KAAKqD,YAAYxD,QAC3DqE,EAAE2F,OACF3F,EAAEkH,UAAU,GAAK,IAEZjF,EAAI,EAAGA,EAAInG,KAAKkC,QAAQkE,OAAQD,SAC5BjE,QAAQiE,GAAGnD,KAAKkB,OAGpBlE,KAAK2B,QAAQiE,SAAU,KAEpBO,EAAI,EAAGA,EAAInG,KAAKkB,MAAMkF,OAAQD,IAC9BjC,EAAEmH,UAAY,EACdnH,EAAEoH,UAAYpH,EAAEqH,YAAevL,KAAKkB,MAAMiF,KAAOnG,KAAKoB,eAAkB,OAAS,aAC5EF,MAAMiF,GAAGnD,KAAKkB,OAEnBiC,EAAI,EAAGA,EAAInG,KAAKmB,MAAMiF,OAAQD,IAC9BjC,EAAEmH,UAAY,EACdnH,EAAEoH,UAAYpH,EAAEqH,YAAevL,KAAKmB,MAAMgF,KAAOnG,KAAKoB,gBACrBpB,KAAKmB,MAAMgF,GAAGjB,UAAYlF,KAAKoB,eAAkB,OAAS,aACtFD,MAAMgF,GAAGnD,KAAKkB,GAEC,OAArBlE,KAAKqB,cACJ6C,EAAEmH,UAAY,EACdnH,EAAEoH,UAAYpH,EAAEqH,YAAc,aACzBlK,YAAY2B,KAAKkB,IAI9BA,EAAEsH,eACG3B,QAGF,CACH4B,YAAa/L"}
\ No newline at end of file
diff --git a/amd/build/ui_html.min.js.map b/amd/build/ui_html.min.js.map
deleted file mode 100644
index 9d3bb59cf..000000000
--- a/amd/build/ui_html.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"ui_html.min.js","sources":["../src/ui_html.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 util.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 * Implementation of the html_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a div\n * containing the author-supplied HTML. The serialisation of that HTML,\n * which is what is essentially copied back into the textarea for submissions\n * as the answer, is a JSON object. The fields of that object are the names\n * of all author-supplied HTML elements with a class 'coderunner-ui-element';\n * all such objects are expected to have a 'name' attribute as well. The\n * associated field values are lists. Each list contains all the values, in\n * document order, of the results of calling the jquery val() method in turn\n * on each of the UI elements with that name.\n * This means that at least input, select and textarea\n * elements are supported. The author is responsible for checking the\n * compatibility of other elements with jquery's val() method.\n *\n * The HTML to use in the answer area must be provided as the contents of\n * either the globalextra field or the prototypeextra field in the question\n * authoring form. The choice of which is set by the html_src UI parameter, which\n * must be either 'globalextra' or 'prototypeextra'.\n *\n * If any fields of the answer html are to be preloaded, these should be specified\n * in the answer preload with json of the form '{\"\": \"\",...}'\n * where fieldValueList is a list of all the values to be assigned to the fields\n * with the given name, in document order.\n *\n * To accommodate the possibility of dynamic HTML, any leftover preload values,\n * that is, values that cannot be positioned within the HTML either because\n * there is no field of the required name or because, in the case of a list,\n * there are insufficient elements, are assigned to the data['leftovers']\n * attribute of the outer html div, as a sub-object of the original object.\n * This outer div can be located as the 'closest' (in a jQuery sense)\n * div.qtype-coderunner-html-outer-div. The author-supplied HTML must include\n * JavaScript to make use of the 'leftovers'.\n *\n * As a special case of the serialisation, if all values in the serialisation\n * are either empty strings or a list of empty strings, the serialisation is\n * itself the empty string.\n *\n * @module coderunner/ui_html\n * @copyright Richard Lobb, 2018, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the HtmlUi object.\n * @param {string} textareaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\n function HtmlUi(textareaId, width, height, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n this.textareaId = textareaId;\n var srcField = uiParams.html_src || 'globalextra';\n this.html = this.textArea.attr('data-' + srcField);\n this.html = this.html.replace(/___textareaId___/gm, textareaId);\n this.readOnly = this.textArea.prop('readonly');\n this.uiParams = uiParams;\n this.fail = false;\n this.htmlDiv = null;\n this.reload();\n }\n\n HtmlUi.prototype.failed = function() {\n return this.fail;\n };\n\n\n HtmlUi.prototype.failMessage = function() {\n return 'htmluiloadfail';\n };\n\n\n // Copy the serialised version of the HTML UI area to the TextArea.\n HtmlUi.prototype.sync = function() {\n var\n serialisation = {},\n name,\n empty = true;\n\n this.getFields().each(function() {\n var value, type;\n type = $(this).attr('type');\n name = $(this).attr('name');\n if ((type === 'checkbox' || type === 'radio') && !($(this).is(':checked'))) {\n value = '';\n } else {\n value = $(this).val();\n }\n if (serialisation.hasOwnProperty(name)) {\n serialisation[name].push(value);\n } else {\n serialisation[name] = [value];\n }\n if (value !== '') {\n empty = false;\n }\n });\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n\n HtmlUi.prototype.getElement = function() {\n return this.htmlDiv;\n };\n\n HtmlUi.prototype.getFields = function() {\n return $(this.htmlDiv).find('.coderunner-ui-element');\n };\n\n // Set the value of the jQuery field to the given value.\n // If the field is a radio button or a checkbox and its name matches\n // the given value, the checked attribute is set. Otherwise the field's\n // val() function is called to set the value.\n HtmlUi.prototype.setField = function(field, value) {\n if (field.attr('type') === 'checkbox' || field.attr('type') === 'radio') {\n field.prop('checked', field.val() === value);\n } else {\n field.val(value);\n }\n };\n\n HtmlUi.prototype.reload = function() {\n var\n content = $(this.textArea).val(), // JSON-encoded HTML element settings.\n valuesToLoad,\n values,\n i,\n fields,\n leftOvers,\n outerDivId = 'qtype-coderunner-outer-div-' + this.textareaId.toString(),\n outerDiv = \"
\");\n this.htmlDiv.data('uiparams', this.uiParams); // For use by scripts embedded in html.\n this.htmlDiv.data('templateparams', this.uiParams); // Legacy support only. DEPRECATED.\n if (content) {\n try {\n valuesToLoad = JSON.parse(content);\n leftOvers = {};\n for (var name in valuesToLoad) {\n values = valuesToLoad[name];\n fields = this.getFields().filter(\"[name='\" + name + \"']\");\n leftOvers[name] = [];\n for (i = 0; i < values.length; i++) {\n if (i < fields.length) {\n this.setField($(fields[i]), values[i]);\n } else {\n leftOvers[name].push(values[i]);\n }\n }\n if (leftOvers[name].length === 0) {\n delete leftOvers[name];\n }\n }\n\n if (!$.isEmptyObject(leftOvers)) {\n this.htmlDiv.data('leftovers', leftOvers);\n }\n\n } catch(e) {\n this.fail = true;\n }\n }\n };\n\n HtmlUi.prototype.resize = function() {}; // Nothing to see here. Move along please.\n\n HtmlUi.prototype.hasFocus = function() {\n var focused = false;\n this.getFields().each(function() {\n if (this === document.activeElement) {\n focused = true;\n }\n });\n return focused;\n };\n\n // Destroy the HTML UI and serialise the result into the original text area.\n HtmlUi.prototype.destroy = function() {\n this.sync();\n $(this.htmlDiv).remove();\n this.htmlDiv = null;\n };\n\n return {\n Constructor: HtmlUi\n };\n});\n"],"names":["define","$","HtmlUi","textareaId","width","height","uiParams","textArea","document","getElementById","srcField","html_src","html","this","attr","replace","readOnly","prop","fail","htmlDiv","reload","prototype","failed","failMessage","sync","name","serialisation","empty","getFields","each","value","type","is","val","hasOwnProperty","push","JSON","stringify","getElement","find","setField","field","valuesToLoad","values","i","fields","leftOvers","content","outerDiv","toString","data","parse","filter","length","isEmptyObject","e","resize","hasFocus","focused","activeElement","destroy","remove","Constructor"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DAA,kCAAO,CAAC,WAAW,SAASC,YAQfC,OAAOC,WAAYC,MAAOC,OAAQC,eAClCC,SAAWN,EAAEO,SAASC,eAAeN,kBACrCA,WAAaA,eACdO,SAAWJ,SAASK,UAAY,mBAC/BC,KAAOC,KAAKN,SAASO,KAAK,QAAUJ,eACpCE,KAAOC,KAAKD,KAAKG,QAAQ,qBAAsBZ,iBAC/Ca,SAAWH,KAAKN,SAASU,KAAK,iBAC9BX,SAAWA,cACXY,MAAO,OACPC,QAAU,UACVC,gBAGTlB,OAAOmB,UAAUC,OAAS,kBACfT,KAAKK,MAIhBhB,OAAOmB,UAAUE,YAAc,iBACpB,kBAKXrB,OAAOmB,UAAUG,KAAO,eAGhBC,KADAC,cAAgB,GAEhBC,OAAQ,OAEPC,YAAYC,MAAK,eACdC,MAAOC,KACXA,KAAO9B,EAAEY,MAAMC,KAAK,QACpBW,KAAOxB,EAAEY,MAAMC,KAAK,QAIhBgB,MAHU,aAATC,MAAgC,UAATA,MAAuB9B,EAAEY,MAAMmB,GAAG,YAGlD/B,EAAEY,MAAMoB,MAFR,GAIRP,cAAcQ,eAAeT,MAC7BC,cAAcD,MAAMU,KAAKL,OAEzBJ,cAAcD,MAAQ,CAACK,OAEb,KAAVA,QACAH,OAAQ,MAGZA,WACKpB,SAAS0B,IAAI,SAEb1B,SAAS0B,IAAIG,KAAKC,UAAUX,iBAKzCxB,OAAOmB,UAAUiB,WAAa,kBACnBzB,KAAKM,SAGhBjB,OAAOmB,UAAUO,UAAY,kBAClB3B,EAAEY,KAAKM,SAASoB,KAAK,2BAOhCrC,OAAOmB,UAAUmB,SAAW,SAASC,MAAOX,OACb,aAAvBW,MAAM3B,KAAK,SAAiD,UAAvB2B,MAAM3B,KAAK,QAChD2B,MAAMxB,KAAK,UAAWwB,MAAMR,QAAUH,OAEtCW,MAAMR,IAAIH,QAIlB5B,OAAOmB,UAAUD,OAAS,eAGlBsB,aACAC,OACAC,EACAC,OACAC,UALAC,QAAU9C,EAAEY,KAAKN,UAAU0B,MAO3Be,SAAW,gFADE,8BAAgCnC,KAAKV,WAAW8C,YAC4C,aAExG9B,QAAUlB,EAAE+C,SAAWnC,KAAKD,KAAO,eACnCO,QAAQ+B,KAAK,WAAYrC,KAAKP,eAC9Ba,QAAQ+B,KAAK,iBAAkBrC,KAAKP,UACrCyC,gBAIS,IAAItB,QAFTiB,aAAeN,KAAKe,MAAMJ,SAC1BD,UAAY,GACKJ,aAAc,KAC3BC,OAASD,aAAajB,MACtBoB,OAAShC,KAAKe,YAAYwB,OAAO,UAAY3B,KAAO,MACpDqB,UAAUrB,MAAQ,GACbmB,EAAI,EAAGA,EAAID,OAAOU,OAAQT,IACvBA,EAAIC,OAAOQ,YACNb,SAASvC,EAAE4C,OAAOD,IAAKD,OAAOC,IAEnCE,UAAUrB,MAAMU,KAAKQ,OAAOC,IAGL,IAA3BE,UAAUrB,MAAM4B,eACTP,UAAUrB,MAIpBxB,EAAEqD,cAAcR,iBACZ3B,QAAQ+B,KAAK,YAAaJ,WAGrC,MAAMS,QACCrC,MAAO,IAKxBhB,OAAOmB,UAAUmC,OAAS,aAE1BtD,OAAOmB,UAAUoC,SAAW,eACnBC,SAAU,cACV9B,YAAYC,MAAK,WACdhB,OAASL,SAASmD,gBAClBD,SAAU,MAGXA,SAIXxD,OAAOmB,UAAUuC,QAAU,gBAClBpC,OACLvB,EAAEY,KAAKM,SAAS0C,cACX1C,QAAU,MAGZ,CACH2C,YAAa5D"}
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js b/amd/build/ui_scratchpad.min.js
new file mode 100644
index 000000000..ce4cbf97a
--- /dev/null
+++ b/amd/build/ui_scratchpad.min.js
@@ -0,0 +1,54 @@
+define("qtype_coderunner/ui_scratchpad",["exports","core/ajax","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_ajax,_templates,_userinterfacewrapper,_outputdisplayarea){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+/**
+ * Implementation of the scratchpad_ui user interface plugin. For overall details
+ * of the UI plugin architecture, see userinterfacewrapper.js.
+ *
+ * This plugin replaces the usual textarea answer element with a UI is designed to
+ * allow the execution of code in the CodeRunner question in a manner similar to an IDE.
+ * It contains two editor boxes, one on top of another, allowing users to enter and
+ * edit code in both.
+ * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,
+ * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,
+ * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that
+ * provides information about how to use the Scratchpad.
+ * It's possible to run code 'in-browser' by clicking the Run Button,
+ * without making a submission via the Check Button:
+ * If Prefix with Answer is not checked, only the code in the Scratchpad is run --
+ * allowing for a rough working spot to quickly check the result of code.
+ * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is
+ * appended to the code in the first editor before being run.
+ * The Run Button has some limitations when using its default configuration:
+ * Does not support programs that use STDIN (by default);
+ * Only supports textual STDOUT (by default).
+ * Note: These features can be supported, see the README section on wrappers...
+ * The serialisation of this UI is a JSON object with the fields
+ * with fields:
+ * answer_code: [""] A list containing a string with answer code from the first editor;
+ * test_code: [""] A list containing a string with containing answer code from the second editor;
+ * show_hide: ["1"] when scratchpad is visible, otherwise [""];
+ * prefix_ans: ["1"] when Prefix with Answer is checked, otherwise [""].
+ * . The fields of that object are the names
+ *
+ * UI Parameters:
+ * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.
+ * - button_name: run button text.
+ * - prefix_name: prefix with answer check-box label text.
+ * - help_text: help text to show.
+ * - run_lang: language used to run code when the run button is clicked,
+ * this should be the language your wrapper is written in (if applicable).
+ * - wrapper_src: location of wrapper code to be used by the run button, if applicable:
+ * setting to globalextra will use text in global extra field,
+ * - prototypeextra will use the prototype extra field.
+ * - html_output: when true, the output from run will be displayed as raw HTML instead of text.
+ * - disable_scratchpad: disable the scratchpad, effectively returning back to Ace UI
+ * from student perspective.
+ * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.
+ * This can be used to swap the default state.
+ *
+ * @module coderunner/ui_scratchpad
+ * @copyright Richard Lobb, 2022, The University of Canterbury
+ * @copyright James Napier, 2022, The University of Canterbury
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Constructor=void 0,_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates);const invertSerial=current=>"1"==current[0]?[""]:["1"],overwriteValues=(defaults,prescribed)=>{let overwritten={...defaults};if(prescribed)for(const[key,value]of Object.entries(defaults))overwritten[key]=prescribed[key]||value;return overwritten};_exports.Constructor=class{constructor(textAreaId,width,height,uiParams){const DEF_UI_PARAMS={scratchpad_name:"",button_name:"",prefix_name:"",help_text:"",run_lang:uiParams.lang,html_output:!1,disable_scratchpad:!1,wrapper_src:null};this.textArea=document.getElementById(textAreaId),this.textAreaId=textAreaId,this.height=height,this.readOnly=this.textArea.readonly,this.fail=!1,this.outerDiv=null,this.outputDisplay=null,this.invertPreload=uiParams.invert_prefix,this.lang=uiParams.lang,this.numRows=this.textArea.rows,this.uiParams=overwriteValues(DEF_UI_PARAMS,uiParams),this.runWrapper=this.getRunWrapper(),this.reload()}getRunWrapper(){const wrapperSrc=this.uiParams.wrapper_src;let runWrapper=null;return wrapperSrc&&("globalextra"===wrapperSrc||"prototypeextra"===wrapperSrc?runWrapper=this.textArea.dataset[wrapperSrc]:(this.fail=!0,this.failString="scratchpad_ui_badrunwrappersrc")),runWrapper}failed(){return this.fail}failMessage(){return this.failString}sync(){if(!this.context)return;const serialisation=this.getSerialisation();this.setSerialisation(serialisation)}getSerialisation(){const prefixAns=document.getElementById(this.context.prefix_ans.id),showHide=document.getElementById(this.context.show_hide.id);let serialisation={answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:[""]};return this.answerTextarea&&(serialisation.answer_code=[this.answerTextarea.value]),this.testTextarea&&(serialisation.test_code=[this.testTextarea.value]),showHide&&!(el=>{if(!el.classList.contains("collapse"))throw Error("Element does not have collapse class");return!el.classList.contains("show")})(showHide)&&(serialisation.show_hide=["1"]),(null!=prefixAns&&prefixAns.checked||this.context.disable_scratchpad)&&(serialisation.prefix_ans=["1"]),this.invertPreload&&(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans)),serialisation}setSerialisation(serialisation){serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),Object.values(serialisation).some((val=>1===val.length&&val[0].length>0))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}getElement(){return this.outerDiv}handleRunButtonClick(){this.sync();const preloadString=this.textArea.value,serial=this.readJson(preloadString),sandboxParams=this.uiParams.params,language=this.lang,code=(answerCode=serial.answer_code,testCode=serial.test_code,prefixAns=serial.prefix_ans[0],(template=this.runWrapper)||(template="{{ ANSWER_CODE }}\n{{ SCRATCHPAD_CODE }}"),prefixAns||(answerCode=""),(template=template.replaceAll("{{ ANSWER_CODE }}",answerCode)).replaceAll("{{ SCRATCHPAD_CODE }}",testCode));var answerCode,testCode,prefixAns,template;this.outputDisplay.handleRunButtonClick(code,language,sandboxParams)}updateContext(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}readJson(preloadString){let serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch{serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}async reload(){const preloadString=this.textArea.value;let preload;try{preload=this.readJson(preloadString)}catch(error){return this.fail=!0,void(this.failString="scratchpad_ui_invalidserialisation")}this.updateContext(preload);try{const{html:html}=await _templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context),outputMode=this.uiParams.html_output?"html":"text";this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,outputMode),this.addEventListeners()}catch(e){return this.fail=!0,void(this.failString="scratchpad_ui_templateloadfail")}}drawUi(html){const wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}addAceUis(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}addEventListeners(){const runButton=document.getElementById(this.textAreaId+"_run-btn"),outputDisplayarea=document.getElementById(this.context.output_display.id);runButton&&runButton.addEventListener("click",(()=>this.handleRunButtonClick(_ajax.default,outputDisplayarea)))}resize(){}hasFocus(){var _this$answerCodeUi,_this$testCodeUi;let focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}destroy(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}}));
+
+//# sourceMappingURL=ui_scratchpad.min.js.map
\ No newline at end of file
diff --git a/amd/build/ui_table.min.js.map b/amd/build/ui_table.min.js.map
deleted file mode 100644
index 0c2069ba6..000000000
--- a/amd/build/ui_table.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"ui_table.min.js","sources":["../src/ui_table.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 util.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 * Implementation of the table_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a div\n * containing an HTML table. The number of columns, and\n * the initial number of rows are specified by required UI parameters\n * num_columns and num_rows respectively.\n * Optional additional UI parameters are:\n * 1. column_headers: a list of strings that can be used to provide a\n * fixed header row at the top.\n * 2. row_labels: a list of strings that can be used to provide a\n * fixed row label column at the left.\n * 3. dynamic_rows, which, if true, allows the user to add rows.\n * 4. locked_cells: a list of [row, column] pairs, being the coordinates\n * of table cells that cannot be changed by the user. row and column numbers\n * are zero origin and do not include the header row or the row labels.\n * 5. width_percents: a list of the percentages of the width occupied\n * by each column. This list must include a value for the row labels, if present.\n *\n * The serialisation of the table, which is what is essentially copied back\n * into the textarea for submissions as the answer, is a JSON array. Each\n * element in the array is itself an array containing the values of one row\n * of the table. Empty cells are empty strings. The table header row and row\n * label columns are not provided in the serialisation.\n *\n * To preload the table with data, simply set the answer_preload of the question\n * to a json array of row values (each itself an array). If the number of rows\n * in the preload exceeds the number set by num_rows, extra rows are\n * added. If the number is less than num_rows, or if there is no\n * answer preload, undefined rows are simply left blank.\n *\n * As a special case of the serialisation, if all cells in the serialisation\n * are empty strings, the serialisation is itself the empty string.\n *\n * @module qtype_coderunner/ui_table\n * @copyright Richard Lobb, 2018, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the TableUI object.\n * @param {string} textareaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\n function TableUi(textareaId, width, height, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n this.readOnly = this.textArea.prop('readonly');\n this.tableDiv = null;\n this.uiParams = uiParams;\n if (!uiParams.num_columns ||\n !uiParams.num_rows) {\n this.fail = true;\n this.failString = 'table_ui_missingparams';\n return; // We're dead, fred.\n }\n\n this.fail = false;\n this.lockedCells = uiParams.locked_cells || [];\n this.hasHeader = uiParams.column_headers && uiParams.column_headers.length > 0 ? true : false;\n this.hasRowLabels = uiParams.row_labels && uiParams.row_labels.length > 0 ? true : false;\n this.numDataColumns = uiParams.num_columns;\n this.rowsPerCell = uiParams.lines_per_cell || 2;\n this.totNumColumns = this.numDataColumns + (this.hasRowLabels ? 1 : 0);\n this.columnWidths = this.computeColumnWidths();\n this.reload();\n }\n\n // Return an array of the percentage widths required for each of the\n // totNumColumns columns.\n TableUi.prototype.computeColumnWidths = function() {\n var defaultWidth = Math.trunc(100 / this.totNumColumns),\n columnWidths = [];\n if (this.uiParams.column_width_percents && this.uiParams.column_width_percents.length > 0) {\n return this.uiParams.column_width_percents;\n } else if (Array.prototype.fill) { // Anything except bloody IE.\n return new Array(this.totNumColumns).fill(defaultWidth);\n } else { // IE. What else?\n for (var i = 0; i < this.totNumColumns; i++) {\n columnWidths.push(defaultWidth);\n }\n return columnWidths;\n }\n };\n\n // Return True if the cell at the given row and column is locked.\n // The given row and column numbers exclude column headers and row labels.\n TableUi.prototype.isLockedCell = function(row, col) {\n for (var i = 0; i < this.lockedCells.length; i++) {\n if (this.lockedCells[i][0] == row && this.lockedCells[i][1] == col) {\n return true;\n }\n }\n return false;\n };\n\n TableUi.prototype.getElement = function() {\n return this.tableDiv;\n };\n\n TableUi.prototype.failed = function() {\n return this.fail;\n };\n\n TableUi.prototype.failMessage = function() {\n return this.failString;\n };\n\n // Copy the serialised version of the Table UI area to the TextArea.\n TableUi.prototype.sync = function() {\n var\n serialisation = [],\n empty = true,\n tableRows = $(this.tableDiv).find('table tbody tr');\n\n tableRows.each(function () {\n var rowValues = [];\n $(this).find('textarea').each(function () {\n var cellVal = $(this).val();\n rowValues.push(cellVal);\n if (cellVal) {\n empty = false;\n }\n });\n serialisation.push(rowValues);\n });\n\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Return the HTML for row number iRow.\n TableUi.prototype.tableRow = function(iRow, preload) {\n var html = '
', widthIndex = 0, width;\n\n // Insert the row label if required.\n if (this.hasRowLabels) {\n width = this.columnWidths[0];\n widthIndex = 1;\n html += \"
\";\n if (iRow < this.uiParams.row_labels.length) {\n html += this.uiParams.row_labels[iRow];\n }\n html += \"
\";\n }\n\n for (var iCol = 0; iCol < this.numDataColumns; iCol++) {\n width = this.columnWidths[widthIndex++];\n html += \"
\";\n html += '';\n html += \"
\";\n }\n html += '
';\n return html;\n };\n\n // Return the HTML for the table's head section.\n TableUi.prototype.tableHeadSection = function() {\n var html = \"\\n\",\n colIndex = 0; // Column index including row label if present.\n\n if (this.hasHeader) {\n html += \"
\";\n if (iCol < this.uiParams.column_headers.length) {\n html += this.uiParams.column_headers[iCol];\n }\n colIndex++;\n html += \"
\";\n }\n html += \"
\\n\";\n }\n html += \"\\n\";\n return html;\n };\n\n // Build the HTML table, filling it with the data from the serialisation\n // currently in the textarea (if there is any).\n TableUi.prototype.reload = function() {\n var\n preloadJson = $(this.textArea).val(), // JSON-encoded table values.\n preload = [],\n divHtml = \"
\\n\" +\n \"
\\n\";\n\n if (preloadJson) {\n try {\n preload = JSON.parse(preloadJson);\n } catch(error) {\n this.fail = true;\n this.failString = 'table_ui_invalidjson';\n return;\n }\n }\n\n try {\n // Build the table head section.\n divHtml += this.tableHeadSection();\n\n // Build the table body. Each table cell has a textarea inside it,\n // except for row labels (if present).\n divHtml += \"\\n\";\n var num_rows_required = Math.max(this.uiParams.num_rows, preload.length);\n for (var iRow = 0; iRow < num_rows_required; iRow++) {\n divHtml += this.tableRow(iRow, preload);\n }\n\n divHtml += '\\n
\\n
';\n this.tableDiv = $(divHtml);\n if (this.uiParams.dynamic_rows) {\n this.addButtons();\n }\n } catch (error) {\n this.fail = true;\n this.failString = 'table_ui_invalidserialisation';\n }\n };\n\n // Add 'Add row' and 'Delete row' buttons at the end of the table.\n TableUi.prototype.addButtons = function() {\n var deleteButtonHtml = '',\n deleteButton = $(deleteButtonHtml),\n t = this;\n this.tableDiv.append(deleteButton);\n deleteButton.click(function() {\n var numRows = t.tableDiv.find('table tbody tr').length,\n lastRow = t.tableDiv.find('tr:last');\n if (numRows > t.uiParams.num_rows) {\n lastRow.remove();\n }\n lastRow = t.tableDiv.find('tr:last'); // New last row.\n if (numRows == t.uiParams.num_rows + 1) {\n $(this).prop('disabled', true);\n }\n });\n\n var addButtonHtml = '',\n addButton = $(addButtonHtml);\n t.tableDiv.append(addButton);\n addButton.click(function() {\n var lastRow, newRow;\n lastRow = t.tableDiv.find('table tbody tr:last');\n newRow = lastRow.clone(); // Copy the last row of the table.\n newRow.find('textarea').each(function() { // Clear all td elements in it.\n $(this).val('');\n });\n lastRow.after(newRow);\n $(this).prev().prop('disabled', false);\n });\n };\n\n TableUi.prototype.resize = function() {}; // Nothing to see here. Move along please.\n\n TableUi.prototype.hasFocus = function() {\n var focused = false;\n $(this.tableDiv).find('textarea').each(function() {\n if (this === document.activeElement) {\n focused = true;\n }\n });\n return focused;\n };\n\n // Destroy the HTML UI and serialise the result into the original text area.\n TableUi.prototype.destroy = function() {\n this.sync();\n $(this.tableDiv).remove();\n this.tableDiv = null;\n };\n\n return {\n Constructor: TableUi\n };\n});\n"],"names":["define","$","TableUi","textareaId","width","height","uiParams","textArea","document","getElementById","readOnly","this","prop","tableDiv","num_columns","num_rows","fail","failString","lockedCells","locked_cells","hasHeader","column_headers","length","hasRowLabels","row_labels","numDataColumns","rowsPerCell","lines_per_cell","totNumColumns","columnWidths","computeColumnWidths","reload","prototype","defaultWidth","Math","trunc","column_width_percents","Array","fill","i","push","isLockedCell","row","col","getElement","failed","failMessage","sync","serialisation","empty","find","each","rowValues","cellVal","val","JSON","stringify","tableRow","iRow","preload","html","widthIndex","iCol","tableHeadSection","colIndex","preloadJson","divHtml","parse","error","num_rows_required","max","dynamic_rows","addButtons","deleteButton","t","append","click","numRows","lastRow","remove","addButton","newRow","clone","after","prev","resize","hasFocus","focused","activeElement","destroy","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/userinterfacewrapper.min.js.map b/amd/build/userinterfacewrapper.min.js.map
deleted file mode 100644
index f46f952fc..000000000
--- a/amd/build/userinterfacewrapper.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"userinterfacewrapper.min.js","sources":["../src/userinterfacewrapper.js"],"sourcesContent":["/******************************************************************************\n *\n * This module provides a wrapper for user-interface modules, handling hiding\n * of the textArea that is being replaced by the UI element,\n * resizing of the UI component, and support of such usability functions as\n * ctrl-alt-M to disable/re-enable the entire user interface, including the\n * wrapper.\n *\n * @module coderunner/userinterfacewrapper\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * The InterfaceWrapper class is constructed either by Moodle PHP calls of\n * the form\n *\n * $PAGE->requires->js_call_amd($modulename, $functionname, $params)\n *\n * (e.g. from within render.php) or by JavaScript require calls, e.g. from\n * authorform.js when the question author changes UI type.\n *\n * The InterfaceWrapper provides:\n *\n * 1. A constructor InterfaceWrapper(uiname, textareaId) which\n * hides the given text area, replaces it with a wrapper div (resizable in\n * height by the user but with width resizing managed by changes in window\n * width), created an instance of nameInstance as defined in the file\n * ui_name.js (see below).\n * params is a record containing the decoded value of\n *\n * 2. A stop() method that destroys the embedded UI and hides the wrapper.\n *\n * 3. A restart() method that shows the wrapper again and re-creates the prior\n * embedded UI component within it.\n *\n * 4. A loadUi(uiname, params) method that kills any currently running UI element\n * (if there is one) and (re)loads the specified one. The params parameter\n * is a record that allows additional parameters to be passed in, such as\n * those from the question's uiParams field and, in the case of the\n * Ace UI, the 'lang' (language) that the editor is editing. This data\n * is supplied by the PHP via the data-params attribute of the answer's\n * base textarea.\n *\n * 5. Regular checking for any resizing of the wrapper, which are passed on to\n * the embedded UI element's resize() method.\n *\n * 6. Monitoring of alt-ctrl-M key presses which toggle the visibility of the\n * wrapper plus UI element and the syncronised textArea by calls to stop()\n * and restart\n *\n * =========================================================================\n *\n * The embedded user-interface module must be defined in a JavaScript file\n * of the form ui_name.js which must define a class nameInstance with\n * the following functionality:\n *\n * 1. A constructor SomeUiName(textareaId, width, height, params) that\n * builds an HTML component of the given width and height. textareaId is the\n * ID of the textArea from which the UI element should obtain its initial\n * serialisation and to which it should write the serialisation when its save\n * or destroy methods are called. params is a JavaScript object,\n * decoded from the JSON uiParams defined by the question plus any\n * additional data required, such as the 'lang' in the case of Ace.\n *\n * 2. A getElement() method that returns the HTML element that the\n * InterfaceWrapper is to insert into the document tree.\n *\n * 3. A method failed() that should return true unless the constructor\n * failed (e.g. because it was not able to de-serialise the text area's\n * contents). The wrapper will call destroy() on the object if failed()\n * returns true and abort the use of the UI element. The text area will\n * have the uiloadfailed class added, which CSS will display in some\n * error mode (e.g. a red border).\n *\n * 4. A method failMessage() that will be called only when failed() returns\n * True. It should be a defined CodeRunner language string key.\n *\n * 5. A sync() method that copies the serialised represention of the UI plugin's\n * data to the related TextArea. This is used when submit is clicked.\n *\n * 6. A destroy() method that should sync the contents to the text area then\n * destroy any HTML elements or other created content. This method is called\n * when CTRL-ALT-M is typed by the user to turn off all UI plugins\n *\n * 7. A resize(width, height) method that should resize the entire UI element\n * to the given dimensions.\n *\n * 8. A hasFocus() method that returns true if the UI element has focus.\n *\n * 9. A syncIntervalSecs() method that returns the time interval between\n * calls to the sync() method. 0 for no sync calls. The userinterfacewrapper\n * provides all instances with a generic (base-class) version that returns\n * the value of a UI parameter sync_interval_secs if given else uses the\n * UI interface wrapper default (currently 10).\n *\n * The return value from the module define is a record with a single field\n * 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)\n *\n *****************************************************************************/\n\n/**\n * 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 util.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\ndefine(['jquery'], function($) {\n /**\n * Constructor for a new user interface.\n * @param {string} uiname The name of the interface element (e.g. ace, graph, etc)\n * which should be in file ui_ace.js, ui_graph.js etc.\n * @param {string} textareaId The id of the text area that the UI is to manage.\n * The text area should have an attribute data-params, which is a\n * JSON encoded record containing whatever additional parameters might\n * be needed by the User interface. As a minimum it should contain all\n * the parameters from the uiparameters field of\n * the question so that question authors can pass in additional data\n * such as whether graph edges are bidirectional or not in the case of\n * the graph UI. Additionally the Ace editor requires a 'lang' field\n * to specify what language the editor is editing.\n * When the wrapper has been set up on a text area, the text area's\n * data attribute contains an entry for 'current-ui-wrapper' that is\n * a reference to the wrapper ('this').\n */\n function InterfaceWrapper(uiname, textareaId) {\n let t = this; // For use by embedded functions.\n\n this.GUTTER = 14; // Size of gutter at base of wrapper Node (pixels)\n this.DEFAULT_SYNC_INTERVAL_SECS = 5;\n\n const PIXELS_PER_ROW = 19; // For estimating height of textareas.\n const MAX_GROWN_ROWS = 50; // Upper limit to artifically grown textarea rows.\n const MIN_WRAPPER_HEIGHT = 50;\n\n this.taId = textareaId;\n this.loadFailId = textareaId + '_loadfailerr';\n const ta = document.getElementById(textareaId);\n this.textArea = $(ta);\n const params = this.textArea.attr('data-params');\n if (params) {\n this.uiParams = JSON.parse(params);\n } else {\n this.uiParams = {};\n }\n this.uiParams.lang = this.textArea.attr('data-lang');\n this.readOnly = this.textArea.prop('readonly');\n this.isLoading = false; // True if we're busy loading a UI element.\n this.loadFailed = false; // True if UI failed to initialise properly.\n this.retries = 0; // Number of failed attempts to load a UI component.\n\n let h = parseInt(this.textArea.css(\"height\"));\n let content_lines = this.textArea.val().split('\\n').length;\n let rows = ta.rows;\n if (content_lines > rows) {\n // Allow reloaded text areas with lots of text to grow bigger, within limits.\n rows = Math.min(content_lines, MAX_GROWN_ROWS);\n }\n h = Math.max(h, rows * PIXELS_PER_ROW, MIN_WRAPPER_HEIGHT);\n\n /**\n * Construct an empty hidden wrapper div, inserted directly after the\n * textArea, ready to contain the actual UI.\n */\n this.wrapperNode = $(\"\");\n this.textArea.after(this.wrapperNode);\n this.wrapperNode.hide();\n this.wrapperNode.css({\n resize: 'vertical',\n overflow: 'hidden',\n minHeight: h,\n width: \"100%\",\n border: \"1px solid darkgrey\"\n });\n\n /**\n * Record a reference to this wrapper in the text area's data attribute\n * for use by external javascript that needs to interact with the\n * wrapper, e.g. the multilanguage.js module.\n */\n this.textArea.data('current-ui-wrapper', this);\n\n /**\n * Load the UI into the wrapper (aysnchronous).\n */\n this.uiInstance = null; // Defined by loadUi asynchronously\n this.loadUi(uiname, this.uiParams); // Load the required UI element\n\n /**\n * Add event handlers\n */\n $(document).mousemove(function() {\n t.checkForResize();\n });\n $(window).resize(function() {\n t.checkForResize();\n });\n this.textArea.closest('form').submit(function() {\n if (t.uiInstance !== null) {\n t.uiInstance.sync();\n }\n });\n $(document.body).on('keydown', function(e) {\n const KEY_M = 77;\n if (e.keyCode === KEY_M && e.ctrlKey && e.altKey) {\n if (t.uiInstance !== null || t.loadFailed) {\n t.stop();\n } else {\n t.restart(); // Reactivate\n }\n }\n });\n }\n\n /**\n * Load the specified UI element (which in the case of Ace will need\n * to know the language, lang, as well - this must be supplied as\n * a 'lang' attribute of the record params.\n * When ui is up and running, this.uiInstance will reference it.\n * To avoid a potential race problem, if this method is already busy\n * with a load, try again in 200 msecs.\n * @param {string} uiname The name of the User Interface to be used.\n * @param {object} params The UI parameters object that passes parameters\n * to the actual UI object.\n */\n InterfaceWrapper.prototype.loadUi = function(uiname, params) {\n const t = this,\n errPart1 = 'Failed to load ',\n errPart2 = ' UI component. If this error persists, please report it to the forum on coderunner.org.nz';\n\n /**\n * Get the given language string and plug it into the given jQuery\n * div element as its html, plus a 'fallback' message on a separate line.\n * @param {string} langString The language string specifier for the error message,\n * to be loaded by AJAX.\n * @param {object} errorDiv The div object into which the error message\n * is to be inserted.\n */\n function setLoadFailMessage(langString, errorDiv) {\n require(['core/str'], function(str) {\n /**\n * Get langString text via AJAX\n */\n const\n s = str.get_string(langString, 'qtype_coderunner'),\n fallback = str.get_string('ui_fallback', 'qtype_coderunner');\n $.when(s, fallback).done(function(s, fallback) {\n errorDiv.html(s + ' ' + fallback);\n });\n });\n }\n\n /**\n * The default method for a UIs sync_interval_secs method.\n * Returns the sync_interval_secs parameter if given, else\n * DEFAULT_SYNC_INTERVAL_SECS.\n */\n function syncIntervalSecsBase() {\n if (params.hasOwnProperty('sync_interval_secs')) {\n return parseInt(params.sync_interval_secs);\n } else {\n return t.DEFAULT_SYNC_INTERVAL_SECS;\n }\n }\n\n if (this.isLoading) { // Oops, we're loading a UI element already\n this.retries += 1;\n if (this.retries > 20) {\n alert(errPart1 + uiname + errPart2);\n this.retries = 0;\n this.loading = 0;\n } else {\n setTimeout(function() {\n t.loadUi(uiname, params);\n }, 200); // Try again in 200 msecs\n }\n return;\n }\n this.retries = 0;\n this.params = params; // Save in case need to restart\n\n this.stop(); // Kill any active UI first\n this.uiname = uiname;\n\n if (this.uiname === '' || this.uiname === 'none' || sessionStorage.getItem('disableUis')) {\n this.uiInstance = null;\n } else {\n this.isLoading = true;\n require(['qtype_coderunner/ui_' + this.uiname],\n function(ui) {\n const h = t.wrapperNode.innerHeight() - t.GUTTER;\n const w = t.wrapperNode.innerWidth();\n const uiInstance = new ui.Constructor(t.taId, w, h, params);\n if (uiInstance.failed()) {\n /*\n * Constructor failed to load serialisation.\n * Set uiloadfailed class on text area.\n */\n t.loadFailed = true;\n t.wrapperNode.hide();\n uiInstance.destroy();\n t.uiInstance = null;\n t.textArea.addClass('uiloadfailed');\n const loadFailDiv = '';\n let jqLoadFailDiv = $(loadFailDiv);\n jqLoadFailDiv.insertBefore(t.textArea);\n setLoadFailMessage(uiInstance.failMessage(), jqLoadFailDiv); // Insert error by AJAX\n } else {\n t.hLast = 0; // Force resize (and hence redraw)\n t.wLast = 0; // ... on first call to checkForResize\n t.textArea.hide();\n t.wrapperNode.show();\n t.wrapperNode.append(uiInstance.getElement());\n t.uiInstance = uiInstance;\n t.loadFailed = false;\n t.checkForResize();\n\n /*\n * Set a default syncIntervalSecs method if uiInstance lacks one.\n */\n let uiInstancePrototype = Object.getPrototypeOf(uiInstance);\n uiInstancePrototype.syncIntervalSecs = uiInstancePrototype.syncIntervalSecs || syncIntervalSecsBase;\n t.startSyncTimer(uiInstance);\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Start a sync timer on the given uiInstance, unless its time interval is 0.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.startSyncTimer = function(uiInstance) {\n const timeout = uiInstance.syncIntervalSecs();\n if (timeout) {\n this.uiInstance.timer = setInterval(function () {\n uiInstance.sync();\n }, timeout * 1000);\n } else {\n this.uiInstance.timer = null;\n }\n };\n\n\n /**\n * Stop the sync timer on the given uiInstance, if running.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.stopSyncTimer = function(uiInstance) {\n if (uiInstance.timer) {\n clearTimeout(uiInstance.timer);\n }\n };\n\n\n InterfaceWrapper.prototype.stop = function() {\n /*\n * Disable (shutdown) the embedded ui component.\n * The wrapper remains active for ctrl-alt-M events, but is hidden.\n */\n if (this.uiInstance !== null) {\n this.stopSyncTimer(this.uiInstance);\n this.textArea.show();\n if (this.uiInstance.hasFocus()) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n this.uiInstance.destroy();\n this.uiInstance = null;\n this.wrapperNode.hide();\n }\n this.loadFailed = false;\n this.textArea.removeClass('uiloadfailed'); // Just in case it failed before\n $(document.getElementById(this.loadFailId)).remove();\n };\n\n /*\n * Re-enable the ui element (e.g. after alt-cntrl-M). This is\n * a full re-initialisation of the ui element.\n */\n InterfaceWrapper.prototype.restart = function() {\n if (this.uiInstance === null) {\n /**\n * Restart the UI component in the textarea\n */\n this.loadUi(this.uiname, this.params);\n }\n };\n\n\n /**\n * Check for wrapper resize - propagate to ui element.\n */\n InterfaceWrapper.prototype.checkForResize = function() {\n const SIZE_HACK = 25; // Horrible but best I can do. TODO: FIXME\n\n if (this.uiInstance) {\n const h = this.wrapperNode.innerHeight();\n const w = this.wrapperNode.innerWidth();\n if (h != this.hLast || w != this.wLast) {\n const xLeft = this.wrapperNode.offset().left;\n const maxWidth = $(window).innerWidth() - xLeft - SIZE_HACK;\n const hAdjusted = h - this.GUTTER;\n const wAdjusted = Math.min(maxWidth, w);\n this.uiInstance.resize(wAdjusted, hAdjusted);\n this.hLast = this.wrapperNode.innerHeight();\n this.wLast = this.wrapperNode.innerWidth();\n }\n }\n };\n\n /**\n * The external entry point from the PHP.\n * @param {string} uiname The name of the User Interface to use e.g. 'ace'\n * @param {string} textareaId The ID of the textarea to be wrapped.\n */\n function newUiWrapper(uiname, textareaId) {\n if (uiname) {\n return new InterfaceWrapper(uiname, textareaId);\n } else {\n return null;\n }\n }\n\n\n return {\n newUiWrapper: newUiWrapper,\n InterfaceWrapper: InterfaceWrapper\n };\n});\n"],"names":["define","$","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","taId","loadFailId","ta","document","getElementById","textArea","params","attr","uiParams","JSON","parse","lang","readOnly","prop","isLoading","loadFailed","retries","h","parseInt","css","content_lines","val","split","length","rows","Math","min","max","wrapperNode","after","hide","resize","overflow","minHeight","width","border","data","uiInstance","loadUi","mousemove","checkForResize","window","closest","submit","sync","body","on","e","keyCode","ctrlKey","altKey","stop","restart","prototype","syncIntervalSecsBase","hasOwnProperty","sync_interval_secs","alert","loading","setTimeout","sessionStorage","getItem","require","ui","innerHeight","w","innerWidth","Constructor","failed","destroy","addClass","loadFailDiv","jqLoadFailDiv","insertBefore","langString","failMessage","errorDiv","str","s","get_string","fallback","when","done","html","hLast","wLast","show","append","getElement","uiInstancePrototype","Object","getPrototypeOf","syncIntervalSecs","startSyncTimer","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","remove","xLeft","offset","left","maxWidth","hAdjusted","wAdjusted","newUiWrapper"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/outputdisplayarea.js b/amd/src/outputdisplayarea.js
new file mode 100644
index 000000000..247fca51f
--- /dev/null
+++ b/amd/src/outputdisplayarea.js
@@ -0,0 +1,203 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more util.details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+/**
+ * @module coderunner/outputdisplayarea
+ * @copyright James Napier, 2023, The University of Canterbury
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import $ from 'jquery';
+
+import ajax from 'core/ajax';
+import {get_string as getLangString} from 'core/str';
+
+
+/**
+ * Get the specified language string using
+ * AJAX and plug it into the given textarea
+ * @param {string} langStringName The language string name.
+ * @param {DOMnode} textarea The textarea into which the error message
+ * should be plugged.
+ * @param {string} additionalText Extra text to follow the result code.
+ */
+const setLangString = async(langStringName, textarea, additionalText) => {
+ const message = await getLangString(langStringName, 'filter_ace_inline');
+ textarea.show();
+ textarea.html(escapeHtml("*** " + message + " ***\n" + additionalText));
+};
+
+const diagnoseWebserviceResponse = response => {
+ // Table of error conditions.
+ // Each row is response.error, response.result, langstring
+ // response.result is ignored if response.error is non-zero.
+ // Any condition not in the table is deemed an "Unknown runtime error".
+ const ERROR_RESPONSES = [
+ [1, 0, 'Sandbox access denied.'], // Sandbox AUTH_ERROR
+ [2, 0, 'error_unknown_language'], // Sandbox WRONG_LANG_ID
+ [3, 0, 'Sandbox access denied.'], // Sandbox ACCESS_DENIED
+ [4, 0, 'error_submission_limit_reached'], // Sandbox SUBMISSION_LIMIT_EXCEEDED
+ [5, 0, 'Sandbox overload. Please wait and try again later'], // Sandbox SERVER_OVERLOAD
+ [0, 11, ''], // RESULT_COMPILATION_ERROR
+ [0, 12, 'Scratchpad crashed. Out of memory, perhaps?'], // RESULT_RUNTIME_ERROR (supervisor process broke)
+ [0, 13, 'Scratchpad time limit error. Please report'], // RESULT TIME_LIMIT (supervisor process broke)
+ [0, 15, ''], // RESULT_SUCCESS
+ [0, 17, 'Scratchpad memory limit error. Please report'], // RESULT_MEMORY_LIMIT
+ [0, 21, 'Sandbox overload. Please wait and try again later'], // RESULT_SERVER_OVERLOAD
+ [0, 30, 'Excessive output.'] // RESULT OUTPUT_LIMIT
+ ];
+ for (let i = 0; i < ERROR_RESPONSES.length; i++) {
+ let row = ERROR_RESPONSES[i];
+ if (row[0] == response.error && (response.error != 0 || response.result == row[1])) {
+ return row[2];
+ }
+ }
+ return 'error_unknown_runtime'; // We're dead, Fred.
+};
+
+const getImage = base64 => {
+ let image = $(``);
+ return image;
+};
+
+/**
+ *
+ */
+class OutputDisplayArea {
+ constructor(displayAreaId, outputMode) {
+ this.displayAreaId = displayAreaId;
+ this.displayArea = document.getElementById(displayAreaId);
+ this.textDisplay = document.getElementById(displayAreaId + '-text');
+ this.htmlDisplay = document.getElementById(displayAreaId + '-html');
+ this.imageDisplay = document.getElementById(displayAreaId + '-images');
+ this.mode = outputMode;
+ this.stdIn = [];
+ }
+
+ clearDisplay() {
+ this.textDisplay.innerHTML = "";
+ this.htmlDisplay.innerHTML = "";
+ this.imageDisplay.innerHTML = "";
+ }
+
+ displayText(response) {
+ const output = response.output;
+ const error = response.stderr;
+ this.textDisplay.innerText = output + error;
+ }
+
+ displayHtml(response) {
+ const output = response.output;
+ const error = response.stderr;
+ this.textDisplay.innerHTML = output + error;
+ }
+
+ displayNoOutput(response) {
+ const isNoOutput = response.output === '' && response.stderr === '';
+ if (isNoOutput) {
+ this.textDisplay.innerHTML = '< No output! >';
+ }
+ return isNoOutput;
+ }
+
+ display(response) {
+ if (this.displayNoOutput(response)) {
+ return;
+ }
+ if (this.mode === 'json') {
+ // TODO: error handling.
+ const json = JSON.parse(response);
+ this.displayJson(json);
+ } else if (this.mode === 'html') {
+ this.displayHtml(response);
+ } else if (this.mode === 'text') {
+ const text = response;
+ this.displayText(text);
+ } else {
+ throw Error(`Invalid outputMode given: "${this.mode}"`);
+ }
+ }
+
+ async handleRunButtonClick(code, lang, sandboxParams) {
+ this.clearDisplay();
+ ajax.call([{
+ methodname: 'qtype_coderunner_run_in_sandbox',
+ args: {
+ contextid: M.cfg.contextid, // Moodle context ID
+ sourcecode: code,
+ language: lang,
+ params: JSON.stringify(sandboxParams) // Sandbox params
+ },
+ done: (responseJson) => {
+ // TODO: error handling.
+ const response = JSON.parse(responseJson);
+ // Const error = diagnoseWebserviceResponse(response);
+ this.display(response);
+ },
+ fail: (error) => {
+ alert(error.message);
+ }
+ }]);
+ }
+
+ displayJson(json) {
+ var result = json;
+ var text = result.stdout;
+
+ if (result.returncode !== 42) {
+ text += result.stderr;
+ }
+ if (result.returncode == 13) { // Timeout
+ text += "\n*** Timeout error ***\n";
+ }
+
+ var numImages = 0;
+ if (result.files) {
+ $(this.imageDisplay).empty();
+ for (var prop in result.files) {
+ var image = getImage(result.files[prop]);
+ $(this.imageDisplay).append(image);
+ numImages += 1;
+ }
+ }
+
+ if (text.trim() === '' && result.returncode !== 42) {
+ if (numImages == 0) {
+ $(this.textDisplay).html('< No output! >');
+ }
+ } else {
+ $(this.textDisplay).text(text);
+ }
+
+ if (result.returncode === 42) {
+ var inputId = `${this.displayAreaId}-input-field`;
+ $(this.textDisplay).html($(this.textDisplay).html() + ``);
+ var inputEl = $(document.getElementById(inputId));
+ inputEl.focus();
+
+ inputEl.on('keyup', (e) => {
+ if (e.keyCode === 13) {
+ const line = inputEl.val();
+ inputEl.remove();
+ $(this.textDisplay).html($(this.textDisplay).html() + line);
+ this.handleRunButtonClick();
+ }
+ });
+ }
+ }
+}
+
+export {
+ OutputDisplayArea
+};
diff --git a/amd/src/ui_scratchpad.js b/amd/src/ui_scratchpad.js
index 934725939..64808a1ca 100644
--- a/amd/src/ui_scratchpad.js
+++ b/amd/src/ui_scratchpad.js
@@ -14,149 +14,65 @@
// along with Moodle. If not, see .
/**
- * Implementation of the html_ui user interface plugin. For overall details
+ * Implementation of the scratchpad_ui user interface plugin. For overall details
* of the UI plugin architecture, see userinterfacewrapper.js.
*
- * This plugin replaces the usual textarea answer element with a div
- * containing the author-supplied HTML. The serialisation of that HTML,
- * which is what is essentially copied back into the textarea for submissions
- * as the answer, is a JSON object. The fields of that object are the names
- * of all author-supplied HTML elements with a class 'coderunner-ui-element';
- * all such objects are expected to have a 'name' attribute as well. The
- * associated field values are lists. Each list contains all the values, in
- * document order, of the results of calling the jquery val() method in turn
- * on each of the UI elements with that name.
- * This means that at least input, select and textarea
- * elements are supported. The author is responsible for checking the
- * compatibility of other elements with jquery's val() method.
+ * This plugin replaces the usual textarea answer element with a UI is designed to
+ * allow the execution of code in the CodeRunner question in a manner similar to an IDE.
+ * It contains two editor boxes, one on top of another, allowing users to enter and
+ * edit code in both.
+ * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,
+ * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,
+ * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that
+ * provides information about how to use the Scratchpad.
+ * It's possible to run code 'in-browser' by clicking the Run Button,
+ * without making a submission via the Check Button:
+ * If Prefix with Answer is not checked, only the code in the Scratchpad is run --
+ * allowing for a rough working spot to quickly check the result of code.
+ * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is
+ * appended to the code in the first editor before being run.
+ * The Run Button has some limitations when using its default configuration:
+ * Does not support programs that use STDIN (by default);
+ * Only supports textual STDOUT (by default).
+ * Note: These features can be supported, see the README section on wrappers...
+ * The serialisation of this UI is a JSON object with the fields
+ * with fields:
+ * answer_code: [""] A list containing a string with answer code from the first editor;
+ * test_code: [""] A list containing a string with containing answer code from the second editor;
+ * show_hide: ["1"] when scratchpad is visible, otherwise [""];
+ * prefix_ans: ["1"] when Prefix with Answer is checked, otherwise [""].
+ * . The fields of that object are the names
*
- * The HTML to use in the answer area must be provided as the contents of
- * either the globalextra field or the prototypeextra field in the question
- * authoring form. The choice of which is set by the html_src UI parameter, which
- * must be either 'globalextra' or 'prototypeextra'.
+ * UI Parameters:
+ * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.
+ * - button_name: run button text.
+ * - prefix_name: prefix with answer check-box label text.
+ * - help_text: help text to show.
+ * - run_lang: language used to run code when the run button is clicked,
+ * this should be the language your wrapper is written in (if applicable).
+ * - wrapper_src: location of wrapper code to be used by the run button, if applicable:
+ * setting to globalextra will use text in global extra field,
+ * - prototypeextra will use the prototype extra field.
+ * - html_output: when true, the output from run will be displayed as raw HTML instead of text.
+ * - disable_scratchpad: disable the scratchpad, effectively returning back to Ace UI
+ * from student perspective.
+ * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.
+ * This can be used to swap the default state.
*
- * If any fields of the answer html are to be preloaded, these should be specified
- * in the answer preload with json of the form '{"": "",...}'
- * where fieldValueList is a list of all the values to be assigned to the fields
- * with the given name, in document order.
- *
- * To accommodate the possibility of dynamic HTML, any leftover preload values,
- * that is, values that cannot be positioned within the HTML either because
- * there is no field of the required name or because, in the case of a list,
- * there are insufficient elements, are assigned to the data['leftovers']
- * attribute of the outer html div, as a sub-object of the original object.
- * This outer div can be located as the 'closest' (in a jQuery sense)
- * div.qtype-coderunner-html-outer-div. The author-supplied HTML must include
- * JavaScript to make use of the 'leftovers'.
- *
- * As a special case of the serialisation, if all values in the serialisation
- * are either empty strings or a list of empty strings, the serialisation is
- * itself the empty string.
- *
- * @module coderunner/ui_html
+ * @module coderunner/ui_scratchpad
* @copyright Richard Lobb, 2022, The University of Canterbury
* @copyright James Napier, 2022, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-import $ from 'jquery';
+
import ajax from 'core/ajax';
-import {get_string as getLangString} from 'core/str';
import Templates from 'core/templates';
import {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';
+import {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';
-const RESULT_SUCCESS = 15; // Code for a correct Jobe run.
-const DEFAULT_MAX_OUTPUT_LEN = 30000;
-
-
-/**
- * Escape text special HTML characters.
- * @param {string} text
- * @returns {string} text with various special chars replaced with equivalent
- * html entities. Newlines are replaced with .
- */
-const escapeHtml = text => {
- const map = {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": '''
- };
- return text.replace(/[&<>"']/g, function(m) {
- return map[m];
- });
-};
-
-/**
- * Analyse the response for errors. There are two sorts of error: sandbox failures,
- * for which the field response.error is non-zero meaning the run didn't take
- * place at all and failures in the run
- * itself, such as compile errors, timeouts, runtime errors etc. The
- * various codes are documented in the CodeRunner file sandbox.php.
- * Some error returns, notably compilation error and runtime error, are not
- * treated as errors here, since the stdout + stderr should reveal what
- * happened anyway. More obscure errors are lumped together as 'Unknown
- * runtime error'.
- * @param {object} response The response from the web-service sandbox request.
- * @returns string The language string to use for an error message or '' if
- * no error message.
- */
-const diagnose = response => {
- // Table of error conditions.
- // Each row is response.error, response.result, langstring
- // response.result is ignored if response.error is non-zero.
- // Any condition not in the table is deemed an "Unknown runtime error".
- const ERROR_RESPONSES = [
- [1, 0, 'error_access_denied'], // Sandbox AUTH_ERROR
- [2, 0, 'error_unknown_language'], // Sandbox WRONG_LANG_ID
- [3, 0, 'error_access_denied'], // Sandbox ACCESS_DENIED
- [4, 0, 'error_submission_limit_reached'], // Sandbox SUBMISSION_LIMIT_EXCEEDED
- [5, 0, 'error_sandbox_server_overload'], // Sandbox SERVER_OVERLOAD
- [0, 11, ''], // RESULT_COMPILATION_ERROR
- [0, 12, ''], // RESULT_RUNTIME_ERROR
- [0, 13, 'error_timeout'], // RESULT TIME_LIMIT
- [0, RESULT_SUCCESS, ''], // RESULT_SUCCESS
- [0, 17, 'error_memory_limit'], // RESULT_MEMORY_LIMIT
- [0, 21, 'error_sandbox_server_overload'], // RESULT_SERVER_OVERLOAD
- [0, 30, 'error_excessive_output'] // RESULT OUTPUT_LIMIT
- ];
- for (const row of ERROR_RESPONSES) {
- if (row[0] == response.error && (response.error != 0 || response.result == row[1])) {
- return row[2];
- }
- }
- return 'error_unknown_runtime';
-};
-
-/**
- * Get the specified language string using
- * AJAX and plug it into the given textarea
- * @param {string} langStringName The language string name.
- * @param {DOMnode} textarea The textarea into which the error message
- * should be plugged.
- * @param {string} additionalText Extra text to follow the result code.
- */
-const setLangString = async(langStringName, textarea, additionalText) => {
- const message = await getLangString(langStringName, 'filter_ace_inline');
- textarea.show();
- textarea.html(escapeHtml("*** " + message + " ***\n" + additionalText));
-};
-
-/**
- * Concatenates the cmpinfo, stdout and stderr fields of the sandbox
- * response, truncating both stdout and stderr to a given maximum length
- * if necessary (in which case '... (truncated)' is appended.
- * @param {object} response Sandbox response object
- * @param {int} maxLen The maximum length of the trimmed stringlen.
- */
-const combinedOutput = (response, maxLen) => {
- const limit = s => s.length <= maxLen ? s : s.substr(0, maxLen) + '... (truncated)';
- return response.cmpinfo + limit(response.output) + limit(response.stderr);
-};
-
/**
* Invert serialisation from '1' to '', vice versa.
* @param {string} current serialisation.
@@ -177,7 +93,7 @@ const invertSerial = current => current[0] == '1' ? [''] : ['1'];
const fillWrapper = (answerCode, testCode, prefixAns, template) => {
if (!template) {
template = '{{ ANSWER_CODE }}\n' +
- '{{ SCRATCHPAD_CODE }}';
+ '{{ SCRATCHPAD_CODE }}';
}
if (!prefixAns) {
answerCode = '';
@@ -188,7 +104,7 @@ const fillWrapper = (answerCode, testCode, prefixAns, template) => {
};
/**
- * Returns anew object containg default values. If a matching key exists in
+ * Returns a new object contain default values. If a matching key exists in
* prescribed, the corresponding value from prescribed will replace the defualt value.
* Does not add keys/values to the result if that key is not in defualts.
* @param {object} defaults object with values to be overwritten.
@@ -205,17 +121,19 @@ const overwriteValues = (defaults, prescribed) => {
return overwritten;
};
-
/**
- * Is an element currently hidden?
- * @param {Element} el to check visibility of.
- * @returns {boolean} true if el is visible.
+ * Is a collapsed element currently collapsed?
+ * @param {Element} el which is collapsed using a bootstrap collapse.
+ * @returns {boolean} true if el is collapsed.
*/
-const isVisible = (el) => {
- const styles = window.getComputedStyle(el);
- return styles.display === 'none' || styles.visibility === 'hidden';
+const isCollapsed = (el) => {
+ if (!el.classList.contains('collapse')) {
+ throw Error('Element does not have collapse class');
+ }
+ return !el.classList.contains('show');
};
+
/**
* Constructor for the ScratchpadUi object.
* @param {string} textAreaId The ID of the html textarea.
@@ -235,34 +153,33 @@ class ScratchpadUi {
disable_scratchpad: false,
wrapper_src: null
};
-
this.textArea = document.getElementById(textAreaId);
this.textAreaId = textAreaId;
this.height = height;
this.readOnly = this.textArea.readonly;
this.fail = false;
-
- this.lang = uiParams.lang;
-
- uiParams.num_rows = this.textArea.readOnly;
+ this.outerDiv = null;
+ this.outputDisplay = null;
+ this.invertPreload = uiParams.invert_prefix;
+ this.lang = uiParams.lang; // Todo: this vs this.ui params
+ this.numRows = this.textArea.rows;
this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);
+ this.runWrapper = this.getRunWrapper();
+ this.reload(); // Draw my beautiful blobs.
+ }
- // Find the run wrapper source location.
- this.runWrapper = null;
+ getRunWrapper() {
const wrapperSrc = this.uiParams.wrapper_src;
+ let runWrapper = null;
if (wrapperSrc) {
if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {
- this.runWrapper = this.textArea.dataset[wrapperSrc];
+ runWrapper = this.textArea.dataset[wrapperSrc];
} else {
- // TODO: raise some sort of exception? Invalid, params.
- // Bad wrapper src provided by user...
- this.runWrapper = null;
+ this.fail = true;
+ this.failString = 'scratchpad_ui_badrunwrappersrc';
}
}
-
- this.outerDiv = null; // TODO: Investigate...
- this.scratchpadDiv = null;
- this.reload(); // Draw my beautiful blobs.
+ return runWrapper;
}
failed() {
@@ -277,9 +194,13 @@ class ScratchpadUi {
if (!this.context) {
return;
}
+ const serialisation = this.getSerialisation();
+ this.setSerialisation(serialisation);
+ }
+
+ getSerialisation() {
const prefixAns = document.getElementById(this.context.prefix_ans.id);
const showHide = document.getElementById(this.context.show_hide.id);
-
let serialisation = {
answer_code: [''],
test_code: [''],
@@ -292,13 +213,19 @@ class ScratchpadUi {
if (this.testTextarea) {
serialisation.test_code = [this.testTextarea.value];
}
- if (!isVisible(showHide)) {
+ if (showHide && !isCollapsed(showHide)) {
serialisation.show_hide = ['1'];
}
- if (prefixAns?.checked) {
+ if (prefixAns?.checked || this.context.disable_scratchpad) {
serialisation.prefix_ans = ['1'];
}
+ if (this.invertPreload) {
+ serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);
+ }
+ return serialisation;
+ }
+ setSerialisation(serialisation) {
serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);
if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {
serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);
@@ -312,76 +239,20 @@ class ScratchpadUi {
return this.outerDiv;
}
- async handleRunButtonClick(ajax, outputDisplayArea) {
- outputDisplayArea = $(outputDisplayArea);
+ handleRunButtonClick() {
this.sync(); // Use up-to-date serialization.
-
- const htmlOutput = this.uiParams.html_output;
- const maxLen = this.uiParams['max-output-length'] || DEFAULT_MAX_OUTPUT_LEN;
- const preloadString = $(this.textArea).val();
- const serial = this.serialize(preloadString);
- const params = this.uiParams.params;
+ const preloadString = this.textArea.value;
+ const serial = this.readJson(preloadString);
+ const sandboxParams = this.uiParams.params;
+ const language = this.lang;
const code = fillWrapper(
serial.answer_code,
serial.test_code,
serial.prefix_ans[0],
this.runWrapper
- );
-
- // Clear all output areas.
- outputDisplayArea.html('');
- if (htmlOutput) {
- outputDisplayArea.hide();
- }
- outputDisplayArea.next('div.filter-ace-inline-html').remove(); // TODO: Naming
-
-
- ajax.call([{
- methodname: 'qtype_coderunner_run_in_sandbox',
- args: {
- contextid: M.cfg.contextid, // Moodle context ID
- sourcecode: code,
- language: this.uiParams.run_lang,
- params: JSON.stringify(params) // Sandbox params
- },
- done: function(responseJson) {
- const response = JSON.parse(responseJson);
- const error = diagnose(response);
- if (error === '') {
- // If no errors or compilation error or runtime error
- if (!htmlOutput || response.result !== RESULT_SUCCESS) {
- // Either it's not HTML output or it is but we have compilation or runtime errors.
- const text = combinedOutput(response, maxLen);
- outputDisplayArea.show();
- if (text.trim() === '') {
- outputDisplayArea.html('< No output! >');
- } else {
- outputDisplayArea.html(escapeHtml(text));
- }
- } else { // Valid HTML output - just plug in the raw html to the DOM.
- // Repeat the deletion of previous output in case of multiple button clicks.
- outputDisplayArea.next('div.filter-ace-inline-html').remove();
-
- const html = $("
" +
- response.output + "
");
- outputDisplayArea.after(html);
- }
- } else {
- // If an error occurs, display the language string in the
- // outputDisplayArea plus additional info.
- let extra = response.error == 0 ? combinedOutput(response, maxLen) : '';
- if (error === 'error_unknown_runtime') {
- extra += response.error ? '(Sandbox error code ' + response.error + ')' :
- '(Run result: ' + response.result + ')';
- }
- setLangString(error, outputDisplayArea, extra);
- }
- },
- fail: function(error) {
- alert(error.message);
- }
- }]);
+ );
+ // TODO: handle case where no output display area exists...
+ this.outputDisplay.handleRunButtonClick(code, language, sandboxParams);
}
updateContext(preload) {
@@ -390,16 +261,17 @@ class ScratchpadUi {
"disable_scratchpad": this.uiParams.disable_scratchpad,
"scratchpad_name": this.uiParams.scratchpad_name,
"button_name": this.uiParams.button_name,
- "prefix_name": this.uiParams.prefix_name,
- "help_text": {"text": this.uiParams.help_text}, // TODO: context doesnt match...
+ "help_text": {"text": this.uiParams.help_text},
"answer_code": {
"id": this.textAreaId + '_answer-code',
+ "name": "answer_code",
"text": preload.answer_code[0],
"lang": this.lang,
- "rows": this.uiParams.rows
+ "rows": this.numRows
},
"test_code": {
"id": this.textAreaId + '_test-code',
+ "name": "test_code",
"text": preload.test_code[0],
"lang": this.lang,
"rows": 6
@@ -410,31 +282,46 @@ class ScratchpadUi {
},
"prefix_ans": {
"id": this.textAreaId + '_prefix-ans',
+ "label": this.uiParams.prefix_name,
"checked": preload.prefix_ans[0]
},
"output_display": {
- "id": this.textAreaId + '_output-displayarea'
+ "id": this.textAreaId + '_run-output'
},
+ // Bootstrap collapse requires jQuerry friendly ids to work...
"jquery_escape": function() {
return function(text, render) {
- return $.escapeSelector(render(text));
+ return CSS.escape(render(text));
};
}
};
}
- serialize(preloadString) {
+ readJson(preloadString) {
const defaultSerial = {
- answer_code: [''],
- test_code: [''],
- show_hide: [''],
- prefix_ans: ['1'] // Ticked by default!
+ "answer_code": [''],
+ "test_code": [''],
+ "show_hide": [''],
+ "prefix_ans": ['1'] // Ticked by default!
};
let serial;
- if (preloadString) {
- serial = JSON.parse(preloadString);
+ if (preloadString !== "") {
+ try {
+ serial = JSON.parse(preloadString);
+ } catch {
+ // Preload is not JSON, so use preloaded string as answer_code.
+ serial = {"answer_code": [preloadString]};
+ }
+ if (!serial.hasOwnProperty("answer_code")) {
+ // No student_answer field... something is wrong!
+ throw TypeError("JSON has wrong signature, missing answer_code field.");
+ }
}
serial = overwriteValues(defaultSerial, serial);
+
+ if (this.invertPreload) {
+ serial.prefix_ans = invertSerial(serial.prefix_ans);
+ }
return serial;
}
@@ -442,44 +329,52 @@ class ScratchpadUi {
const preloadString = this.textArea.value;
let preload;
try {
- preload = this.serialize(preloadString);
+ preload = this.readJson(preloadString);
} catch (error) {
this.fail = true;
this.failString = 'scratchpad_ui_invalidserialisation';
return;
}
-
this.updateContext(preload);
-
try {
const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);
-
- const div = document.createElement('div');
- div.innerHTML = html;
- document.getElementById(this.textAreaId)
- .nextSibling
- .innerHTML = html;
- this.answerTextarea = document.getElementById(this.context.answer_code.id);
- this.testTextarea = document.getElementById(this.context.test_code.id);
-
- this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);
- if (this.testTextarea) {
- this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);
- }
-
- const runButton = document.getElementById(this.textAreaId + '_run-btn');
- const outputDisplayarea = document.getElementById(this.context.output_display.id);
- if (runButton) {
- runButton.addEventListener('click', () => this.handleRunButtonClick(ajax, outputDisplayarea));
- }
+ const outputMode = this.uiParams.html_output ? 'html' : 'text';
+ this.drawUi(html);
+ this.addAceUis();
+ this.outputDisplay = new OutputDisplayArea(this.context.output_display.id, outputMode); // TODO: change!
+ this.addEventListeners();
} catch (e) {
this.fail = true;
- this.failString = "UI template failed to load."; // TODO: Lang-string goes here.
+ this.failString = "scratchpad_ui_templateloadfail";
+
+ return;
}
+ }
+ drawUi(html) {
+ const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;
+ wrapperDiv.innerHTML = html;
+ this.outerDiv = wrapperDiv.firstChild;
// No resizing the outer wrapper. Instead, resize the two sub UIs,
// they will expand accordingly.
- document.getElementById(this.textAreaId + '_wrapper').style.resize = 'none';
+ wrapperDiv.style.resize = 'none';
+ }
+
+ addAceUis() {
+ this.answerTextarea = document.getElementById(this.context.answer_code.id);
+ this.testTextarea = document.getElementById(this.context.test_code.id);
+ this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);
+ if (this.testTextarea) {
+ this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);
+ }
+ }
+
+ addEventListeners() {
+ const runButton = document.getElementById(this.textAreaId + '_run-btn');
+ const outputDisplayarea = document.getElementById(this.context.output_display.id);
+ if (runButton) {
+ runButton.addEventListener('click', () => this.handleRunButtonClick(ajax, outputDisplayarea));
+ }
}
resize() {} // Nothing to see here. Move along please.
@@ -497,6 +392,8 @@ class ScratchpadUi {
destroy() {
this.sync();
+ this.answerCodeUi?.uiInstance.destroy();
+ this.testCodeUiCodeUi?.uiInstance.destroy();
this.outerDiv?.remove();
this.outerDiv = null;
}
diff --git a/amd/src/ui_scratchpad.json b/amd/src/ui_scratchpad.json
index 5dedf9fd1..b3f0262e9 100644
--- a/amd/src/ui_scratchpad.json
+++ b/amd/src/ui_scratchpad.json
@@ -36,6 +36,10 @@
"disable_scratchpad": {
"type": "boolean",
"default": false
+ },
+ "invert_prefix": {
+ "type": "boolean",
+ "default": false
}
}
}
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index cd0b8834b..3c73a0f00 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -1245,16 +1245,23 @@ function should be applied, e.g. {{STUDENT_ANSWER | e(\'py\')}} is
$string['scratchpadui_scratchpad_name_descr'] = 'The display name of the scratchpad, used to hide/un-hide the scratchpad.';
-$string['scratchpadui_button_name_descr'] = 'The run button text';
+$string['scratchpadui_button_name_descr'] = 'The run button text.';
$string['scratchpadui_prefix_name_descr'] = 'The prefix with answer check-box label text.';
$string['scratchpadui_run_lang_descr'] = 'The language used to run code when the run button is clicked, this should be the language your wrapper is written in (if applicable).';
-$string['scratchpadui_run_wrapper_descr'] = 'The wrapper to be used by the run button: setting to `globalextra` will use text in global extra as the wrapper. Otherwise, the string in this parameter will be used';
$string['scratchpadui_params_descr'] = 'The prefix with answer check-box label text.';
$string['scratchpadui_html_output_descr'] = 'Display the output from run as raw HTML instead of text.';
-
-$string['scratchpadui_def_button_name'] = 'Run!';
+$string['scratchpadui_help_text_descr'] = 'The help text to show.';
+$string['scratchpadui_wrapper_src_descr'] = 'The location of wrapper to be used by the run button: setting to \'globalextra\' will use text in global extra field, \'prototypeextra\' will use the prototype extra field.';
+$string['scratchpadui_disable_scratchpad_descr'] = 'Disable the scratchpad, effectively revert back to Ace UI from student perspective.';
+$string['scratchpadui_invert_prefix_descr'] = 'Inverts meaning of prefix_ans serialisation -- \'1\' means un-ticked, vice versa. This can be used to swap the default state.';
+$string['scratchpadui_def_button_name'] = 'Run';
$string['scratchpadui_def_scratchpad_name'] = 'Scratchpad';
$string['scratchpadui_def_prefix_name'] = 'Prefix with Answer';
+
$string['scratchpadui_def_help_text'] = 'You can enter code into this panel and click \'Run\' to execute it.
-By default, the code in this panel is prefixed with the contents of the answer box, giving you an easy way to test your answer.
+By default, the code in this panel is prefixed with the contents of the answer box, giving you an easy way to test your answer.
You can uncheck the \'Prefix with answer\' checkbox to run the code in this panel standalone, e.g. to explore how small code fragments behave.';
+
+$string['scratchpad_ui_invalidserialisation'] = 'Invalid JSON serialisation provided, must include \"answer_code\" field.';
+$string['scratchpad_ui_templateloadfail'] = 'Scratchpad UI template failed to load, please refresh the page.';
+$string['scratchpad_ui_badrunwrappersrc'] = 'Invalid run wrapper source given, please contact question author.';
diff --git a/samples/input_mpl_wrapper.py b/samples/input_mpl_wrapper.py
new file mode 100644
index 000000000..ce7288608
--- /dev/null
+++ b/samples/input_mpl_wrapper.py
@@ -0,0 +1,117 @@
+import ast, traceback, sys, io, subprocess, base64, os, ast, traceback, json
+MAX_OUTPUT_CHARS = 30000
+
+student_code = """
+{{ STUDENT_ANSWER }}
+{{ TEST_CODE }}
+"""
+
+uses_matplotlib = 'matplotlib' in student_code
+
+subproc_code = """
+import sys
+MAX_OUTPUT_CHARS = 30000
+__saved_input__ = input
+
+def input(prompt=''):
+ try:
+ line = __saved_input__()
+ except EOFError:
+ print(prompt, end = '')
+ sys.stderr.flush()
+ sys.stdout.flush()
+ sys.exit(42)
+ print(prompt, end='')
+ print(line)
+ return line
+
+__saved_print__ = print
+__output_chars__ = 0
+
+def print(*params, **keyparams):
+ global __output_chars__
+ for param in params:
+ try:
+ __output_chars__ += len(str(param))
+ except:
+ pass
+ if __output_chars__ > 2 * MAX_OUTPUT_CHARS:
+ __saved_print__("\\\\n*** Excessive output. Job aborted ***", file=sys.stderr)
+ sys.exit(1)
+ else:
+ __saved_print__(*params, **keyparams)
+"""
+
+if uses_matplotlib:
+ subproc_code += """
+import os, tempfile, sys
+os.environ["MPLCONFIGDIR"] = tempfile.mkdtemp()
+import matplotlib as _mpl
+_mpl.use("Agg")
+"""
+
+subproc_code += student_code
+
+if uses_matplotlib:
+ subproc_code += """
+figs = _mpl.pyplot.get_fignums()
+for i, fig in enumerate(figs):
+_mpl.pyplot.figure(fig)
+filename = f'image{i}.png'
+_mpl.pyplot.savefig(filename, bbox_inches='tight')
+"""
+
+
+
+
+
+def b64encode(filename):
+ """Return the contents of the given file in base64"""
+ with open(filename, "br") as fin:
+ contents = fin.read()
+ contents_b64 = base64.b64encode(contents).decode("utf8")
+ return contents_b64
+
+def truncated(s):
+ return s if len(s) < MAX_OUTPUT_CHARS else s[:MAX_OUTPUT_CHARS] + ' ... (truncated)'
+
+def check_syntax():
+ try:
+ ast.parse("""${escapedCode}""")
+ return ''
+ except SyntaxError:
+ catcher = io.StringIO()
+ traceback.print_exc(limit=0, file=catcher)
+ return catcher.getvalue()
+
+stdout = ''
+stderr = check_syntax()
+if stderr == '': # No syntax errors
+ program_code = """${subprocCode}""";
+ with open('prog.py', 'w') as outfile:
+ outfile.write(program_code)
+ proc = subprocess.Popen([sys.executable, 'prog.py'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+ try:
+ stdout, stderr = proc.communicate(timeout=3)
+ returncode = proc.returncode
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ stdout, stderr = proc.communicate()
+ returncode = 13
+
+
+else:
+ returncode = 1 # Syntax errors
+
+# Pick up any .png or .jpg image files.
+image_extensions = ['png', 'jpg', 'jpeg']
+image_files = [fname for fname in os.listdir() if fname.lower().split('.')[-1] in image_extensions]
+files = {fname: b64encode(fname) for fname in image_files}
+
+output = {'returncode': returncode,
+ 'stdout' : truncated(stdout),
+ 'stderr' : truncated(stderr),
+ 'files' : files,
+}
+
+print(json.dumps(output))
\ No newline at end of file
diff --git a/templates/answer_textarea.mustache b/templates/answer_textarea.mustache
index 826940eb1..24f2ffedc 100644
--- a/templates/answer_textarea.mustache
+++ b/templates/answer_textarea.mustache
@@ -22,7 +22,7 @@
Example context (json):
{
"id": "question:12",
- "text": "print('hello world!')"
+ "text": "print('hello world!')",
"rows": 16,
"name": "answer_code",
"lang": "C"
diff --git a/templates/output_displayarea.mustache b/templates/output_displayarea.mustache
index c8f92106d..2a1f946a0 100644
--- a/templates/output_displayarea.mustache
+++ b/templates/output_displayarea.mustache
@@ -21,10 +21,15 @@
Example context (json):
{
- "id": "question:12",
+ "id": "question:12_run-output",
"text": "hello world"
}
}}
-
{{# prefix_ans }}
-
+
{{/ prefix_ans }}
{{! TODO: use blocks to clean this mess up.}}
diff --git a/templates/scratchpad_ui.mustache b/templates/scratchpad_ui.mustache
index e9b3fef68..a85cd433c 100644
--- a/templates/scratchpad_ui.mustache
+++ b/templates/scratchpad_ui.mustache
@@ -21,7 +21,7 @@
{
"id": "question:12",
"answer_code": {
- "id": "question:12_answer-code"
+ "id": "question:12_answer-code",
"rows": 16,
"name": "answer_code",
"lang": "C"
@@ -33,7 +33,7 @@
"lang": "C"
},
"prefix_ans": {
- "id": "question:12_prefix-ans"
+ "id": "question:12_prefix-ans",
"checked": true
},
"output_display": {
diff --git a/tests/behat/scratchpad_ui.feature b/tests/behat/scratchpad_ui.feature
index 82429f6f7..e0ee42135 100644
--- a/tests/behat/scratchpad_ui.feature
+++ b/tests/behat/scratchpad_ui.feature
@@ -33,15 +33,15 @@ Feature: Test the Scratchpad UI
Then I should see "Print answer"
When I choose "Preview" action for "Print answer" in the question bank
- Then I should not see "Run!"
+ Then I should not see "Run"
And I should see "Scratchpad"
When I click on "Scratchpad" "button"
- Then I should see "Run!"
+ Then I should see "Run"
And I should see "Prefix with Answer"
When I click on "Scratchpad" "button"
- Then I should not see "Run!"
+ Then I should not see "Run"
And I should not see "Prefix Answer?"
Scenario: Click the run button with program that outputs nothing.
@@ -56,7 +56,7 @@ Feature: Test the Scratchpad UI
When I choose "Preview" action for "Print answer" in the question bank
And I click on "Scratchpad" "button"
- Then I press "Run!"
+ Then I press "Run"
And I should see "< No output! >"
Scenario: Click the run button with program that outputs in Scratchpad Code
@@ -73,7 +73,7 @@ Feature: Test the Scratchpad UI
And I click on "Scratchpad" "button"
And I set the ace field "test_code" to "print(\"hello\" + \" \" + \"world\")"
- Then I press "Run!"
+ Then I press "Run"
And I should see "hello world"
Scenario: Click the run button with program that outputs in Answer Code
@@ -90,11 +90,11 @@ Feature: Test the Scratchpad UI
And I click on "Scratchpad" "button"
And I set the ace field "answer_code" to "print(\"hello\" + \" \" + \"world\")"
- Then I press "Run!"
+ Then I press "Run"
And I should see "hello world"
Then I set the field "prefix_ans" to ""
- And I press "Run!"
+ And I press "Run"
And I should not see "hello world"
Scenario: Click the run button with program that outputs in Answer Code and Scratchpad Code
@@ -112,12 +112,12 @@ Feature: Test the Scratchpad UI
And I set the ace field "answer_code" to "print(\"hello\" + \" \" + \"world\")"
And I set the ace field "test_code" to "print(\"goodbye\" + \" \" + \"world\")"
- When I press "Run!"
+ When I press "Run"
Then I should see "hello world"
And I should see "goodbye world"
When I set the field "prefix_ans" to ""
- And I press "Run!"
+ And I press "Run"
Then I should not see "hello world"
And I should see "goodbye world"
@@ -199,6 +199,7 @@ Feature: Test the Scratchpad UI
When I choose "Preview" action for "Print answer" in the question bank
And I click on "Scratchpad" "button"
+ And I should see "Run"
And I set the ace field "test_code" to "print('hello world')"
And I set the field "prefix_ans" to ""
@@ -209,13 +210,19 @@ Feature: Test the Scratchpad UI
"""
When I press the CTRL + ALT M key
- And I click on "Scratchpad" "button"
- And I press the CTRL + ALT M key
+ Then I wait "2" seconds
+ Then I click on "Scratchpad" "button"
+
+ Then I wait "2" seconds
+ Then I should not see "Run"
+
+ When I press the CTRL + ALT M key
Then I should see in answer field:
"""
{"answer_code":[""],"test_code":["print('hello world')"],"show_hide":[""],"prefix_ans":[""]}
"""
+ @serial
Scenario: Get UI serialization, while Scratchpad Shown and prefix box NOT ticked
When I am on the "Print answer" "core_question > edit" page logged in as teacher1
And I set the field "id_answer" to ""
@@ -289,3 +296,73 @@ Feature: Test the Scratchpad UI
"""
{"answer_code":["print('hello world')"],"test_code":[""],"show_hide":[""],"prefix_ans":["1"]}
"""
+
+ @serial
+ Scenario: Enter serialization as string.
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+ | id_expected_0 | '' |
+ And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ Then I press the CTRL + ALT M key
+ And I set answer field to:
+ """
+ print('hello world')
+ """
+ And I press the CTRL + ALT M key
+ Then I should see "print('hello world')"
+
+ When I press the CTRL + ALT M key
+ Then I should see in answer field:
+ """
+ {"answer_code":["print('hello world')"],"test_code":[""],"show_hide":[""],"prefix_ans":["1"]}
+ """
+
+ @serial
+ Scenario: Enter serialisation missing answer_code.
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_validateonsave | 0 |
+ | id_expected_0 | '' |
+ And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ Then I press the CTRL + ALT M key
+ And I set answer field to:
+ """
+ {"not_answer_code":["print('hello world')"]}
+ """
+ And I press the CTRL + ALT M key
+ Then I should not see "print('hello world')"
+ And I should see "Falling back to raw text area."
+
+ Scenario: Change question using Scratchpad UI to table UI in authorform, tests destroy method.
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Table |
+ | id_validateonsave | 0 |
+ | id_expected_0 | '' |
+ And I press "id_updatebutton"
+
+ When I set the field "id_uiplugin" to "Scratchpad"
+ And I press the CTRL + ALT M key
+ Then I set the field "id_answer" to "print('hello world')"
+ Then I press the CTRL + ALT M key
+ And I set the field "id_uiplugin" to "Table"
+ Then I should not see "print('hello world')"
+# This line should work, but behat can't find the text... Not essential, but does improve test's accuracy.
+# And I should see "Table UI needs parameters num_columns and num_rows."
diff --git a/tests/behat/scratchpad_ui_params.feature b/tests/behat/scratchpad_ui_params.feature
index c1feb0351..52d1b82e1 100644
--- a/tests/behat/scratchpad_ui_params.feature
+++ b/tests/behat/scratchpad_ui_params.feature
@@ -2,7 +2,7 @@
Feature: Test the Scratchpad UI, UI Params
In order to use the Scratchpad UI
As a teacher
- I should be able specify the UI Paramiters to change the Scratchpad UI
+ I should be able specify the UI Parameters to change the Scratchpad UI
Background:
Given the following "users" exist:
@@ -40,14 +40,16 @@ Feature: Test the Scratchpad UI, UI Params
"prefix_name":"unhelpful label :)",
"wrapper_src": "globalextra",
"run_lang": "Python3",
+ "help_text": "hi",
"params": {
"numprocs":100,
"memlimit":1000
- }
+ },
+ "disable_scratchpad": false,
+ "invert_prefix": true
}
"""
And I press "id_updatebutton"
-
Then I should not see "The UI parameters for this question or its prototype are broken. Proceed with caution."
Scenario: Change UI param for run button name
@@ -62,17 +64,17 @@ Feature: Test the Scratchpad UI, UI Params
When I choose "Preview" action for "Print answer" in the question bank
And I should not see "superuniquename123"
- And I should not see "Run!"
+ And I should not see "Run"
And I should see "Scratchpad"
When I click on "Scratchpad" "button"
Then I should see "superuniquename123"
- But I should not see "Run!"
+ But I should not see "Run"
And I should see "Prefix with Answer"
When I click on "Scratchpad" "button"
Then I should not see "superuniquename123"
- And I should not see "Run!"
+ And I should not see "Run"
And I should not see "Prefix Answer?"
Scenario: Change UI param for Scratchpad name
@@ -85,17 +87,17 @@ Feature: Test the Scratchpad UI, UI Params
Then I should see "Print answer"
When I choose "Preview" action for "Print answer" in the question bank
- Then I should not see "Run!"
+ Then I should not see "Run"
And I should not see "Scratchpad"
But I should see "superuniquename123"
When I click on "superuniquename123" "button"
Then I should see "superuniquename123"
- And I should see "Run!"
+ And I should see "Run"
And I should see "Prefix with Answer"
When I click on "superuniquename123" "button"
- And I should not see "Run!"
+ And I should not see "Run"
And I should not see "Prefix Answer?"
Scenario: Change UI param for run button name
@@ -111,16 +113,16 @@ Feature: Test the Scratchpad UI, UI Params
When I choose "Preview" action for "Print answer" in the question bank
Then I should see "Scratchpad"
And I should not see "superuniquename123"
- And I should not see "Run!"
+ And I should not see "Run"
When I click on "Scratchpad" "button"
Then I should see "superuniquename123"
But I should not see "Prefix Answer?"
- And I should see "Run!"
+ And I should see "Run"
When I click on "Scratchpad" "button"
Then I should not see "superuniquename123"
- And I should not see "Run!"
+ And I should not see "Run"
And I should not see "Prefix Answer?"
Scenario: Set HTML output to true, 'print' a button to output area
@@ -136,7 +138,7 @@ Feature: Test the Scratchpad UI, UI Params
When I choose "Preview" action for "Print answer" in the question bank
And I click on "Scratchpad" "button"
And I set the ace field "test_code" to "print('')"
- Then I press "Run!"
+ Then I press "Run"
And I press "Hi"
Scenario: Define wrapper in UI params and click run, insert both answer and Scratchpad code, NO prefix with answer, click run
@@ -160,10 +162,10 @@ Feature: Test the Scratchpad UI, UI Params
And I set the ace field "answer_code" to "print('Hello Answercode', end=' ')"
And I set the ace field "test_code" to "print('Hello Scratchpadcode', end=' ')"
And I set the field "prefix_ans" to ""
- Then I press "Run!"
+ Then I press "Run"
And I should see "Hello Wrapper Hello Scratchpadcode"
- Scenario: Define wrapper in UI params and click run, insert both answer and Scratchpad code, prefix with answer, click run
+ Scenario: Define wrapper in global extra and click run, insert both answer and Scratchpad code, prefix with answer, click run
And I set the field "id_answer" to ""
And I set the following fields to these values:
| id_customise | 1 |
@@ -183,9 +185,44 @@ Feature: Test the Scratchpad UI, UI Params
And I click on "Scratchpad" "button"
And I set the ace field "answer_code" to "print('Hello Answercode', end=' ')"
And I set the ace field "test_code" to "print('Hello Scratchpadcode', end=' ')"
- Then I press "Run!"
+ Then I press "Run"
And I should see "Hello Wrapper Hello Answercode Hello Scratchpadcode"
+ Scenario: Define wrapper in prototype extra and click run, insert both answer and Scratchpad code, prefix with answer, click run
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"wrapper_src": "prototypeextra"} |
+ | id_prototypetype | 2 |
+ | id_typename | typename123 |
+ | id_validateonsave | 0 |
+
+ And I set the field "globalextra" to:
+ """
+ print('Hello GlobalExtra', end=' ')
+ {{ ANSWER_CODE }}
+ {{ SCRATCHPAD_CODE }}
+ """
+ And I set the field "prototypeextra" to:
+ """
+ print('Hello PrototypeExtra', end=' ')
+ {{ ANSWER_CODE }}
+ {{ SCRATCHPAD_CODE }}
+ """
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I wait "2" seconds
+ Then I click on "Scratchpad" "button"
+ And I set the ace field "answer_code" to "print('Hello Answercode', end=' ')"
+ And I set the ace field "test_code" to "print('Hello Scratchpadcode', end=' ')"
+ Then I press "Run"
+ Then I wait "2" seconds
+ Then I should see "Hello PrototypeExtra Hello Answercode Hello Scratchpadcode"
+ And I should not see "Hello GlobalExtra"
+
Scenario: Define wrapper in global extra, insert both answer and Scratchpad code, NO prefix with answer, click run
And I set the field "id_answer" to ""
And I set the following fields to these values:
@@ -206,7 +243,7 @@ Feature: Test the Scratchpad UI, UI Params
And I set the ace field "answer_code" to "print('Hello Answercode', end=' ')"
And I set the ace field "test_code" to "print('Hello Scratchpadcode', end=' ')"
And I set the field "prefix_ans" to ""
- Then I press "Run!"
+ Then I press "Run"
And I should see "Hello Wrapper Hello Scratchpadcode"
Scenario: Define wrapper in global extra, insert both answer and Scratchpad code, prefix with answer, click run
@@ -230,5 +267,121 @@ Feature: Test the Scratchpad UI, UI Params
And I set the ace field "answer_code" to "print('Hello Answercode', end=' ')"
And I set the ace field "test_code" to "print('Hello Scratchpadcode', end=' ')"
And I set the field "prefix_ans" to "1"
- Then I press "Run!"
+ Then I press "Run"
And I should see "Hello Wrapper Hello Answercode Hello Scratchpadcode"
+
+ Scenario: I set UI param invert_prefix to true and check serialisation
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"invert_prefix": "true"} |
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I should see "Run"
+ And I set the field "prefix_ans" to "1"
+
+ Then I press the CTRL + ALT M key
+ And I should see in answer field:
+ """
+ {"answer_code":[""],"test_code":[""],"show_hide":["1"],"prefix_ans":[""]}
+ """
+
+ When I press the CTRL + ALT M key
+ Then I press the CTRL + ALT M key
+ And I should see in answer field:
+ """
+ {"answer_code":[""],"test_code":[""],"show_hide":["1"],"prefix_ans":[""]}
+ """
+
+ Scenario: I set UI param invert_prefix to true and check serialisation
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"invert_prefix": "true"} |
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the field "prefix_ans" to ""
+
+ Then I press the CTRL + ALT M key
+ And I should see in answer field:
+ """
+ {"answer_code":[""],"test_code":[""],"show_hide":["1"],"prefix_ans":["1"]}
+ """
+
+ Scenario: I set UI param invert_prefix to true and check prefix run functionality
+ When I am on the "Print answer" "core_question > edit" page logged in as teacher1
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"invert_prefix": "true"} |
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "answer_code" to "print(\"hello\" + \" \" + \"world\")"
+ And I set the ace field "test_code" to "print(\"goodbye\" + \" \" + \"world\")"
+
+ When I press "Run"
+ Then I should not see "hello world"
+ And I should see "goodbye world"
+
+ When I set the field "prefix_ans" to "1"
+ And I press "Run"
+ Then I should see "hello world"
+ And I should see "goodbye world"
+
+ Scenario: Set UI param for disabling scratchpad and check serialisation.
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"disable_scratchpad": "true"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ Then I should not see "Scratchpad"
+ And I should not see "Run"
+ And I should not see "Prefix with Answer"
+
+ When I press the CTRL + ALT M key
+ Then I should see in answer field ""
+
+ When I press the CTRL + ALT M key
+ Then I wait "2" seconds
+ And I set the ace field "answer_code" to "print('hello world')"
+
+ Then I press the CTRL + ALT M key
+ And I should see in answer field:
+ """
+ {"answer_code":["print('hello world')"],"test_code":[""],"show_hide":[""],"prefix_ans":["1"]}
+ """
+
+ Scenario: Set UI param for help text.
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"help_text": "superusefulhelptext"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the field "prefix_ans" to ""
+ And I press the tab key
+ Then I should see "superusefulhelptext"
From d92c2f31d57e2b2a52dfe21db4d428b3404b4016 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Sat, 28 Jan 2023 12:47:57 +1300
Subject: [PATCH 046/188] Bug fix: the UI parameters from the prototype should
be ignored if the UI has changed from that of the prototype.
---
question.php | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/question.php b/question.php
index f9041dd08..c9f98e84b 100644
--- a/question.php
+++ b/question.php
@@ -288,7 +288,7 @@ private function twig_render_with_seed($text, $seed) {
// Get the default ui parameters for the ui plugin and merge in
- // both the prototypes and this questions parameters.
+ // both the prototype's and this question's parameters.
// In order to support the legacy method of including ui parameters
// within the template parameters, we need to filter out only the
// valid ui parameters, so need to load the uiplugin json file to find
@@ -301,7 +301,8 @@ private function twig_render_with_seed($text, $seed) {
private function evaluate_merged_ui_parameters() {
$uiplugin = $this->uiplugin === null ? 'ace' : strtolower($this->uiplugin);
$uiparams = new qtype_coderunner_ui_parameters($uiplugin);
- if (isset($this->prototype->uiparameters)) { // Ensure prototype not missing.
+ // Merge prototype's UI parameters unless prototype is missing or UI plugin has changed.
+ if (isset($this->prototype->uiparameters) && strtolower($this->prototype->uiplugin) === $uiplugin) {
$uiparams->merge_json($this->prototype->uiparameters);
}
$uiparams->merge_json($this->templateparamsjson, true); // Legacy support.
From 618baf0e6ad13adb885d7932b1384e583f83286a Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Sat, 28 Jan 2023 14:07:03 +1300
Subject: [PATCH 047/188] To work around a Linux/Chrome bug that always matches
the media check if a light theme is required, only auto-switch from light to
dark, not vice-versa.
---
amd/src/ui_ace.js | 48 +++++++++++++++++++++++++-----------
lang/en/qtype_coderunner.php | 5 ++++
2 files changed, 39 insertions(+), 14 deletions(-)
diff --git a/amd/src/ui_ace.js b/amd/src/ui_ace.js
index 23e316467..40d029cdb 100644
--- a/amd/src/ui_ace.js
+++ b/amd/src/ui_ace.js
@@ -53,6 +53,7 @@ define(['jquery'], function($) {
focused = textarea[0] === document.activeElement,
lang = params.lang,
session,
+ code,
t = this; // For embedded callbacks.
try {
@@ -79,30 +80,39 @@ define(['jquery'], function($) {
this.editor.setOptions({
enableBasicAutocompletion: true,
+ enableLiveAutocompletion: params.live_autocompletion,
+ fontSize: params.font_size ? params.font_size : "14px",
newLineMode: "unix",
});
+
this.editor.$blockScrolling = Infinity;
session = this.editor.getSession();
- session.setValue(this.textarea.val());
-
- this.fixSlowLoad();
-
- // Set theme if available (not currently enabled).
- if (params.theme) {
- this.editor.setTheme("ace/theme/" + params.theme);
+ code = this.textarea.val();
+ if (params.import_from_scratchpad === undefined || params.import_from_scratchpad) {
+ code = this.extract_from_json_maybe(code);
}
-
- // Set theme to dark if user-prefers-color-scheme is dark,
- // else use the uiParams theme if provided else use light.
- if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
- this.editor.setTheme(ACE_DARK_THEME);
- } else if (params.theme) {
+ session.setValue(code);
+ if (params.theme) {
this.editor.setTheme("ace/theme/" + params.theme);
} else {
this.editor.setTheme(ACE_LIGHT_THEME);
}
+ // If auto dark switching allowed, and user-prefers-color-scheme
+ // is defined, set to the dark theme accordingly.
+ // Logically this should work in both directions, switching from a
+ // dark theme to a light theme if requested. But Chrome under Linux
+ // always matches 'light' so this breaks. As a workaround, only
+ // one way switching (light to dark) is supported.
+ if (params.auto_switch_dark === undefined || params.auto_switch_dark) {
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ this.editor.setTheme(ACE_DARK_THEME);
+ }
+ }
+
+ this.fixSlowLoad();
+
this.setLanguage(lang);
this.setEventHandlers(textarea);
@@ -138,6 +148,16 @@ define(['jquery'], function($) {
}
}
+ AceWrapper.prototype.extract_from_json_maybe = function(code) {
+ // If the given code looks like JSON from the Scratchpad UI,
+ // extract and return the answer_code attribute.
+ try {
+ const jsonObj = JSON.parse(code);
+ code = jsonObj.answer_code[0];
+ } catch(err) {}
+
+ return code;
+ };
AceWrapper.prototype.failed = function() {
return this.fail;
@@ -180,7 +200,7 @@ define(['jquery'], function($) {
};
// Sometimes Ace editors do not load until the mouse is moved. To fix this,
- // 'move' the mouse using JQuerry when the editor div enters the viewport.
+ // 'move' the mouse using JQuery when the editor div enters the viewport.
AceWrapper.prototype.fixSlowLoad = function () {
const observer = new IntersectionObserver( () => {
$(document).trigger('mousemove');
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index 3c73a0f00..6176dfff7 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -25,6 +25,11 @@
$string['aborted'] = 'Testing was aborted due to error.';
$string['ace_gapfillerui_ui_source_descr'] = '"globalextra" to take the code to display from the globalextra field or "test0" to take it from the testcode field of the first test';
$string['ace_ui_notready'] = 'Ace editor not ready. Perhaps reload page?';
+$string['aceui_auto_switch_dark_descr'] = 'Allow a user, browser or OS preference for dark themes to override a preset Ace light theme.';
+$string['aceui_live_autocompletion_descr'] = 'Enable the Ace editor\'s live autocompletion mode.';
+$string['aceui_font_size_descr'] = 'Ace editor font size.';
+$string['aceui_import_from_scratchpad_descr'] = 'True to allow the Ace editor to receive the JSON-format answer used by the scratchpad UI and extract the answer code from it. Facilitates switching UIs. Leave true unless you want Ace to edit JSON objects with an "answer_code" key.';
+$string['aceui_theme_descr'] = 'Ace editor theme. Default is the light theme "textmate". An alternative dark theme is "tomorrow_night". Light theme be overridden by user preferences - see auto_switch_dark.';
$string['addingcoderunner'] = 'Adding a new CodeRunner Question';
$string['ajax_error'] = '*** AJAX ERROR. DON\'T SAVE THIS! ***';
$string['allok'] = 'Passed all tests! ';
From 7e590e2e553c2b96d6cb42da3daf3cad0dc02959 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Sat, 28 Jan 2023 14:07:58 +1300
Subject: [PATCH 048/188] Add various UI parameters to the Ace UI.
---
amd/src/ui_ace.json | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
create mode 100644 amd/src/ui_ace.json
diff --git a/amd/src/ui_ace.json b/amd/src/ui_ace.json
new file mode 100644
index 000000000..23ec6d67f
--- /dev/null
+++ b/amd/src/ui_ace.json
@@ -0,0 +1,25 @@
+{
+ "name": "ui_ace",
+ "parameters": {
+ "auto_switch_dark": {
+ "type": "boolean",
+ "default": true
+ },
+ "font_size": {
+ "type": "string",
+ "default": "14px"
+ },
+ "import_from_scratchpad": {
+ "type": "boolean",
+ "default": true
+ },
+ "live_autocompletion": {
+ "type": "boolean",
+ "default": false
+ },
+ "theme": {
+ "type": "string",
+ "default": "textmate"
+ }
+ }
+}
From 9a74eaa98d63421cc5f8fcb2589a015219f5842a Mon Sep 17 00:00:00 2001
From: James Napier <61300251+jimbonothing64@users.noreply.github.com>
Date: Thu, 2 Feb 2023 10:16:28 +1300
Subject: [PATCH 049/188] Scratchpad Development (#164)
* init
* init
* final init...
* UI to serialization working
* serialization to UI working (both ways)
* code grunted
* before altering serialization
* fixed bug where garbled serialization caused crash
* change serialisation to use object
* html bug in reload method fixed
* object serialisation for both text areas
* added checkbox (very big!)
* fixed checkbox and button sizes
* fixed prefix-ans checkbox not remembering state
* added show/hide scratchpad serialization
* show/hide tickbox now shows/hides sp
* make grunt happy
* sp is now hidden when serialization tells it to be
* grunt
* run button runs hello world program
* run button runs serialized field
* run button now runs code as it should
* spacing fixed
* Added error messaging and trunction for run
* fixed language choice for run
* refactored UI html code
* fixed bug were new scratchpad not serializing
* fixed scratchpad show/hide
* fixed scratchpad show/hide (again)
* refactor function out of object
* add ui param for 'run' button
* added scratchpad name ui param
* sp html display now works
* added new params
* changed sb_ prefix to sp_ for ui params
* added ui param for changing prefix with ans text
* remove stray sb_ prefixes
* added run specific language param
* refactored combine code to be function not method
* added ui_params for wrapper
* trial version of wrapper/template
* added global template/wrapper option functionality
* wrapper functionality working
* Cleaned up comments
* fixed sandbox params
* bug fix: sp display area now clears in text and html mode
* bug fix: globalextra
* bug fix: \n bug in answer code leading to unterminated strings etc.
* change names in wrapper
extract \n bugfix to wrapper
* added ace editor to top
* added ace editor to scratchpad (serial to reload broken)
* Fixed serialization with ace
* Ace added using two UI wrappers, each containing an ACE UI
* Refactored reload method
* Refactored sub-UI creation
* Added syntax highlighting for ace
* bug fix: undefined ID for sub-textareas
* bug fix: fixed size and resizing of answer-box
* bug fix: sizing, weird flashing while loading
* bug fix: remove list from serialization
* grunted
* Serialization when empty now empty string
* remove get fields method
* removed blob names, replaced with scratchpad
* renamed blob ui files to scratchpad ui files
* changed dual blob filenames to scratchpad filenames
* implement hasFocus method
* add configure sandbox method
* fixed a tag to be html compliant
* added scratchpad ui acceptance tests
* added scratchpad ui acceptance tests for using run and serialization
* added scratchpad ui acceptance tests for using run and serialization
* acceptance tests for using run
* bug fix: serialization empty unless scratchpad shown
* bug fix: sandbox not init for test properly
* Added feedback when run program provides no output
* fixed validate on save breaking tests
* added serialisation tests
* fixed serialisation tests
* I see in answer box added (no pystring)
* I see in answer box added pystring
* Changed name of test to match others better
* Added wrapper tests
* Fixed serialisation tests
* Cleaned up scenario descriptions
* added I set ace field function
* fixed tests to work with Ace editors
* bug fix: ace now allowed for Scratchpad
* bug fix: test with wrong deffinition
* cleaned up comments
* bug fix: clicking label checks checkbox now
* invert prefix ans default state and serialization
* prefix ans functionality now matches inverted serialisation
* match tests with new defualt prefix on state of SP
* cleaned up serialization inversion
* uninvert serialisation
* uninvert serialisation
* uninvert serialisation
* updated default prefix text
* added help button
* simplified ui param names
* added scratchpad ui strings
* added language string support
* added language strings for sp
* changed test's UI param names to match new names
* cleaned up spacing, semicolons
* removed sp_ prefixes
* added langauge string support for buttons, help text ect
* spacing
* fixed wrong direction of scratchpad arrow
* clean up help popover
* switch show/hide to use bootstrap collapse
* added help_text UI param for setting help text
* modified wrapper_src to alow prototypeextra field use, disallowed wrapper entry
* fixed slow loading ace bug
* added spacing between precheck checkbox and label
* bug fix: scratchpad button oppening ALL scratchpads
* changed serialization to use lists
* fixed tab highlight being too big for space on scratchpad bar
* bug fix: scratchpad name param and lang string replacement
* bug fix: propper listy serialization for answer_code and test_code
* update tests to reflect changes to design
* update tests to test serialization missing fields
* ui can now handle serialization missing fields
* bug fix: ace not reading lang correctly
* added disable scratchpad ui param
* Eslint
* gurkin lint
* make use of mustache templates, jquerry removal
* re-add functionality to overwrite langauge strings with ui params
* switch to class notation
* es6ify
* es6
* commit before pull
* fix show/hide serialization
* update json parsing rules
* fixed UI params, show/hide arrow pointing wrong way
* fix test not waiting long enough
* added invert_prefix ui param, lang strings for ui params
* eslint
* added new tests for UI params, serialisation and demo readme
* bug fix: destroy ui method, add new tests, lint
* touched up readme, refactored code
* Update readme
* Add display area class, HTML, JSON and Text output support
* Add display area class HTML, JSON stdin support
* Update documentation
* add langauge string error msg for run button
* update readme
* commit before merge
* Add tests for display modes, fix json image display
* update docs, fix empty json displaymode bug
* update docs, fix empty json displaymode bug
---
ReadmeScratchpadUi.md | 272 +++++++++++-----
amd/build/ajaxquestionloader.min.js.map | 1 +
amd/build/authorform.min.js.map | 1 +
amd/build/graphelements.min.js.map | 1 +
amd/build/graphutil.min.js.map | 1 +
amd/build/multilanguagequestion.min.js.map | 1 +
amd/build/outputdisplayarea.min.js | 26 +-
amd/build/outputdisplayarea.min.js.map | 1 +
amd/build/resetbutton.min.js.map | 1 +
amd/build/showdiff.min.js.map | 1 +
amd/build/textareas.min.js.map | 1 +
amd/build/ui_ace.min.js.map | 1 +
amd/build/ui_ace_gapfiller.min.js.map | 1 +
amd/build/ui_gapfiller.min.js.map | 1 +
amd/build/ui_graph.min.js.map | 1 +
amd/build/ui_html.min.js.map | 1 +
amd/build/ui_scratchpad.min.js | 17 +-
amd/build/ui_scratchpad.min.js.map | 1 +
amd/build/ui_table.min.js.map | 1 +
amd/build/userinterfacewrapper.min.js.map | 1 +
amd/src/outputdisplayarea.js | 351 ++++++++++++++++-----
amd/src/ui_scratchpad.js | 48 +--
amd/src/ui_scratchpad.json | 8 +-
lang/en/qtype_coderunner.php | 11 +
templates/help_icon.mustache | 2 +-
templates/output_displayarea.mustache | 7 +-
tests/behat/behat_coderunner.php | 46 ++-
tests/behat/scratchpad_ui_params.feature | 201 +++++++++++-
tests/fixtures/input_mpl_wrapper_html.py | 129 ++++++++
tests/fixtures/input_mpl_wrapper_json.py | 118 +++++++
30 files changed, 1022 insertions(+), 231 deletions(-)
create mode 100644 amd/build/ajaxquestionloader.min.js.map
create mode 100644 amd/build/authorform.min.js.map
create mode 100644 amd/build/graphelements.min.js.map
create mode 100644 amd/build/graphutil.min.js.map
create mode 100644 amd/build/multilanguagequestion.min.js.map
create mode 100644 amd/build/outputdisplayarea.min.js.map
create mode 100644 amd/build/resetbutton.min.js.map
create mode 100644 amd/build/showdiff.min.js.map
create mode 100644 amd/build/textareas.min.js.map
create mode 100644 amd/build/ui_ace.min.js.map
create mode 100644 amd/build/ui_ace_gapfiller.min.js.map
create mode 100644 amd/build/ui_gapfiller.min.js.map
create mode 100644 amd/build/ui_graph.min.js.map
create mode 100644 amd/build/ui_html.min.js.map
create mode 100644 amd/build/ui_scratchpad.min.js.map
create mode 100644 amd/build/ui_table.min.js.map
create mode 100644 amd/build/userinterfacewrapper.min.js.map
create mode 100644 tests/fixtures/input_mpl_wrapper_html.py
create mode 100644 tests/fixtures/input_mpl_wrapper_json.py
diff --git a/ReadmeScratchpadUi.md b/ReadmeScratchpadUi.md
index 3815cef33..3897e14d3 100644
--- a/ReadmeScratchpadUi.md
+++ b/ReadmeScratchpadUi.md
@@ -40,14 +40,18 @@ A valid serialisation is one with all four specified fields. All other serialisa
- `wrapper_src`: location of wrapper code to be used by the run button, if applicable:
- setting to `globalextra` will use text in global extra field,
- `prototypeextra` will use the prototype extra field.
-- `html_output`: when true, the output from run will be displayed as raw HTML instead of text.
+- `output_display_mode`: control how program output is displayed on runs, there are three modes:
+ - `text`: display program output as text, html escaped;
+ - `json`: display program output, when it is json, see next section...
+ - `html`: display program output as raw html.
- `disable_scratchpad`: disable the scratchpad, effectively revert back to Ace UI from student perspective.
- `invert_prefix`: inverts meaning of prefix_ans serialisation -- `'1'` means un-ticked, vice versa. This can be used to swap the default state.
- `params` : **THESE ARE NOT WELL DOCUMENTED**
### Advanced Customization: Wrappers
-A wrapper can be used to wrap code before it is run using the sandbox.
+A wrapper is be used to wrap code before it is run using the sandbox.
+A wrapper could be used to enclose the code in a function call, or to run the program as a subprocess after manipulation.
Some tasks that require this include: running languages installed on Jobe but not supported by coderunner; reading standard input during runs; or displaying Matplotlib graphs.
@@ -61,77 +65,201 @@ The default configuration uses the following wrapper:
{{ SCRATCHPAD_CODE }}
```
-Wrappers can be in a different language to their question; you can set the Run language, using `run_lang`.
-This changes the language the sandbox service uses to run the wrapper.
-This would be invisible to the student answering the question.
-Below is an example of a C program being wrapped using Python (see the multi-language question for further inspiration):
- ```
- import subprocess
-
-student_answer = """{{ ANSWER_CODE }}"""
-test_code = """{{ SCRATCHPAD_CODE }}"""
-all_code = student_answer + '\n' + test_code
- filename = '__tester__.c
- with open(filename, "w") as src:
- print(all_code, file=src)
-
-cflags = "-std=c99 -Wall -Werror"
-return_code = subprocess.call("gcc {0} -o __tester__ __tester__.c".format(cflags).split())
-if return_code != 0:
- raise Exception("** Compilation failed. Testing aborted **")
-exec_command = ["./__tester__"]
-
- output = subprocess.check_output(exec_command, universal_newlines=True)
-print(output)
- ```
-To expand: it is possible to wrap the scratchpad code inside a main function or use a modified wrapper to run unsupported code.
-
-The `html_output` parameter, in conjunction with a wrapper, can be used to display graphical/non-textual output in the output display area. Using HTML output, it is possible to insert images and input boxes.
-
-Example of a wrapper to display `Matplotlib` graphs in the output display area:
-```
-import subprocess, base64, html, os, tempfile
+Three UI parameters are of particular importance when writing wrappers:
+- `wrapper_src` sets the location of the wrapper code.
+- `run_lang` sets the language the Sandbox Webservice uses to run code when the **Run Button** is pressed.
+- `output_display_mode` controls how run output is displayed, see below.
-def make_data_uri(filename):
- with open(filename, "br") as fin:
- contents = fin.read()
- contents_b64 = base64.b64encode(contents).decode("utf8")
- return "data:image/png;base64,{}".format(contents_b64)
+There are three modes of displaying program run output, set by `output_display_mode`:
+ - `text`: Display the output as text, html escaped. **(default)**
+ - `json`: Display programs that output JSON, useful for capturing stdin and displaying images. **(recommended)**
+ - Accepts JSON in run output with the fields:
+ - `returncode`: Exit code from program run.
+ - `stdout`: Stdout text from program run.
+ - `stderr`: Error text from program run.
+ - `files`: An object containing filenames mapped to base64 encoded images. These will be displayed below any stdout text.
+ - When the `returncode` is set to `42`, an HTML input field will be added after the last `stdout` received.
+ When the enter key is pressed inside the input, the input's value is added to stdin and the program is run again with this updated stdin.
+ This is repeated until `returncode` is not set to `42`.
+ - `html`: Display program output as raw html inside the output area. **(advanced)**
+ - This can be used to show images and insert other HTML.
+ - Giving an `` element the class `coderunner-run-input` will add an event: when the enter key is pressed inside the input, the input's value is added to stdin and the program is run again with this updated stdin.
+Note: JSON is the preferred display mode; wrapper debugging is much simpler than HTML mode.
+HTML output is only recommended if you are trying to display HTML elements and for very advanced users, enter at your own risk...
-code = r"""{{ ANSWER_CODE }}
-{{ SCRATCHPAD_CODE }}
-"""
-
-prefix = """import os, tempfile
-os.environ["MPLCONFIGDIR"] = tempfile.mkdtemp()
-import matplotlib as _mpl
-_mpl.use("Agg")
-"""
-
-suffix = """
-figs = _mpl.pyplot.get_fignums()
-for i, fig in enumerate(figs):
- _mpl.pyplot.figure(fig)
- filename = f'image{i}.png'
- _mpl.pyplot.savefig(filename, bbox_inches='tight')
-"""
-
-prog_to_exec = prefix + code + suffix
-
-with open('prog.py', 'w') as outfile:
- outfile.write(prog_to_exec)
-
-result = subprocess.run(['python3', 'prog.py'], capture_output=True, text=True)
-print('
'))
+
+[//]: # ()
+[//]: # (for fname in os.listdir():)
+
+[//]: # ( if fname.endswith('png'):)
+
+[//]: # ( print(f''))
+
+[//]: # (```)
+
+[//]: # (#### Wrappers III: Reading stdin during runs)
+
+[//]: # (A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally)
+
+[//]: # (developed for use in the Scratchpad UI. It has three modes of operation:)
+
+[//]: # (- 'text': Just display the output as text, html escaped.)
+
+[//]: # (- 'json': The recommended way to display programs that use stdin or output images (or both).)
+
+[//]: # ( - Accepts JSON in the CRWS response output with fields:)
+
+[//]: # ( - `"returncode"`: Error/return code from running program.)
+
+[//]: # ( - `"stdout"`: Stdout text from running program.)
+
+[//]: # ( - `"stderr"`: Error text from running program.)
+
+[//]: # ( - `"files"`: Images encoded in base64 text encoding. These will be displayed above any stdout text.)
+
+[//]: # ( - When input from stdin is required the returncode 42 should be returned, raise this)
+
+[//]: # ( any time the program asks for input. An (html) input will be added after the last stdout received.)
+
+[//]: # ( When enter is pressed, runCode is called with value of the input added to the stdin string.)
+
+[//]: # ( This repeats until returncode is no longer 42.)
+
+[//]: # (- 'html': Display program output as raw html inside the output area.)
+
+[//]: # ( - This can be used to show images and insert other HTML tags (and beyond).)
+
+[//]: # ( - Giving an `` tag the class 'coderunner-run-input' will add an event that)
+
+[//]: # ( on pressing enter will call the runCode method again with the value of that input field added to stdin.)
+
+[//]: # ( This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.)
\ No newline at end of file
diff --git a/amd/build/ajaxquestionloader.min.js.map b/amd/build/ajaxquestionloader.min.js.map
new file mode 100644
index 000000000..a39d8300d
--- /dev/null
+++ b/amd/build/ajaxquestionloader.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ajaxquestionloader.min.js","sources":["../src/ajaxquestionloader.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 * JavaScript for filling in the question text with the contents of one\n * of the question's support files. Intended primarily for program contest\n * problems, where the support file is a single domjudge or ICPC problem zip,\n * with the problem spec within it.\n *\n * @module qtype_coderunner/ajaxquestionloader\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function ($) {\n /**\n * Append to the question text div in the question a data-URL containing\n * the contents of the question specification file (usu. a pdf).\n * @param {int} qid The question ID in the database.\n * @param {string} divId The ID of the question text
element.\n * @param {string} questionFilename The name of the problem spec file within\n * the problem zip file.\n */\n function loadQuestionText(qid, divId, questionFilename) {\n var questionTextDiv = $('#' + divId),\n errorDiv = '
Failed to load problem spec
';\n if (questionTextDiv.length != 1) {\n questionTextDiv.append(errorDiv);\n return;\n }\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/problemspec.php',\n {\n questionid: qid,\n sesskey: M.cfg.sesskey,\n filename: questionFilename\n },\n function (response) {\n if (response.filecontentsb64) {\n\n questionTextDiv.append(\n '
');\n } else {\n questionTextDiv.append(errorDiv);\n }\n\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred.\n questionTextDiv.append(errorDiv);\n });\n }\n\n return {\n loadQuestionText: loadQuestionText\n };\n});\n"],"names":["define","$","loadQuestionText","qid","divId","questionFilename","questionTextDiv","errorDiv","length","getJSON","M","cfg","wwwroot","questionid","sesskey","filename","response","filecontentsb64","append","fail"],"mappings":";;;;;;;;;;AA2BAA,6CAAO,CAAC,WAAW,SAAUC,SAuClB,CACHC,0BA/BsBC,IAAKC,MAAOC,sBAC9BC,gBAAkBL,EAAE,IAAMG,OAC1BG,SAAW,2DACe,GAA1BD,gBAAgBE,OAIpBP,EAAEQ,QAAQC,EAAEC,IAAIC,QAAU,4CAClB,CACIC,WAAYV,IACZW,QAASJ,EAAEC,IAAIG,QACfC,SAAUV,mBAEd,SAAUW,UACFA,SAASC,gBAETX,gBAAgBY,OACd,sDACAF,SAASC,gBAAkB,4BAE7BX,gBAAgBY,OAAOX,aAIrCY,MAAK,WAEHb,gBAAgBY,OAAOX,aAtBvBD,gBAAgBY,OAAOX"}
\ No newline at end of file
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
new file mode 100644
index 000000000..a44df6300
--- /dev/null
+++ b/amd/build/authorform.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n */\n function setUis() {\n var uiname = uiplugin.val();\n var enableUi = true;\n if (uiname === 'html' && uiparameters.val().trim() !== '') {\n try {\n var uiparams = JSON.parse(uiparameters.val());\n if (uiparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","enableUi","trim","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/graphelements.min.js.map b/amd/build/graphelements.min.js.map
new file mode 100644
index 000000000..8487227c5
--- /dev/null
+++ b/amd/build/graphelements.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"graphelements.min.js","sources":["../src/graphelements.js"],"sourcesContent":["/******************************************************************************\n *\n * A module for use by ui_graph, defining classes Node, Link, SelfLink,\n * StartLink and TemporaryLink\n *\n * @module qtype_coderunner/graphelements\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// This code is a modified version of Finite State Machine Designer\n// (http://madebyevan.com/fsm/)\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(['qtype_coderunner/graphutil'], function(util) {\n\n /**\n * Define a class Node that represents a node in a graph\n * @param {object} parent The Graph to which this node belongs.\n * @param {int} x The x-coordinate of the node.\n * @param {int} y The y-coordinate of the node.\n *\n */\n function Node(parent, x, y) {\n this.parent = parent; // The ui_graph instance.\n this.x = x;\n this.y = y;\n this.mouseOffsetX = 0;\n this.mouseOffsetY = 0;\n this.isAcceptState = false;\n this.textBox = new TextBox('', this);\n this.caretPosition = 0;\n }\n\n // At the start of a drag, record our position relative to the mouse.\n Node.prototype.setMouseStart = function(mouseX, mouseY) {\n this.mouseOffsetX = this.x - mouseX;\n this.mouseOffsetY = this.y - mouseY;\n };\n\n Node.prototype.setAnchorPoint = function(x, y) {\n this.x = x + this.mouseOffsetX;\n this.y = y + this.mouseOffsetY;\n };\n\n // Given a new mouse position during a drag, move to the appropriate\n // new position.\n Node.prototype.trackMouse = function(mouseX, mouseY) {\n this.x = this.mouseOffsetX + mouseX;\n this.y = this.mouseOffsetY + mouseY;\n };\n\n Node.prototype.draw = function(c) {\n // Draw the circle.\n c.beginPath();\n c.arc(this.x, this.y, this.parent.nodeRadius(), 0, 2 * Math.PI, false);\n c.stroke();\n\n // Draw the text.\n this.textBox.draw(this.x, this.y, null, this);\n\n // Draw a double circle for an accept state.\n if(this.isAcceptState) {\n c.beginPath();\n c.arc(this.x, this.y, this.parent.nodeRadius() - 6, 0, 2 * Math.PI, false);\n c.stroke();\n }\n };\n\n Node.prototype.closestPointOnCircle = function(x, y) {\n var dx = x - this.x;\n var dy = y - this.y;\n var scale = Math.sqrt(dx * dx + dy * dy);\n return {\n 'x': this.x + dx * this.parent.nodeRadius() / scale,\n 'y': this.y + dy * this.parent.nodeRadius() / scale,\n };\n };\n\n Node.prototype.containsPoint = function(x, y) {\n return (x - this.x) * (x - this.x) + (y - this.y) * (y - this.y) < this.parent.nodeRadius() * this.parent.nodeRadius();\n };\n\n // Method of a Node that, given a list of all links in a graph, returns\n // a list of any nodes that contain a link to this node (excluding StartLinks\n // and SelfLinks).\n Node.prototype.neighbours = function(links) {\n var neighbours = [], link;\n for (var i = 0; i < links.length; i++) {\n link = links[i];\n if (link instanceof Link) { // Exclude SelfLinks and StartLinks.\n if (link.nodeA === this && !neighbours.includes(link.nodeB)) {\n neighbours.push(link.nodeB);\n } else if (link.nodeB === this && !neighbours.includes(link.nodeA)) {\n neighbours.push(link.nodeA);\n }\n }\n }\n return neighbours;\n };\n\n // Method of Node that traverses a graph defined by a given set of links\n // starting at 'this' node and updating the visited list for each new\n // node. Returns the updated visited list, which (for the root call)\n // is a list of all nodes connected to the given start node.\n Node.prototype.traverseGraph = function(links, visited) {\n var neighbours,\n neighbour;\n if (!visited.includes(this)) {\n visited.push(this);\n neighbours = this.neighbours(links);\n for (var i = 0; i < neighbours.length; i++) {\n neighbour = neighbours[i];\n if (!visited.includes(neighbour)) {\n neighbour.traverseGraph(links, visited);\n }\n }\n }\n return visited;\n };\n\n /**\n * Define a class Link that represents a connection between two nodes.\n * @param {object} parent The graph to which this link belongs.\n * @param {object} a The node at one end of the link.\n * @param {object} b The node at the other end of the link.\n *\n */\n function Link(parent, a, b) {\n this.parent = parent; // The parent ui_digraph instance.\n this.nodeA = a;\n this.nodeB = b;\n this.textBox = new TextBox('', this);\n this.lineAngleAdjust = 0; // Value to add to textAngle when link is straight line.\n this.caretPosition = 0;\n\n // Make anchor point relative to the locations of nodeA and nodeB.\n this.parallelPart = 0.5; // Percentage from nodeA to nodeB.\n this.perpendicularPart = 0; // Pixels from line between nodeA and nodeB.\n }\n\n Link.prototype.getAnchorPoint = function() {\n var dx = this.nodeB.x - this.nodeA.x;\n var dy = this.nodeB.y - this.nodeA.y;\n var scale = Math.sqrt(dx * dx + dy * dy);\n return {\n 'x': this.nodeA.x + dx * this.parallelPart - dy * this.perpendicularPart / scale,\n 'y': this.nodeA.y + dy * this.parallelPart + dx * this.perpendicularPart / scale\n };\n };\n\n Link.prototype.setAnchorPoint = function(x, y) {\n var dx = this.nodeB.x - this.nodeA.x;\n var dy = this.nodeB.y - this.nodeA.y;\n var scale = Math.sqrt(dx * dx + dy * dy);\n this.parallelPart = (dx * (x - this.nodeA.x) + dy * (y - this.nodeA.y)) / (scale * scale);\n this.perpendicularPart = (dx * (y - this.nodeA.y) - dy * (x - this.nodeA.x)) / scale;\n // Snap to a straight line.\n if(this.parallelPart > 0 && this.parallelPart < 1 && Math.abs(this.perpendicularPart) < this.parent.SNAP_TO_PADDING) {\n this.lineAngleAdjust = (this.perpendicularPart < 0) * Math.PI;\n this.perpendicularPart = 0;\n }\n };\n\n Link.prototype.getEndPointsAndCircle = function() {\n if(this.perpendicularPart === 0) {\n var midX = (this.nodeA.x + this.nodeB.x) / 2;\n var midY = (this.nodeA.y + this.nodeB.y) / 2;\n var start = this.nodeA.closestPointOnCircle(midX, midY);\n var end = this.nodeB.closestPointOnCircle(midX, midY);\n return {\n 'hasCircle': false,\n 'startX': start.x,\n 'startY': start.y,\n 'endX': end.x,\n 'endY': end.y,\n };\n }\n var anchor = this.getAnchorPoint();\n var circle = util.circleFromThreePoints(this.nodeA.x, this.nodeA.y, this.nodeB.x, this.nodeB.y, anchor.x, anchor.y);\n var isReversed = (this.perpendicularPart > 0);\n var reverseScale = isReversed ? 1 : -1;\n var rRatio = reverseScale * this.parent.nodeRadius() / circle.radius;\n var startAngle = Math.atan2(this.nodeA.y - circle.y, this.nodeA.x - circle.x) - rRatio;\n var endAngle = Math.atan2(this.nodeB.y - circle.y, this.nodeB.x - circle.x) + rRatio;\n var startX = circle.x + circle.radius * Math.cos(startAngle);\n var startY = circle.y + circle.radius * Math.sin(startAngle);\n var endX = circle.x + circle.radius * Math.cos(endAngle);\n var endY = circle.y + circle.radius * Math.sin(endAngle);\n return {\n 'hasCircle': true,\n 'startX': startX,\n 'startY': startY,\n 'endX': endX,\n 'endY': endY,\n 'startAngle': startAngle,\n 'endAngle': endAngle,\n 'circleX': circle.x,\n 'circleY': circle.y,\n 'circleRadius': circle.radius,\n 'reverseScale': reverseScale,\n 'isReversed': isReversed,\n };\n };\n\n Link.prototype.draw = function(c) {\n var linkInfo = this.getEndPointsAndCircle(), textX, textY, textAngle, relDist;\n // Draw arc.\n c.beginPath();\n if(linkInfo.hasCircle) {\n c.arc(linkInfo.circleX,\n linkInfo.circleY,\n linkInfo.circleRadius,\n linkInfo.startAngle,\n linkInfo.endAngle,\n linkInfo.isReversed);\n } else {\n c.moveTo(linkInfo.startX, linkInfo.startY);\n c.lineTo(linkInfo.endX, linkInfo.endY);\n }\n c.stroke();\n // Draw the head of the arrow.\n if(linkInfo.hasCircle) {\n this.parent.arrowIfReqd(c,\n linkInfo.endX,\n linkInfo.endY,\n linkInfo.endAngle - linkInfo.reverseScale * (Math.PI / 2));\n } else {\n this.parent.arrowIfReqd(c,\n linkInfo.endX,\n linkInfo.endY,\n Math.atan2(linkInfo.endY - linkInfo.startY, linkInfo.endX - linkInfo.startX));\n }\n // Draw the text.\n relDist = this.textBox.relDist;\n if(linkInfo.hasCircle) {\n var startAngle = linkInfo.startAngle;\n var endAngle = linkInfo.endAngle;\n if (endAngle < startAngle) {\n endAngle += Math.PI * 2;\n }\n\n textAngle = ((1 - relDist) * startAngle + relDist * endAngle);\n if (linkInfo.isReversed){\n textAngle += (1 - relDist) * (2 * Math.PI); // Reflect text across the line between the link points\n }\n textX = linkInfo.circleX + linkInfo.circleRadius * Math.cos(textAngle);\n textY = linkInfo.circleY + linkInfo.circleRadius * Math.sin(textAngle);\n this.textBox.draw(textX, textY, textAngle, this);\n } else {\n textX = ((1 - relDist) * linkInfo.startX + relDist * linkInfo.endX);\n textY = ((1 - relDist) * linkInfo.startY + relDist * linkInfo.endY);\n textAngle = Math.atan2(linkInfo.endX - linkInfo.startX, linkInfo.startY - linkInfo.endY);\n this.textBox.draw(textX, textY, textAngle + this.lineAngleAdjust, this);\n }\n };\n\n Link.prototype.containsPoint = function(x, y) {\n var linkInfo = this.getEndPointsAndCircle(), dx, dy, distance;\n if(linkInfo.hasCircle) {\n dx = x - linkInfo.circleX;\n dy = y - linkInfo.circleY;\n distance = Math.sqrt(dx * dx + dy * dy) - linkInfo.circleRadius;\n if(Math.abs(distance) < this.parent.HIT_TARGET_PADDING) {\n var angle = Math.atan2(dy, dx);\n var startAngle = linkInfo.startAngle;\n var endAngle = linkInfo.endAngle;\n if(linkInfo.isReversed) {\n var temp = startAngle;\n startAngle = endAngle;\n endAngle = temp;\n }\n if(endAngle < startAngle) {\n endAngle += Math.PI * 2;\n }\n if(angle < startAngle) {\n angle += Math.PI * 2;\n } else if(angle > endAngle) {\n angle -= Math.PI * 2;\n }\n return (angle > startAngle && angle < endAngle);\n }\n } else {\n dx = linkInfo.endX - linkInfo.startX;\n dy = linkInfo.endY - linkInfo.startY;\n var length = Math.sqrt(dx * dx + dy * dy);\n var percent = (dx * (x - linkInfo.startX) + dy * (y - linkInfo.startY)) / (length * length);\n distance = (dx * (y - linkInfo.startY) - dy * (x - linkInfo.startX)) / length;\n return (percent > 0 && percent < 1 && Math.abs(distance) < this.parent.HIT_TARGET_PADDING);\n }\n return false;\n };\n\n /**\n * Define a class SelfLink that represents a connection from a node back\n * to itself.\n * @param {object} parent The graph to which this link belongs.\n * @param {object} node The node the link emerges from and returns to.\n * @param {object} mouse The current position of the mouse that's defining\n * the position of the self-link.\n */\n function SelfLink(parent, node, mouse) {\n this.parent = parent;\n this.node = node;\n this.anchorAngle = 0;\n this.mouseOffsetAngle = 0;\n this.textBox = new TextBox('', this);\n\n if(mouse) {\n this.setAnchorPoint(mouse.x, mouse.y);\n }\n }\n\n SelfLink.prototype.setMouseStart = function(x, y) {\n this.mouseStartX = x;\n this.mouseStartY = y;\n };\n\n SelfLink.prototype.setAnchorPoint = function(x, y) {\n this.anchorAngle = Math.atan2(y - this.node.y, x - this.node.x) + this.mouseOffsetAngle;\n // Snap to 90 degrees.\n var snap = Math.round(this.anchorAngle / (Math.PI / 2)) * (Math.PI / 2);\n if(Math.abs(this.anchorAngle - snap) < 0.1) {\n this.anchorAngle = snap;\n }\n // Keep in the range -pi to pi so our containsPoint() function always works.\n if(this.anchorAngle < -Math.PI) {\n this.anchorAngle += 2 * Math.PI;\n }\n if(this.anchorAngle > Math.PI) {\n this.anchorAngle -= 2 * Math.PI;\n }\n };\n\n SelfLink.prototype.getEndPointsAndCircle = function() {\n var circleX = this.node.x + 1.5 * this.parent.nodeRadius() * Math.cos(this.anchorAngle);\n var circleY = this.node.y + 1.5 * this.parent.nodeRadius() * Math.sin(this.anchorAngle);\n var circleRadius = 0.75 * this.parent.nodeRadius();\n var startAngle = this.anchorAngle - Math.PI * 0.8;\n var endAngle = this.anchorAngle + Math.PI * 0.8;\n var startX = circleX + circleRadius * Math.cos(startAngle);\n var startY = circleY + circleRadius * Math.sin(startAngle);\n var endX = circleX + circleRadius * Math.cos(endAngle);\n var endY = circleY + circleRadius * Math.sin(endAngle);\n return {\n 'hasCircle': true,\n 'startX': startX,\n 'startY': startY,\n 'endX': endX,\n 'endY': endY,\n 'startAngle': startAngle,\n 'endAngle': endAngle,\n 'circleX': circleX,\n 'circleY': circleY,\n 'circleRadius': circleRadius\n };\n };\n\n SelfLink.prototype.draw = function(c) {\n var linkInfo = this.getEndPointsAndCircle();\n // Draw arc.\n c.beginPath();\n c.arc(linkInfo.circleX, linkInfo.circleY, linkInfo.circleRadius, linkInfo.startAngle, linkInfo.endAngle, false);\n c.stroke();\n // Draw the text on the loop.\n var relDist = this.textBox.relDist;\n var textAngle = linkInfo.startAngle * (1 - relDist) + linkInfo.endAngle * relDist;\n var textX = linkInfo.circleX + linkInfo.circleRadius * Math.cos(textAngle);\n var textY = linkInfo.circleY + linkInfo.circleRadius * Math.sin(textAngle);\n this.textBox.draw(textX, textY, textAngle, this);\n // Draw the head of the arrow.\n this.parent.arrowIfReqd(c, linkInfo.endX, linkInfo.endY, linkInfo.endAngle + Math.PI * 0.4);\n };\n\n SelfLink.prototype.containsPoint = function(x, y) {\n var linkInfo = this.getEndPointsAndCircle();\n var dx = x - linkInfo.circleX;\n var dy = y - linkInfo.circleY;\n var distance = Math.sqrt(dx * dx + dy * dy) - linkInfo.circleRadius;\n return (Math.abs(distance) < this.parent.HIT_TARGET_PADDING);\n };\n\n /**\n * Define a class StartLink that represents a start link in a finite\n * state machine. Not useful in general digraphs.\n * @param {object} parent The graph to which this link belongs.\n * @param {node} node The node that the link leads into.\n * @param {object} start The point at the open end of the link.\n */\n function StartLink(parent, node, start) {\n this.parent = parent;\n this.node = node;\n this.deltaX = 0;\n this.deltaY = 0;\n\n if(start) {\n this.setAnchorPoint(start.x, start.y);\n }\n }\n\n StartLink.prototype.setAnchorPoint = function(x, y) {\n this.deltaX = x - this.node.x;\n this.deltaY = y - this.node.y;\n\n if(Math.abs(this.deltaX) < this.parent.SNAP_TO_PADDING) {\n this.deltaX = 0;\n }\n\n if(Math.abs(this.deltaY) < this.parent.SNAP_TO_PADDING) {\n this.deltaY = 0;\n }\n };\n\n StartLink.prototype.getEndPoints = function() {\n var startX = this.node.x + this.deltaX;\n var startY = this.node.y + this.deltaY;\n var end = this.node.closestPointOnCircle(startX, startY);\n return {\n 'startX': startX,\n 'startY': startY,\n 'endX': end.x,\n 'endY': end.y,\n };\n };\n\n StartLink.prototype.draw = function(c) {\n var endPoints = this.getEndPoints();\n\n // Draw the line.\n c.beginPath();\n c.moveTo(endPoints.startX, endPoints.startY);\n c.lineTo(endPoints.endX, endPoints.endY);\n c.stroke();\n\n // Draw the head of the arrow.\n this.parent.arrowIfReqd(c, endPoints.endX, endPoints.endY, Math.atan2(-this.deltaY, -this.deltaX));\n };\n\n StartLink.prototype.containsPoint = function(x, y) {\n var endPoints = this.getEndPoints();\n var dx = endPoints.endX - endPoints.startX;\n var dy = endPoints.endY - endPoints.startY;\n var length = Math.sqrt(dx * dx + dy * dy);\n var percent = (dx * (x - endPoints.startX) + dy * (y - endPoints.startY)) / (length * length);\n var distance = (dx * (y - endPoints.startY) - dy * (x - endPoints.startX)) / length;\n return (percent > 0 && percent < 1 && Math.abs(distance) < this.parent.HIT_TARGET_PADDING);\n };\n\n /**\n * Define a class TemporaryLink that represents a link that's in the\n * process of being created.\n * @param {object} parent The graph to which this link belongs.\n * @param {object} from The node the link starts at.\n * @param {object} to The node the link goes to.\n */\n function TemporaryLink(parent, from, to) {\n this.parent = parent;\n this.from = from;\n this.to = to;\n }\n\n TemporaryLink.prototype.draw = function(c) {\n // Draw the line.\n c.beginPath();\n c.moveTo(this.to.x, this.to.y);\n c.lineTo(this.from.x, this.from.y);\n c.stroke();\n\n // Draw the head of the arrow.\n this.parent.arrowIfReqd(c, this.to.x, this.to.y, Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x));\n };\n\n /**\n * Define a class Button for a pseudo-menu button.\n * @param {object} parent The graph to which this button belongs.\n * @param {int} topX The x coordinate of the top left corner of the menu text.\n * @param {int} topY The y coordinate of the top left corner of the menu text.\n * @param {string} text The button label text.\n */\n function Button(parent, topX, topY, text) {\n this.BUTTON_WIDTH = 60;\n this.BUTTON_HEIGHT = 25;\n this.TEXT_OFFSET_X = 30;\n this.TEXT_OFFSET_Y = 17;\n this.topX = topX;\n this.topY = topY;\n this.parent = parent;\n this.text = text;\n this.highLighted = false;\n }\n\n Button.prototype.containsPoint = function(x, y) {\n return util.isInside({x: x, y: y},\n {x: this.topX, y: this.topY, width: this.BUTTON_WIDTH, height: this.BUTTON_HEIGHT});\n };\n\n Button.prototype.draw = function(c) {\n if (this.highLighted) {\n c.fillStyle = '#FFFFFF';\n } else {\n c.fillStyle = '#F0F0F0';\n }\n c.fillRect(this.topX, this.topY,\n this.BUTTON_WIDTH, this.BUTTON_HEIGHT);\n c.lineWidth = 0.5;\n c.strokeStyle = '#000000';\n c.strokeRect(this.topX, this.topY,\n this.BUTTON_WIDTH, this.BUTTON_HEIGHT);\n\n c.font = '12pt Arial';\n c.fillStyle = '#000000';\n c.textAlign = \"center\";\n c.fillText(this.text, this.topX + this.TEXT_OFFSET_X, this.topY + this.TEXT_OFFSET_Y);\n c.textAlign = \"left\";\n };\n\n Button.prototype.onClick = function() {\n\n };\n\n /**\n * Define a class HelpBox for the help box and its pseudo-menu button.\n * @param {object} parent The graph to which this help box belongs.\n * @param {int} topX The x coordinate of the top left corner of the help box.\n * @param {int} topY The y coordinate of the top left corner of the help box.\n */\n function HelpBox(parent, topX, topY) {\n Button.call(this, parent, topX, topY, \"Help\");\n this.helpOpen = false;\n this.LINE_HEIGHT = 18;\n this.HELP_INDENT = 5;\n }\n\n HelpBox.prototype = new Button();\n\n HelpBox.prototype.draw = function(c) {\n var lines, i, y, helpText;\n\n Button.prototype.draw.call(this, c);\n\n if (this.helpOpen) {\n helpText = this.parent.helpText;\n c.font = '12pt Arial';\n lines = helpText.split('\\n');\n y = this.topY + this.BUTTON_HEIGHT;\n for (i = 0; i < lines.length; i += 1) {\n y += this.LINE_HEIGHT;\n c.fillText(lines[i], this.topX + this.HELP_INDENT, y);\n }\n }\n };\n\n HelpBox.prototype.onClick = function() {\n this.helpOpen = ! this.helpOpen;\n this.parent.draw();\n };\n\n\n /**\n * Define a class TextBox for a possibly editable text box that might\n * be contained in another element.\n * @param {string} text The text to put in the text box.\n * @param {object} parent The graph to which the text box belongs.\n **/\n function TextBox(text, parent) {\n this.text = text;\n this.parent = parent;\n this.caretPosition = text.length;\n this.relDist = 0.5;\n this.offset = parent.parent.textOffset();\n this.dragged = false;\n this.boundingBox = {};\n }\n\n // Inserts a given character into the TextBox at its current caretPosition\n TextBox.prototype.insertChar = function(char) {\n this.text = this.text.slice(0, this.caretPosition) + char + this.text.slice(this.caretPosition);\n this.caretRight();\n };\n\n // Deletes the character in the TextBox that is located behind the current caretPosition\n TextBox.prototype.deleteChar = function() {\n if (this.caretPosition > 0){\n this.text = this.text.slice(0, this.caretPosition - 1) + this.text.slice(this.caretPosition);\n this.caretLeft();\n }\n };\n\n // Moves the TextBox's caret left one character if possible\n TextBox.prototype.caretLeft = function() {\n if (this.caretPosition > 0) {\n this.caretPosition --;\n }\n };\n\n // Moves the TextBox's caret right one character if possible\n TextBox.prototype.caretRight = function() {\n if (this.caretPosition < this.text.length) {\n this.caretPosition ++;\n }\n };\n\n TextBox.prototype.containsPoint = function(x, y) {\n var point = {x: x, y: y};\n return util.isInside(point, this.boundingBox);\n };\n\n TextBox.prototype.setMouseStart = function(x, y) {\n // At the start of a drag, record our position relative to the mouse.\n this.mouseOffsetX = this.position.x - x;\n this.mouseOffsetY = this.position.y - y;\n };\n\n TextBox.prototype.setAnchorPoint = function(x, y) {\n x += (this.mouseOffsetX || 0);\n y += (this.mouseOffsetY || 0);\n var linkInfo = this.parent.getEndPointsAndCircle();\n var relDist, offset;\n //Calculate the relative distance of the dragged text along its parent link\n if (linkInfo.hasCircle){\n var textAngle = Math.atan2(y-linkInfo.circleY, x-linkInfo.circleX);\n // Ensure textAngle is either between start and end angle, or more than end angle\n if (textAngle < linkInfo.startAngle) {\n textAngle += Math.PI * 2;\n }\n if (linkInfo.endAngle < linkInfo.startAngle) {\n linkInfo.endAngle += Math.PI * 2;\n }\n // Calculate relDist from angle (inverse of angle-from-relDist calculation in Link.prototype.draw)\n if (linkInfo.isReversed){\n relDist = (textAngle - linkInfo.startAngle - Math.PI*2) / (linkInfo.endAngle - linkInfo.startAngle - Math.PI*2);\n }else{\n relDist = (textAngle - linkInfo.startAngle) / (linkInfo.endAngle - linkInfo.startAngle);\n }\n offset = util.vectorMagnitude({x: x-linkInfo.circleX, y: y-linkInfo.circleY}) - linkInfo.circleRadius;\n }\n else {\n // Calculate relative position of the mouse projected onto the link.\n var textVector = {x: x - linkInfo.startX,\n y: y - linkInfo.startY};\n var linkVector = {x: linkInfo.endX - linkInfo.startX,\n y: linkInfo.endY - linkInfo.startY};\n var projection = util.scalarProjection(textVector, linkVector);\n relDist = projection / util.vectorMagnitude(linkVector);\n // Calculate offset (closest distance) of the mouse position from the link\n offset = Math.sqrt(Math.pow(util.vectorMagnitude(textVector), 2)- Math.pow(projection, 2));\n // If the mouse is on the opposite side of the link from the default text position, negate the offset\n var ccw = util.isCCW(textVector, linkVector);\n var reversed = (this.parent.lineAngleAdjust != 0);\n if ((!ccw && reversed) || (ccw && !reversed)){\n offset *= -1;\n }\n }\n if (relDist > 0 && relDist < 1){ //Ensure text isn't dragged past end of the link\n this.relDist = relDist;\n this.offset = Math.round(offset);\n this.dragged = true;\n }\n };\n\n TextBox.prototype.draw = function(x, y, angleOrNull, parentObject) {\n var graph = parentObject.parent,\n c = graph.getCanvas().getContext('2d');\n\n c.font = graph.fontSize() + 'px Arial';\n //Text before and after caret are drawn separately to expand Latex shortcuts at the caret position\n var beforeCaretText = util.convertLatexShortcuts(this.text.slice(0, this.caretPosition));\n var afterCaretText = util.convertLatexShortcuts(this.text.slice(this.caretPosition));\n var width = c.measureText(beforeCaretText + afterCaretText).width;\n var dy = Math.round(graph.fontSize() / 2);\n\n // Position the text appropriately if it is part of a link\n if(angleOrNull !== null) {\n var cos = Math.cos(angleOrNull);\n var sin = Math.sin(angleOrNull);\n\n //Add text offset in the direction of the text angle\n x += this.offset * cos;\n y += this.offset * sin;\n\n // Position text intelligently if text has not been manually moved\n if (!this.dragged){\n var cornerPointX = (width / 2) * (cos > 0 ? 1 : -1);\n var cornerPointY = (dy / 2) * (sin > 0 ? 1 : -1);\n var slide = sin * Math.pow(Math.abs(sin), 40) * cornerPointX - cos * Math.pow(Math.abs(cos), 10) * cornerPointY;\n x += cornerPointX - sin * slide;\n y += cornerPointY + cos * slide;\n }\n this.position = {x: Math.round(x), y: Math.round(y)}; //Record the position where text is anchored to\n }\n\n x -= width / 2; // Center the text.\n\n //Round the coordinates so they fall on a pixel\n x = Math.round(x);\n y = Math.round(y);\n\n // Draw text and caret\n if('advancedFillText' in c) {\n c.advancedFillText(this.text, this.text, x + width / 2, y, angleOrNull);\n } else {\n // Draw translucent white rectangle behind text\n var prevStyle = c.fillStyle;\n c.fillStyle = \"rgba(255, 255, 255, 0.7)\";\n c.fillRect(x, y-dy, width, dy*2);\n c.fillStyle = prevStyle;\n\n // Draw text\n dy = Math.round(graph.fontSize() / 3); // Don't understand this.\n c.fillText(beforeCaretText, x, y + dy);\n var caretX = x + c.measureText(beforeCaretText).width;\n c.fillText(afterCaretText, caretX, y + dy);\n\n // Draw caret\n dy = Math.round(graph.fontSize() / 2);\n if(parentObject == graph.selectedObject && graph.caretVisible && graph.hasFocus() && document.hasFocus()) {\n c.beginPath();\n c.moveTo(caretX, y - dy);\n c.lineTo(caretX, y + dy);\n c.stroke();\n }\n }\n this.boundingBox = {x: x, y: y - dy, height: dy * 2, width: width};\n };\n\n\n return {\n Node: Node,\n Link: Link,\n SelfLink: SelfLink,\n TemporaryLink: TemporaryLink,\n StartLink: StartLink,\n Button: Button,\n HelpBox: HelpBox,\n TextBox: TextBox\n };\n});\n"],"names":["define","util","Node","parent","x","y","mouseOffsetX","mouseOffsetY","isAcceptState","textBox","TextBox","this","caretPosition","Link","a","b","nodeA","nodeB","lineAngleAdjust","parallelPart","perpendicularPart","SelfLink","node","mouse","anchorAngle","mouseOffsetAngle","setAnchorPoint","StartLink","start","deltaX","deltaY","TemporaryLink","from","to","Button","topX","topY","text","BUTTON_WIDTH","BUTTON_HEIGHT","TEXT_OFFSET_X","TEXT_OFFSET_Y","highLighted","HelpBox","call","helpOpen","LINE_HEIGHT","HELP_INDENT","length","relDist","offset","textOffset","dragged","boundingBox","prototype","setMouseStart","mouseX","mouseY","trackMouse","draw","c","beginPath","arc","nodeRadius","Math","PI","stroke","closestPointOnCircle","dx","dy","scale","sqrt","containsPoint","neighbours","links","link","i","includes","push","traverseGraph","visited","neighbour","getAnchorPoint","abs","SNAP_TO_PADDING","getEndPointsAndCircle","midX","midY","end","anchor","circle","circleFromThreePoints","isReversed","reverseScale","rRatio","radius","startAngle","atan2","endAngle","cos","sin","textX","textY","textAngle","linkInfo","hasCircle","circleX","circleY","circleRadius","moveTo","startX","startY","lineTo","endX","endY","arrowIfReqd","distance","percent","HIT_TARGET_PADDING","angle","temp","mouseStartX","mouseStartY","snap","round","getEndPoints","endPoints","isInside","width","height","fillStyle","fillRect","lineWidth","strokeStyle","strokeRect","font","textAlign","fillText","onClick","lines","helpText","split","insertChar","char","slice","caretRight","deleteChar","caretLeft","point","position","vectorMagnitude","textVector","linkVector","projection","scalarProjection","pow","ccw","isCCW","reversed","angleOrNull","parentObject","graph","getCanvas","getContext","fontSize","beforeCaretText","convertLatexShortcuts","afterCaretText","measureText","cornerPointX","cornerPointY","slide","advancedFillText","prevStyle","caretX","selectedObject","caretVisible","hasFocus","document"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/graphutil.min.js.map b/amd/build/graphutil.min.js.map
new file mode 100644
index 000000000..23ac65002
--- /dev/null
+++ b/amd/build/graphutil.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"graphutil.min.js","sources":["../src/graphutil.js"],"sourcesContent":["/***********************************************************************\n *\n * Utility functions/data/constants for the ui_graph module.\n *\n ***********************************************************************/\n// Most of this code is taken from Finite State Machine Designer:\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(function() {\n /**\n * Contstructor for the Util class.\n */\n function Util() {\n\n this.greekLetterNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',\n 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda',\n 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',\n 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega' ];\n }\n\n Util.prototype.convertLatexShortcuts = function(text) {\n // Html greek characters.\n for(var i = 0; i < this.greekLetterNames.length; i++) {\n var name = this.greekLetterNames[i];\n text = text.replace(new RegExp('\\\\\\\\' + name, 'g'), String.fromCharCode(913 + i + (i > 16)));\n text = text.replace(new RegExp('\\\\\\\\' + name.toLowerCase(), 'g'), String.fromCharCode(945 + i + (i > 16)));\n }\n\n // Subscripts.\n for(var i = 0; i < 10; i++) {\n text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));\n }\n text = text.replace(new RegExp('_a', 'g'), String.fromCharCode(8336));\n return text;\n };\n\n Util.prototype.drawArrow = function(c, x, y, angle) {\n // Draw an arrow head on the graphics context c at (x, y) with given angle.\n\n var dx = Math.cos(angle);\n var dy = Math.sin(angle);\n c.beginPath();\n c.moveTo(x, y);\n c.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);\n c.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);\n c.fill();\n };\n\n Util.prototype.det = function(a, b, c, d, e, f, g, h, i) {\n // Determinant of given matrix elements.\n return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g;\n };\n\n Util.prototype.vectorMagnitude = function(v){\n // Returns magnitude (length) of a vector v\n return Math.sqrt(v.x * v.x + v.y * v.y);\n };\n\n Util.prototype.scalarProjection = function(a, b) {\n // Returns scalar projection of vector a onto vector b\n return (a.x * b.x + a.y * b.y) / this.vectorMagnitude(b);\n };\n\n Util.prototype.isCCW = function(a, b) {\n // Returns true iff vector b is in a counter-clockwise orientation relative to a\n return (a.x * b.y) - (b.x * a.y) > 0;\n };\n\n Util.prototype.circleFromThreePoints = function(x1, y1, x2, y2, x3, y3) {\n // Return {x, y, radius} of circle through (x1, y1), (x2, y2), (x3, y3).\n var a = this.det(x1, y1, 1, x2, y2, 1, x3, y3, 1);\n var bx = -this.det(x1 * x1 + y1 * y1, y1, 1, x2 * x2 + y2 * y2, y2, 1, x3 * x3 + y3 * y3, y3, 1);\n var by = this.det(x1 * x1 + y1 * y1, x1, 1, x2 * x2 + y2 * y2, x2, 1, x3 * x3 + y3 * y3, x3, 1);\n var c = -this.det(x1 * x1 + y1 * y1, x1, y1, x2 * x2 + y2 * y2, x2, y2, x3 * x3 + y3 * y3, x3, y3);\n return {\n 'x': -bx / (2 * a),\n 'y': -by / (2 * a),\n 'radius': Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))\n };\n };\n\n Util.prototype.isInside = function(pos, rect) {\n // True iff given point pos is inside rectangle.\n return pos.x > rect.x && pos.x < rect.x + rect.width && pos.y < rect.y + rect.height && pos.y > rect.y;\n };\n\n Util.prototype.crossBrowserKey = function(e) {\n // Return which key was pressed, given the event, in a browser-independent way.\n e = e || window.event;\n return e.which || e.keyCode;\n };\n\n\n Util.prototype.crossBrowserRelativeMousePos = function(e) {\n // Earlier complex version was breaking in Moodle 4, so replaced\n // with this much simpler version that should work with all modern\n // browsers.\n const rect = e.target.getBoundingClientRect();\n const x = e.clientX - rect.left; //x position within the element.\n const y = e.clientY - rect.top; //y position within the element.\n return {'x': x, 'y': y};\n };\n\n return new Util();\n});\n"],"names":["define","Util","greekLetterNames","prototype","convertLatexShortcuts","text","i","this","length","name","replace","RegExp","String","fromCharCode","toLowerCase","drawArrow","c","x","y","angle","dx","Math","cos","dy","sin","beginPath","moveTo","lineTo","fill","det","a","b","d","e","f","g","h","vectorMagnitude","v","sqrt","scalarProjection","isCCW","circleFromThreePoints","x1","y1","x2","y2","x3","y3","bx","by","abs","isInside","pos","rect","width","height","crossBrowserKey","window","event","which","keyCode","crossBrowserRelativeMousePos","target","getBoundingClientRect","clientX","left","clientY","top"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/multilanguagequestion.min.js.map b/amd/build/multilanguagequestion.min.js.map
new file mode 100644
index 000000000..820d8aa0e
--- /dev/null
+++ b/amd/build/multilanguagequestion.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"multilanguagequestion.min.js","sources":["../src/multilanguagequestion.js"],"sourcesContent":["/******************************************************************************\n *\n * This module simply handles changes in the Language selection dropdown for\n * multilanguage questions as seen by students. It switches the Ace language\n * accordingly.\n * It should only be loaded in conjunction with the ui_ace module.\n *\n * @module qtype_coderunner/multilanguagequestion\n * @copyright Richard Lobb, 2018, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * TODO: is there a race problem here??\n */\n\ndefine(['jquery'], function($) {\n /**\n * Initialise the language selector dropdown when the document is ready.\n * @param {string} taId The ID of the student answer box text area.\n */\n function initLangSelector(taId) {\n $().ready(function() {\n initLangSelectorWhenReady(taId);\n });\n }\n\n /**\n * Initialise the language selector. Called by initLanSelector when the\n * document is ready so the name is a bit of a misnomer.\n * @param {string} taId The ID of the student answer box text area.\n */\n function initLangSelectorWhenReady(taId) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n selector = $(\".coderunner-lang-select\");\n\n /**\n * Set the language for the Ace editor.\n */\n function setAceLang() {\n // Set the language for the Ace plugin (or any other plugin if it\n // has a setLanguage method).\n var lang = selector.val(),\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on reqd ta.\n\n if (uiWrapper && uiWrapper.uiInstance && typeof uiWrapper.uiInstance.setLanguage === 'function') {\n // TODO: define setLanguage as a required method of all UI plugins.\n uiWrapper.uiInstance.setLanguage(lang);\n }\n }\n\n selector.on('change', setAceLang);\n }\n\n return {'initLangSelector' : initLangSelector};\n});"],"names":["define","$","taId","ready","ta","document","getElementById","selector","setAceLang","lang","val","uiWrapper","data","uiInstance","setLanguage","on","initLangSelectorWhenReady"],"mappings":";;;;;;;;;;;;;AAcAA,gDAAO,CAAC,WAAW,SAASC,SAsCjB,2BAjCmBC,MACtBD,IAAIE,OAAM,qBAUqBD,UAC3BE,GAAKH,EAAEI,SAASC,eAAeJ,OAC/BK,SAAWN,EAAE,oCAKRO,iBAGDC,KAAOF,SAASG,MAChBC,UAAYP,GAAGQ,KAAK,sBAEpBD,WAAaA,UAAUE,YAA0D,mBAArCF,UAAUE,WAAWC,aAEjEH,UAAUE,WAAWC,YAAYL,MAIzCF,SAASQ,GAAG,SAAUP,YA5BlBQ,CAA0Bd"}
\ No newline at end of file
diff --git a/amd/build/outputdisplayarea.min.js b/amd/build/outputdisplayarea.min.js
index 102f26059..853953d61 100644
--- a/amd/build/outputdisplayarea.min.js
+++ b/amd/build/outputdisplayarea.min.js
@@ -1,8 +1,28 @@
-define("qtype_coderunner/outputdisplayarea",["exports","jquery","core/ajax","core/str"],(function(_exports,_jquery,_ajax,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+define("qtype_coderunner/outputdisplayarea",["exports","core/ajax","core/str"],(function(_exports,_ajax,_str){var obj;
/**
- * @module coderunner/outputdisplayarea
+ * A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally
+ * developed for use in the Scratchpad UI. It has three modes of operation:
+ * - 'text': Just display the output as text, html escaped.
+ * - 'json': The recommended way to display programs that use stdin or output images (or both).
+ * - Accepts JSON in the CRWS response output with fields:
+ * - "returncode": Error/return code from running program.
+ * - "stdout": Stdout text from running program.
+ * - "stderr": Error text from running program.
+ * - "files": An object containing filenames mapped to base64 encoded images.
+ * These will be displayed below any stdout text.
+ * - When input from stdin is required the returncode 42 should be returned, raise this
+ * any time the program asks for input. An (html) input will be added after the last stdout received.
+ * When enter is pressed, runCode is called with value of the input added to the stdin string.
+ * This repeats until returncode is no longer 42.
+ * - 'html': Display program output as raw html inside the output area.
+ * - This can be used to show images and insert other HTML tags (and beyond).
+ * - Giving an tag the class 'coderunner-run-input' will add an event that
+ * on pressing enter will call the runCode method again with the value of that input field added to stdin.
+ * This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.
+ *
+ * @module qtype_coderunner/outputdisplayarea
* @copyright James Napier, 2023, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.OutputDisplayArea=void 0,_jquery=_interopRequireDefault(_jquery),_ajax=_interopRequireDefault(_ajax);_exports.OutputDisplayArea=class{constructor(displayAreaId,outputMode){this.displayAreaId=displayAreaId,this.displayArea=document.getElementById(displayAreaId),this.textDisplay=document.getElementById(displayAreaId+"-text"),this.htmlDisplay=document.getElementById(displayAreaId+"-html"),this.imageDisplay=document.getElementById(displayAreaId+"-images"),this.mode=outputMode,this.stdIn=[]}clearDisplay(){this.textDisplay.innerHTML="",this.htmlDisplay.innerHTML="",this.imageDisplay.innerHTML=""}displayText(response){const output=response.output,error=response.stderr;this.textDisplay.innerText=output+error}displayHtml(response){const output=response.output,error=response.stderr;this.textDisplay.innerHTML=output+error}displayNoOutput(response){const isNoOutput=""===response.output&&""===response.stderr;return isNoOutput&&(this.textDisplay.innerHTML='< No output! >'),isNoOutput}display(response){if(!this.displayNoOutput(response))if("json"===this.mode){const json=JSON.parse(response);this.displayJson(json)}else if("html"===this.mode)this.displayHtml(response);else{if("text"!==this.mode)throw Error('Invalid outputMode given: "'.concat(this.mode,'"'));{const text=response;this.displayText(text)}}}async handleRunButtonClick(code,lang,sandboxParams){this.clearDisplay(),_ajax.default.call([{methodname:"qtype_coderunner_run_in_sandbox",args:{contextid:M.cfg.contextid,sourcecode:code,language:lang,params:JSON.stringify(sandboxParams)},done:responseJson=>{const response=JSON.parse(responseJson);this.display(response)},fail:error=>{alert(error.message)}}])}displayJson(json){var result=json,text=result.stdout;42!==result.returncode&&(text+=result.stderr),13==result.returncode&&(text+="\n*** Timeout error ***\n");var base64,numImages=0;if(result.files)for(var prop in(0,_jquery.default)(this.imageDisplay).empty(),result.files){var image=(base64=result.files[prop],(0,_jquery.default)('')));(0,_jquery.default)(this.imageDisplay).append(image),numImages+=1}if(""===text.trim()&&42!==result.returncode?0==numImages&&(0,_jquery.default)(this.textDisplay).html('< No output! >'):(0,_jquery.default)(this.textDisplay).text(text),42===result.returncode){var inputId="".concat(this.displayAreaId,"-input-field");(0,_jquery.default)(this.textDisplay).html((0,_jquery.default)(this.textDisplay).html()+''));var inputEl=(0,_jquery.default)(document.getElementById(inputId));inputEl.focus(),inputEl.on("keyup",(e=>{if(13===e.keyCode){const line=inputEl.val();inputEl.remove(),(0,_jquery.default)(this.textDisplay).html((0,_jquery.default)(this.textDisplay).html()+line),this.handleRunButtonClick()}}))}}}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.OutputDisplayArea=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};const JSON_DISPLAY_PROPS=["returncode","stdout","stderr","files"],combinedOutput=response=>response.cmpinfo+response.output+response.stderr,getImage=function(base64){let type=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"png";const image=document.createElement("img");return image.src="data:image/".concat(type,";base64,").concat(base64),image};_exports.OutputDisplayArea=class{constructor(displayAreaId,outputMode,lang,sandboxParams){this.displayAreaId=displayAreaId,this.lang=lang,this.mode=outputMode,this.sandboxParams=sandboxParams,this.textDisplay=document.getElementById(displayAreaId+"-text"),this.imageDisplay=document.getElementById(displayAreaId+"-images"),this.prevRunSettings=null}clearDisplay(){this.textDisplay.innerHTML="",this.imageDisplay.innerHTML=""}displayText(response){this.textDisplay.innerText=combinedOutput(response)}displayHtml(response){this.textDisplay.innerHTML=combinedOutput(response);const inputEl=this.textDisplay.querySelector(".coderunner-run-input");inputEl&&this.addInputEvents(inputEl)}displayJson(response){const result=this.validateJson(response.output);let text=result.stdout;42!==result.returncode&&(text+=result.stderr),13==result.returncode&&(text+="\n*** Timeout error ***\n");const numImages=this.displayImages(result.files);""===text.trim()&&42!==result.returncode?0==numImages&&this.displayNoOutput(null):this.textDisplay.innerText=text,42===result.returncode&&this.addInput()}validateJson(jsonString){let result=null;try{result=JSON.parse(jsonString)}catch(e){window.alert("Error parsing display JSON output: \n ".concat(jsonString,"\n JSON Parsing error Msg: \n ").concat(e.message,"\n The question author must fix this!"))}const missing=((obj,props)=>props.filter((prop=>!obj.hasOwnProperty(prop))))(result,JSON_DISPLAY_PROPS);return missing.length>0&&window.alert("Display JSON (in response.result) is missing the following fields:\n ".concat(missing.join(),"\n The question author must fix this!")),result}displayNoOutput(response){const isNoOutput=!response||0===combinedOutput(response).length;if(isNoOutput||null===response){const span=document.createElement("span");span.style.color="red",span.innerText="< No output! >",this.clearDisplay(),this.textDisplay.append(span)}return isNoOutput}display(response){const error=(response=>{const ERROR_RESPONSES=[[1,0,"error_access_denied"],[2,0,"error_unknown_language"],[3,0,"error_access_denied"],[4,0,"error_submission_limit_reached"],[5,0,"error_sandbox_server_overload"],[0,11,""],[0,12,""],[0,13,"error_timeout"],[0,15,""],[0,17,"error_memory_limit"],[0,21,"error_sandbox_server_overload"],[0,30,"error_excessive_output"]];for(let i=0;i{const message=await(0,_str.get_string)(langStringName,"qtype_coderunner");display.innerText="*** "+message+" ***\n",additionalText&&(display.innerText+=additionalText)})(error,this.textDisplay)}runCode(code,stdin){let shouldClearDisplay=arguments.length>2&&void 0!==arguments[2]&&arguments[2];this.prevRunSettings=[code,stdin],shouldClearDisplay&&this.clearDisplay(),_ajax.default.call([{methodname:"qtype_coderunner_run_in_sandbox",args:{contextid:M.cfg.contextid,sourcecode:code,language:this.lang,stdin:stdin,params:JSON.stringify(this.sandboxParams)},done:responseJson=>{const response=JSON.parse(responseJson);this.display(response)},fail:error=>{alert(error.message)}}])}addInput(){const inputId="".concat(this.displayAreaId,"-input-field");this.textDisplay.innerHTML+='');const inputEl=document.getElementById(inputId);this.addInputEvents(inputEl)}addInputEvents(inputEl){inputEl.focus(),inputEl.addEventListener("keydown",(e=>{13===e.keyCode&&e.preventDefault()})),inputEl.addEventListener("keyup",(e=>{if(13===e.keyCode){const line=inputEl.value;inputEl.remove(),this.textDisplay.innterHTML+=line,this.prevRunSettings[1]+=line+"\n",this.runCode(...this.prevRunSettings,!1)}}))}displayImages(files){let numImages=0;for(const[fname,fcontents]of Object.entries(files)){const fileType=fname.split(".")[1];if(fileType){const image=getImage(fcontents,fileType);this.imageDisplay.append(image),numImages+=1}else window.alert("Could not read filename correctly: ".concat(fname))}return numImages}}}));
//# sourceMappingURL=outputdisplayarea.min.js.map
\ No newline at end of file
diff --git a/amd/build/outputdisplayarea.min.js.map b/amd/build/outputdisplayarea.min.js.map
new file mode 100644
index 000000000..9c72735d3
--- /dev/null
+++ b/amd/build/outputdisplayarea.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"outputdisplayarea.min.js","sources":["../src/outputdisplayarea.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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/**\n * A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally\n * developed for use in the Scratchpad UI. It has three modes of operation:\n * - 'text': Just display the output as text, html escaped.\n * - 'json': The recommended way to display programs that use stdin or output images (or both).\n * - Accepts JSON in the CRWS response output with fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * - When input from stdin is required the returncode 42 should be returned, raise this\n * any time the program asks for input. An (html) input will be added after the last stdout received.\n * When enter is pressed, runCode is called with value of the input added to the stdin string.\n * This repeats until returncode is no longer 42.\n * - 'html': Display program output as raw html inside the output area.\n * - This can be used to show images and insert other HTML tags (and beyond).\n * - Giving an tag the class 'coderunner-run-input' will add an event that\n * on pressing enter will call the runCode method again with the value of that input field added to stdin.\n * This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.\n *\n * @module qtype_coderunner/outputdisplayarea\n * @copyright James Napier, 2023, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\nimport {get_string as getLangString} from 'core/str';\n\n\nconst ENTER_KEY = 13;\nconst INPUT_INTERRUPT = 42;\nconst RESULT_SUCCESS = 15;\nconst INPUT_CLASS = 'coderunner-run-input';\nconst JSON_DISPLAY_PROPS = ['returncode', 'stdout', 'stderr', 'files'];\n\n\n/**\n * Get the specified language string using\n * AJAX and plug it into the given textarea\n * @param {string} langStringName The language string name.\n * @param {DOMnode} display area into which the error message\n * should be plugged.\n * @param {string} additionalText Extra text to follow the result code.\n */\nconst setLangString = async(langStringName, display, additionalText) => {\n const message = await getLangString(langStringName, 'qtype_coderunner');\n display.innerText = \"*** \" + message + \" ***\\n\";\n if (additionalText) {\n display.innerText += additionalText;\n }\n};\n\nconst diagnoseWebserviceResponse = (response) => {\n // Table of error conditions.\n // Each row is response.error, response.result, langstring\n // response.result is ignored if response.error is non-zero.\n // Any condition not in the table is deemed an \"Unknown runtime error\".\n const ERROR_RESPONSES = [\n [1, 0, 'error_access_denied'], // Sandbox AUTH_ERROR\n [2, 0, 'error_unknown_language'], // Sandbox WRONG_LANG_ID\n [3, 0, 'error_access_denied'], // Sandbox ACCESS_DENIED\n [4, 0, 'error_submission_limit_reached'], // Sandbox SUBMISSION_LIMIT_EXCEEDED\n [5, 0, 'error_sandbox_server_overload'], // Sandbox SERVER_OVERLOAD\n [0, 11, ''], // RESULT_COMPILATION_ERROR\n [0, 12, ''], // RESULT_RUNTIME_ERROR\n [0, 13, 'error_timeout'], // RESULT TIME_LIMIT\n [0, RESULT_SUCCESS, ''], // RESULT_SUCCESS\n [0, 17, 'error_memory_limit'], // RESULT_MEMORY_LIMIT\n [0, 21, 'error_sandbox_server_overload'], // RESULT_SERVER_OVERLOAD\n [0, 30, 'error_excessive_output'] // RESULT OUTPUT_LIMIT\n ];\n for (let i = 0; i < ERROR_RESPONSES.length; i++) {\n let row = ERROR_RESPONSES[i];\n if (row[0] == response.error && (response.error != 0 || response.result == row[1])) {\n return row[2];\n }\n }\n return 'error_unknown_runtime'; // We're dead, Fred.\n};\n\n/**\n * Concatenates the cmpinfo, stdout and stderr fields of the sandbox\n * response, truncating both stdout and stderr to a given maximum length\n * if necessary (in which case '... (truncated)' is appended.\n * @param {object} response Sandbox response object\n */\nconst combinedOutput = (response) => {\n return response.cmpinfo + response.output + response.stderr;\n};\n\n/**\n * Check whether obj has the properties in props, returns missing properties.\n * @param {object} obj to check properties of\n * @param {array} props to check for.\n * @returns {array} of missing properties.\n */\nconst missingProperties = (obj, props) => {\n return props.filter(prop => !obj.hasOwnProperty(prop));\n};\n\n/**\n * Insert a base64 encoded string into HTML image.\n * @param {string} base64 encoded string.\n * @param {string} type of encoded image file.\n * @returns {*|jQuery|HTMLElement} image tag containing encoded image from string.\n */\nconst getImage = (base64, type = 'png') => {\n const image = document.createElement('img');\n image.src = `data:image/${type};base64,${base64}`;\n return image;\n};\n\n/**\n * Constructor for OutputDisplayArea object. For use with the output_displayarea template.\n * @param {string} displayAreaId The id of the display area div, this should match the 'id'\n * from the template.\n * @param {string} outputMode The mode being used for output, must be text, html or json.\n * @param {string} lang The language to run code with.\n * @param {string} sandboxParams The sandbox params to run code with.\n */\nclass OutputDisplayArea {\n constructor(displayAreaId, outputMode, lang, sandboxParams) {\n this.displayAreaId = displayAreaId;\n this.lang = lang;\n this.mode = outputMode;\n this.sandboxParams = sandboxParams;\n\n this.textDisplay = document.getElementById(displayAreaId + '-text');\n this.imageDisplay = document.getElementById(displayAreaId + '-images');\n\n this.prevRunSettings = null;\n }\n\n /**\n * Clear the display of any images and text.\n */\n clearDisplay() {\n this.textDisplay.innerHTML = \"\";\n this.imageDisplay.innerHTML = \"\";\n }\n\n /**\n * Display text from a CRWS response to the display (escaped).\n * @param {object} response Coderunner webservice response JSON.\n */\n displayText(response) {\n this.textDisplay.innerText = combinedOutput(response);\n }\n\n /**\n * Display HTML from a CRWS response to the display (un-escaped).\n * Find the first HTML input element with the input class and\n * add event listeners to handle reading stdin.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing HTML.\n */\n displayHtml(response) {\n this.textDisplay.innerHTML = combinedOutput(response);\n const inputEl = this.textDisplay.querySelector('.' + INPUT_CLASS);\n if (inputEl) {\n this.addInputEvents(inputEl);\n }\n }\n\n /**\n * Display JSON from a CRWS response to the display.\n * Assumes response.output will be a JSON with the fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * NOTE: See file header/readme for more info.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing JSON string.\n */\n displayJson(response) {\n const result = this.validateJson(response.output);\n let text = result.stdout;\n\n if (result.returncode !== INPUT_INTERRUPT) {\n text += result.stderr;\n }\n if (result.returncode == 13) { // Timeout\n text += \"\\n*** Timeout error ***\\n\";\n } // TODO: Langstring\n\n const numImages = this.displayImages(result.files);\n\n if (text.trim() === '' && result.returncode !== INPUT_INTERRUPT) {\n if (numImages == 0) {\n this.displayNoOutput(null);\n }\n } else {\n this.textDisplay.innerText = text;\n }\n\n if (result.returncode === INPUT_INTERRUPT) {\n this.addInput();\n }\n }\n\n /**\n * Validate JSON to display, make sure it is valid json and has required fields.\n * @param {string} jsonString string of JSON to be displayed.\n * @returns {object} JSON as object\n */\n validateJson(jsonString) {\n let result = null;\n try {\n result = JSON.parse(jsonString);\n } catch (e) {\n window.alert(\n `Error parsing display JSON output: \n ${jsonString}\n JSON Parsing error Msg: \n ${e.message}\n The question author must fix this!`\n );\n }\n\n const missing = missingProperties(result, JSON_DISPLAY_PROPS);\n if (missing.length > 0) {\n window.alert(\n `Display JSON (in response.result) is missing the following fields:\n ${missing.join()}\n The question author must fix this!`\n );\n }\n return result;\n }\n\n /**\n * Display no output message if no output to display or response is null.\n * @param {object} response Coderunner webservice response JSON, set to null to force\n * display of no output message.\n */\n displayNoOutput(response) {\n const isNoOutput = response ? combinedOutput(response).length === 0 : true;\n if (isNoOutput || response === null) {\n const span = document.createElement('span');\n span.style.color = 'red';\n span.innerText = '< No output! >';\n this.clearDisplay();\n this.textDisplay.append(span);\n }\n return isNoOutput;\n }\n /**\n * Display response using the current display mode.\n * @param {object} response Coderunner webservice response JSON.\n */\n display(response) {\n const error = diagnoseWebserviceResponse(response);\n if (error !== '') {\n setLangString(error, this.textDisplay);\n return;\n }\n if (this.displayNoOutput(response)) {\n return;\n }\n\n if (this.mode === 'json') {\n this.displayJson(response);\n } else if (this.mode === 'html') {\n this.displayHtml(response);\n } else if (this.mode === 'text') {\n this.displayText(response);\n } else {\n throw Error(`Invalid outputMode given: \"${this.mode}\"`);\n }\n }\n\n /**\n * Run code using the Coderunner webservice and then display the output\n * using the selected mode. This function uses AJAX to asynchronously run and\n * display code.\n * @param {string} code to be run.\n * @param {string} stdin to be fed into the program.\n * @param {boolean} shouldClearDisplay will reset the display before displaying.\n * Use false when doing stdin runs.\n */\n runCode(code, stdin, shouldClearDisplay = false) {\n this.prevRunSettings = [code, stdin];\n if (shouldClearDisplay) {\n this.clearDisplay();\n }\n ajax.call([{\n methodname: 'qtype_coderunner_run_in_sandbox',\n args: {\n contextid: M.cfg.contextid, // Moodle context ID\n sourcecode: code,\n language: this.lang,\n stdin: stdin,\n params: JSON.stringify(this.sandboxParams) // Sandbox params\n },\n done: (responseJson) => {\n const response = JSON.parse(responseJson);\n this.display(response);\n },\n fail: (error) => {\n alert(error.message);\n }\n }]);\n }\n\n /**\n * Add an input field with event listeners to support running again\n * with new stdin entered by user.\n */\n addInput() {\n const inputId = `${this.displayAreaId}-input-field`;\n this.textDisplay.innerHTML += ``;\n const inputEl = document.getElementById(inputId);\n this.addInputEvents(inputEl);\n }\n\n /**\n * Add event listeners to inputEl overriding enter key to:\n * - Prevent form-submit.\n * - Call runCode again, adding value in inputEl to stdin.\n * @param {node} inputEl to add event listeners to.\n */\n addInputEvents(inputEl) {\n inputEl.focus();\n\n inputEl.addEventListener('keydown', (e) => {\n if (e.keyCode === ENTER_KEY) {\n e.preventDefault(); // Do NOT form submit.\n }\n });\n inputEl.addEventListener('keyup', (e) => {\n if (e.keyCode === ENTER_KEY) {\n const line = inputEl.value;\n inputEl.remove();\n this.textDisplay.innterHTML += line; // Perhaps this should be sanitized.\n this.prevRunSettings[1] += line + '\\n';\n this.runCode(...this.prevRunSettings, false);\n }\n });\n }\n\n /**\n * Take the files from a JSON response and display them.\n * @param {object} files from response, in filename: filecontents pairs.\n * @returns {number} number of images displayed.\n */\n displayImages(files) {\n let numImages = 0;\n for (const [fname, fcontents] of Object.entries(files)) {\n const fileType = fname.split('.')[1];\n if (fileType) {\n const image = getImage(fcontents, fileType);\n this.imageDisplay.append(image);\n numImages += 1;\n } else {\n window.alert(`Could not read filename correctly: ${fname}`);\n }\n }\n return numImages;\n }\n}\n\n\nexport {\n OutputDisplayArea\n};\n"],"names":["JSON_DISPLAY_PROPS","combinedOutput","response","cmpinfo","output","stderr","getImage","base64","type","image","document","createElement","src","constructor","displayAreaId","outputMode","lang","sandboxParams","mode","textDisplay","getElementById","imageDisplay","prevRunSettings","clearDisplay","innerHTML","displayText","innerText","displayHtml","inputEl","this","querySelector","addInputEvents","displayJson","result","validateJson","text","stdout","returncode","numImages","displayImages","files","trim","displayNoOutput","addInput","jsonString","JSON","parse","e","window","alert","message","missing","obj","props","filter","prop","hasOwnProperty","missingProperties","length","join","isNoOutput","span","style","color","append","display","error","ERROR_RESPONSES","i","row","diagnoseWebserviceResponse","Error","async","langStringName","additionalText","setLangString","runCode","code","stdin","shouldClearDisplay","call","methodname","args","contextid","M","cfg","sourcecode","language","params","stringify","done","responseJson","fail","inputId","focus","addEventListener","keyCode","preventDefault","line","value","remove","innterHTML","fname","fcontents","Object","entries","fileType","split"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/resetbutton.min.js.map b/amd/build/resetbutton.min.js.map
new file mode 100644
index 000000000..6ca89e48d
--- /dev/null
+++ b/amd/build/resetbutton.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"resetbutton.min.js","sources":["../src/resetbutton.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 * This AMD module provides the functionality for the \"Reset\"\n * button that is shown just below the student answer field if the question is\n * defined to have preloaded text.\n * If clicked, the button reloads the student answer field with the original\n * preloaded text (after a Confirm dialogue, of course).\n *\n * @module qtype_coderunner/resetbutton\n * @copyright Richard Lobb, 2016, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Initialise the Reset button.\n * @param {string} buttonId The ID of the button.\n * @param {string} answerId The ID of the answer box whose contents are to\n * be reset.\n * @param {string} confirmText The language string to display when asking\n * the user to confirm the reset.\n */\n function initResetButton(buttonId, answerId, confirmText) {\n var resetButton = $('[id=\"' + buttonId + '\"]'),\n studentAnswer = $('[id=\"' + answerId + '\"]'),\n uiWrapper;\n\n resetButton.on(\"click\", function() {\n if (window.behattesting || window.confirm(confirmText)) {\n var reloadText = resetButton.attr('data-reload-text');\n uiWrapper = studentAnswer.data('current-ui-wrapper');\n if (uiWrapper && uiWrapper.uiInstance) {\n // If the textarea has a UI wrapper, and it's active.\n uiWrapper.stop();\n studentAnswer.val(reloadText);\n uiWrapper.restart();\n } else {\n studentAnswer.val(reloadText);\n }\n }\n });\n }\n\n return { \"initResetButton\": initResetButton };\n});\n"],"names":["define","$","buttonId","answerId","confirmText","uiWrapper","resetButton","studentAnswer","on","window","behattesting","confirm","reloadText","attr","data","uiInstance","stop","val","restart"],"mappings":";;;;;;;;;;;AA4BAA,sCAAO,CAAC,WAAW,SAASC,SA+BjB,0BArBkBC,SAAUC,SAAUC,iBAGrCC,UAFAC,YAAcL,EAAE,QAAUC,SAAW,MACrCK,cAAgBN,EAAE,QAAUE,SAAW,MAG3CG,YAAYE,GAAG,SAAS,cAChBC,OAAOC,cAAgBD,OAAOE,QAAQP,aAAc,KAChDQ,WAAaN,YAAYO,KAAK,qBAClCR,UAAYE,cAAcO,KAAK,wBACdT,UAAUU,YAEvBV,UAAUW,OACVT,cAAcU,IAAIL,YAClBP,UAAUa,WAEVX,cAAcU,IAAIL"}
\ No newline at end of file
diff --git a/amd/build/showdiff.min.js.map b/amd/build/showdiff.min.js.map
new file mode 100644
index 000000000..a80d3f760
--- /dev/null
+++ b/amd/build/showdiff.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"showdiff.min.js","sources":["../src/showdiff.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 * This AMD module provides the functionality for the \"Show differences\"\n * button that is shown in the student's result page if their answer\n * isn't right and an \"exact-match\" (or near equivalent) grader is being used.\n *\n * @module qtype_coderunner/showdiff\n * @copyright Richard Lobb, 2016, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n var NLCHAR = '\\u21A9'; // Unicode \"leftwards arrow with hook\" to show newlines.\n\n /**\n * Given two lists of items, items1 and item2, return the length matrix\n * M defined as M[i][j] = max subsequence length of the two item lists\n * items1[0:i], items2[0:j]\n * @param {array} items1 The first list of items.\n * @param {array} items2 The second list of items.\n * @return {array} The length matrix.\n */\n function lcsLengths(items1, items2) {\n\n var n1 = items1.length,\n n2 = items2.length,\n lengths, i, j,\n has_fill = typeof [1].fill === 'function';\n\n lengths = [];\n\n for (i = 0; i <= n1; i += 1) {\n lengths[i] = new Array(n2 + 1);\n if (has_fill) {\n lengths[i].fill(0);\n } else {\n // Bloody IE.\n for (j = 0; j < n2 + 1; j++) {\n lengths[i][j] = 0;\n }\n }\n }\n for (i = 0; i < n1; i += 1) {\n for (j = 0; j < n2; j += 1) {\n if (items1[i] == items2[j]) {\n lengths[i + 1][j + 1] = 1 + lengths[i][j];\n } else {\n lengths[i + 1][j + 1] = Math.max(lengths[i][j + 1], lengths[i + 1][j]);\n }\n }\n }\n return lengths;\n }\n\n /**\n * Given two lists of items, items1 and item2, return the longest common\n * subsequence.\n * @param {array} items1 The first list of items.\n * @param {array} items2 The second list of items.\n * @return {array} The longest common subsequence.\n */\n function lcss(items1, items2) {\n\n var M, i, j, n, result, length;\n M = lcsLengths(items1, items2);\n length = M[items1.length][items2.length];\n result = [];\n i = items1.length;\n j = items2.length;\n n = length - 1;\n while (n >= 0) {\n if (items1[i - 1] == items2[j - 1]) {\n result[n] = items1[i - 1];\n n -= 1;\n i -= 1;\n j -= 1;\n } else if (M[i - 1][j] == M[i][j]) {\n i -= 1;\n } else {\n j -= 1;\n }\n }\n return result;\n }\n\n /**\n * Process the given token list and a subsequence of it, concatenating\n * tokens and wrapping all items not in the subsequence with\n * del tags (or whatever strings are specified by startDel, endDel).\n * @param {array} tokens A list of tokens in the original string.\n * @param {array} subSeq A subsequence of the tokens array.\n * @param {string} startDel An optional string that denotes the start of\n * a sequence of tokens to be deleted. Default ''\n * @param {string} endDel An optional string to mark the end of a sequence\n * of deleted tokens. Default ''.\n * @return {string} The concatenated sequence of tokens with start and\n * end delete tokens inserted to mark where the tokens from the first\n * parameter are not present in the second.\n */\n function insertDels(tokens, subSeq, startDel, endDel) {\n var html = \"\",\n deleting = false,\n i,\n ssi = 0;\n if (startDel === undefined) {\n startDel = '';\n }\n if (endDel === undefined) {\n endDel = '';\n }\n for (i = 0; i < tokens.length; i += 1) {\n if (ssi < subSeq.length && tokens[i] == subSeq[ssi]) {\n if (deleting) {\n html += endDel;\n deleting = false;\n }\n ssi += 1;\n } else {\n if (!deleting) {\n html += startDel;\n deleting = true;\n }\n }\n\n html += tokens[i];\n }\n if (deleting) {\n html += endDel;\n }\n return html;\n }\n\n /**\n * @param {string} elem An HTML element\n * @return {string} The HTML element type (i.e. its tag name) in lower case\n */\n function elType(elem) {\n return elem.tagName.toLowerCase();\n }\n\n /**\n * Return the sequence of tokens from the given HTML element.\n * A token is either a single character or an HTML entity (&.*;)\n * Extra 'leftward-arrow-with-hook' characters (\\u21A9) are added\n * at the ends of lines.\n * @param {string} element The HTML element whose contents are to be tokenised.\n * @return {array} The list of tokens extracted from the element.\n */\n function getTokens(element) {\n var isPre = elType(element) === 'pre',\n text = element.innerHTML,\n seq,\n i = 0;\n\n /**\n * Extract and return the next token starting at text[i]. Update i.\n * Precondition: i < text.length.\n */\n function nextToken() {\n var token, match;\n if (text[i] != '&') {\n token = text[i];\n i = i + 1;\n } else {\n match = text.substring(i, text.length).match(/(^&[a-zA-Z]+;)|(^[0-9]+;)|(^[xX][0-9a-fA-F]+;)/);\n if (match === null) {\n token = text[i];\n i = i + 1;\n } else {\n token = match[0];\n i = i + token.length;\n }\n }\n return token;\n }\n\n if (isPre) {\n text = text.replace(/\\n/g, NLCHAR + '\\n');\n }\n text = text.replace(/( )/g, NLCHAR + '$1');\n seq = [];\n i = 0;\n while (i < text.length) {\n seq.push(nextToken());\n }\n\n return seq;\n }\n\n /**\n * Given (references to) two HTML elements, extract the innerHTML\n * of both, find the longest common subsequence of chars and wrap text not\n * in that subsequence in del elements.\n * elements within the innerHTML are preceded by a\n * Unicode \"leftwards arrow with hook\" ('\\u21A9') so that line break changes\n * can be highlighted.\n * @param {string} firstEl The first HTML element to be processed.\n * @param {string} secondEl The second HTML element to be processed.\n */\n function showDifferences(firstEl, secondEl) {\n var openDelTag = '',\n closeDelTag = '',\n seq1,\n seq2,\n css;\n\n seq1 = getTokens(firstEl);\n seq2 = getTokens(secondEl);\n css = lcss(seq1, seq2);\n firstEl.innerHTML = insertDels(seq1, css, openDelTag, closeDelTag);\n secondEl.innerHTML = insertDels(seq2, css, openDelTag, closeDelTag);\n }\n\n /**\n * Given (references to) two DOM elements, delete all and \n * tags from the innerHTML of both. Also remove the \"leftwards arrows with\n * hooks\".\n * @param {string} firstEl The first HTML element to be processed.\n * @param {string} secondEl The second HTML element to be processed.\n */\n function hideDifferences(firstEl, secondEl) {\n var replPat = new RegExp('(?del[^>]*>)|(' + NLCHAR + ')', 'g');\n firstEl.innerHTML = firstEl.innerHTML.replace(replPat, '');\n secondEl.innerHTML = secondEl.innerHTML.replace(replPat, '');\n }\n\n /**\n * Now the API for applying diffs to rows in a CodeRunner\n * results table. Defines a class with methods initDiffButton and\n * processAllRows.\n * @param {array} tableRows The list of rows from the CodeRunner results table.\n * @param {int} gotCol The column number of the 'Got' column in the table.\n * @param {int} expectedCol The column number of the 'Expected' column.\n * @param {function} f The function to apply to the (expected, got) pair.\n */\n function processAllRows(tableRows, gotCol, expectedCol, f) {\n var row,\n cells,\n expected,\n got;\n\n for (var i = 0; i < tableRows.length; i++) {\n row = tableRows[i];\n cells = row.getElementsByTagName('td');\n expected = cells[expectedCol].children[0];\n got = cells[gotCol].children[0];\n f(expected, got);\n }\n }\n\n /**\n * Initialise the Show Differences button.\n * @param {string} buttonId The ID of the Show Differences button.\n * @param {string} showValue The text in the button initially.\n * @param {string} hideValue The text in the button when differences are showing.\n * @param {string} expectedString The column header denoting the 'Expected' column.\n * @param {string} gotString The column header denoting the 'Got' column.\n */\n function initDiffButton(buttonId, showValue, hideValue, expectedString, gotString) {\n var diffButton = $('[id=\"' + buttonId + '\"]'),\n table,\n tableRows,\n thEls,\n columnCount=0,\n gotCol=-1,\n expectedCol=-1;\n\n table = diffButton.closest('div.coderunner-test-results');\n thEls = table.find('thead tr').children();\n tableRows = table.find('tbody tr');\n\n // Find 'Expected' and 'Got' columns.\n thEls.each(function() {\n if ($(this).html() === gotString) {\n gotCol = columnCount;\n } else if ($(this).html() === expectedString) {\n expectedCol = columnCount;\n }\n columnCount += 1;\n });\n\n if (gotCol !== -1 && expectedCol !== -1) {\n diffButton.on(\"click\", function() {\n if (diffButton.prop('value') === showValue) {\n processAllRows(tableRows.toArray(), gotCol, expectedCol, showDifferences);\n diffButton.prop('value', hideValue);\n } else {\n processAllRows(tableRows.toArray(), gotCol, expectedCol, hideDifferences);\n diffButton.prop('value', showValue);\n }\n });\n } else {\n diffButton.enabled = false;\n diffButton.hide();\n }\n }\n\n return { \"initDiffButton\": initDiffButton };\n});\n"],"names":["define","$","lcss","items1","items2","M","i","j","n","result","length","lengths","n1","n2","has_fill","fill","Array","Math","max","lcsLengths","insertDels","tokens","subSeq","startDel","endDel","html","deleting","ssi","undefined","getTokens","element","seq","token","match","isPre","tagName","toLowerCase","text","innerHTML","replace","NLCHAR","push","substring","showDifferences","firstEl","secondEl","seq1","seq2","css","hideDifferences","replPat","RegExp","processAllRows","tableRows","gotCol","expectedCol","f","cells","getElementsByTagName","children","buttonId","showValue","hideValue","expectedString","gotString","table","thEls","diffButton","columnCount","closest","find","each","this","on","prop","toArray","enabled","hide"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/textareas.min.js.map b/amd/build/textareas.min.js.map
new file mode 100644
index 000000000..617c95e64
--- /dev/null
+++ b/amd/build/textareas.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"textareas.min.js","sources":["../src/textareas.js"],"sourcesContent":["/**\n * 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/**\n * JavaScript for handling textareas and form actions in CodeRunner question\n * editing forms and student question answering forms.\n *\n * @module qtype_coderunner/textareas\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Function to initialise all code-input text-areas in a page.\n * Used by the form editor but can't be used for question text areas as\n * renderer.php is called once for each question in a quiz, and there is\n * no communication between the questions.\n */\n function setupAllTAs() {\n $('textarea.edit_code').each(initTextArea);\n }\n\n /**\n * Initialise a particular text area (TA), given its ID (which may contain\n * colons, so can't use jQuery selector).\n * @param {string} taId The ID of the textarea html element.\n */\n function initQuestionTA(taId) {\n $(document.getElementById(taId)).each(initTextArea);\n }\n\n\n /**\n * Set up to expand or collapse the question author's answer when the user clicks\n * on the show/hide answer button.\n * @param {string} linkId The ID of the link that the user clicks.\n * @param {sting} divId The ID of the div to be shown or hidden.\n */\n function setupShowHideAnswer(linkId, divId) {\n let link = $(document.getElementById(linkId)),\n div = $(document.getElementById(divId)),\n hdr = link.children()[0], // The
element.\n hdrText = hdr.innerText; // Its body.\n link.click(() => {\n if (div.is(\":visible\")) {\n div.hide();\n hdr.innerText = hdrText.replace(\"\\u25BE\", \"\\u25B8\");\n } else {\n div.show();\n div.trigger('mousemove'); // So user sees contents.\n hdr.innerText = hdrText.replace(\"\\u25B8\", \"\\u25BE\");\n }\n });\n }\n\n /**\n * Set up the JavaScript to handle a text area 'this'.\n * It just does rudimentary autoindent on return and replaces tabs with\n * 4 spaces always.\n * For info on key handling browser inconsistencies see\n * http://unixpapa.com/js/key.html\n * If the AceEditor is handling a text area, this code is unused as\n * the actual textarea is hidden by Ace.\n */\n function initTextArea() {\n var TAB = 9,\n ENTER = 13,\n ESC = 27,\n KEY_M = 77;\n\n $(this).data('clickInProgress', false);\n $(this).data('capturingTab', true);\n\n $(this).on('mousedown', function() {\n /*\n * Event order seems to be (\\ is where the mouse button is pressed, / released):\n * Chrome: \\ mousedown, mouseup, focusin / click.\n * Firefox/IE: \\ mousedown, focusin / mouseup, click.\n */\n $(this).data('clickInProgress', true);\n });\n\n $(this).on('focusin', function() {\n /*\n * At first, pressing TAB moves focus.\n */\n $(this).data('capturingTab', $(this).data('clickInProgress'));\n });\n\n $(this).on('click', function() {\n $(this).data('clickInProgress', false);\n });\n\n $(this).on('keydown', function(e) {\n /*\n * Don't autoindent when behat testing in progress.\n */\n if (window.hasOwnProperty('behattesting') && window.behattesting) { return; }\n\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode == TAB && $(this).data('capturingTab')) {\n /*\n * Ignore SHIFT/TAB. Insert 4 spaces on TAB.\n */\n if (e.shiftKey || insertString(this, \" \")) {\n e.preventDefault();\n }\n }\n else if (e.keyCode === ENTER && this.selectionStart !== undefined) {\n /*\n * Handle autoindent only on non-IE.\n */\n var before = this.value.substring(0, this.selectionStart);\n var eol = before.lastIndexOf(\"\\n\");\n var line = before.substring(eol + 1); // Take from eol to end.\n var indent = \"\";\n for (var i = 0; i < line.length && line.charAt(i) === ' '; i++) {\n indent = indent + \" \";\n }\n if (insertString(this, \"\\n\" + indent)) {\n e.preventDefault();\n }\n /*\n * Once the user has started typing, TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n else if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n /*\n * CTRL + M toggles TAB capturing mode.\n * This is the short-cut recommended by\n * https:www.w3.org/TR/wai-aria-practices/#richtext.\n */\n $(this).data('capturingTab', !$(this).data('capturingTab'));\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n /*\n * ESC always stops capturing TAB.\n */\n $(this).data('capturingTab', false);\n }\n else if (!(e.ctrlKey || e.altKey)) {\n /*\n * Once the user has started typing (not modifier keys) TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n }\n });\n }\n\n /**\n * Insert into the given textarea ta the given string sToInsert.\n * @param {html_element} ta The textarea to be updated.\n * @param {string} sToInsert The string to be inserted at the current selection\n * point.\n */\n function insertString(ta, sToInsert) {\n if (ta.selectionStart !== undefined) { // Firefox etc.\n var before = ta.value.substring(0, ta.selectionStart);\n var selSave = ta.selectionEnd;\n var after = ta.value.substring(ta.selectionEnd, ta.value.length);\n\n /**\n * Update the text field.\n */\n var tmp = ta.scrollTop; // Inhibit annoying auto-scroll.\n ta.value = before + sToInsert + after;\n var pos = selSave + sToInsert.length;\n ta.selectionStart = pos;\n ta.selectionEnd = pos;\n ta.scrollTop = tmp;\n return true;\n\n }\n else if (document.selection && document.selection.createRange) { // IE.\n /*\n * TODO: check if this still works. OLD CODE.\n */\n var r = document.selection.createRange();\n var dr = r.duplicate();\n dr.moveToElementText(ta);\n dr.setEndPoint(\"EndToEnd\", r);\n r.text = sToInsert;\n return true;\n }\n /*\n * Other browsers we can't handle.\n */\n else {\n return false;\n }\n }\n\n return {\n setupAllTAs: setupAllTAs,\n initQuestionTA: initQuestionTA,\n setupShowHideAnswer: setupShowHideAnswer,\n };\n});\n"],"names":["define","$","initTextArea","this","data","on","e","window","hasOwnProperty","behattesting","undefined","which","keyCode","shiftKey","insertString","preventDefault","selectionStart","before","value","substring","eol","lastIndexOf","line","indent","i","length","charAt","ctrlKey","altKey","ta","sToInsert","selSave","selectionEnd","after","tmp","scrollTop","pos","document","selection","createRange","r","dr","duplicate","moveToElementText","setEndPoint","text","setupAllTAs","each","initQuestionTA","taId","getElementById","setupShowHideAnswer","linkId","divId","link","div","hdr","children","hdrText","innerText","click","is","hide","replace","show","trigger"],"mappings":";;;;;;;;AA2BAA,oCAAO,CAAC,WAAW,SAASC,YAsDfC,eAMLD,EAAEE,MAAMC,KAAK,mBAAmB,GAChCH,EAAEE,MAAMC,KAAK,gBAAgB,GAE7BH,EAAEE,MAAME,GAAG,aAAa,WAMpBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,WAIlBJ,EAAEE,MAAMC,KAAK,eAAgBH,EAAEE,MAAMC,KAAK,uBAG9CH,EAAEE,MAAME,GAAG,SAAS,WAChBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,SAASC,QAIvBC,OAAOC,eAAe,iBAAmBD,OAAOE,mBAEpCC,IAAZJ,EAAEK,OAAmC,IAAZL,EAAEK,UAlCzB,GAmCEL,EAAEM,SAAkBX,EAAEE,MAAMC,KAAK,iBAI7BE,EAAEO,UAAYC,aAAaX,KAAM,UACjCG,EAAES,sBAGL,GA1CD,KA0CKT,EAAEM,cAA6CF,IAAxBP,KAAKa,eAA8B,SAI3DC,OAASd,KAAKe,MAAMC,UAAU,EAAGhB,KAAKa,gBACtCI,IAAMH,OAAOI,YAAY,MACzBC,KAAOL,OAAOE,UAAUC,IAAM,GAC9BG,OAAS,GACJC,EAAI,EAAGA,EAAIF,KAAKG,QAA6B,MAAnBH,KAAKI,OAAOF,GAAYA,IACvDD,QAAkB,IAElBT,aAAaX,KAAM,KAAOoB,SAC1BjB,EAAES,iBAKNd,EAAEE,MAAMC,KAAK,gBAAgB,QAzD7B,KA2DKE,EAAEM,SAAqBN,EAAEqB,UAAYrB,EAAEsB,QAM5C3B,EAAEE,MAAMC,KAAK,gBAAiBH,EAAEE,MAAMC,KAAK,iBAC3CE,EAAES,kBAnEJ,KAqEOT,EAAEM,QAIPX,EAAEE,MAAMC,KAAK,gBAAgB,GAEtBE,EAAEqB,SAAWrB,EAAEsB,QAItB3B,EAAEE,MAAMC,KAAK,gBAAgB,eAYpCU,aAAae,GAAIC,mBACIpB,IAAtBmB,GAAGb,eAA8B,KAC7BC,OAASY,GAAGX,MAAMC,UAAU,EAAGU,GAAGb,gBAClCe,QAAUF,GAAGG,aACbC,MAAQJ,GAAGX,MAAMC,UAAUU,GAAGG,aAAcH,GAAGX,MAAMO,QAKrDS,IAAML,GAAGM,UACbN,GAAGX,MAAQD,OAASa,UAAYG,UAC5BG,IAAML,QAAUD,UAAUL,cAC9BI,GAAGb,eAAiBoB,IACpBP,GAAGG,aAAeI,IAClBP,GAAGM,UAAYD,KACR,EAGN,GAAIG,SAASC,WAAaD,SAASC,UAAUC,YAAa,KAIvDC,EAAIH,SAASC,UAAUC,cACvBE,GAAKD,EAAEE,mBACXD,GAAGE,kBAAkBd,IACrBY,GAAGG,YAAY,WAAYJ,GAC3BA,EAAEK,KAAOf,WACF,SAMA,QAIR,CACHgB,uBAjLA7C,EAAE,sBAAsB8C,KAAK7C,eAkL7B8C,wBA1KoBC,MACpBhD,EAAEoC,SAASa,eAAeD,OAAOF,KAAK7C,eA0KtCiD,6BAhKyBC,OAAQC,WAC7BC,KAAOrD,EAAEoC,SAASa,eAAeE,SACjCG,IAAMtD,EAAEoC,SAASa,eAAeG,QAChCG,IAAMF,KAAKG,WAAW,GACtBC,QAAUF,IAAIG,UAClBL,KAAKM,OAAM,KACHL,IAAIM,GAAG,aACPN,IAAIO,OACJN,IAAIG,UAAYD,QAAQK,QAAQ,IAAU,OAE1CR,IAAIS,OACJT,IAAIU,QAAQ,aACZT,IAAIG,UAAYD,QAAQK,QAAQ,IAAU"}
\ No newline at end of file
diff --git a/amd/build/ui_ace.min.js.map b/amd/build/ui_ace.min.js.map
new file mode 100644
index 000000000..9e67f3290
--- /dev/null
+++ b/amd/build/ui_ace.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_ace.min.js","sources":["../src/ui_ace.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 * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n session.setValue(this.textarea.val());\n\n this.fixSlowLoad();\n\n // Set theme if available (not currently enabled).\n if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n }\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // Nothing to do ... always sync'd\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 0;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n // Sometimes Ace editors do not load until the mouse is moved. To fix this,\n // 'move' the mouse using JQuerry when the editor div enters the viewport.\n AceWrapper.prototype.fixSlowLoad = function () {\n const observer = new IntersectionObserver( () => {\n $(document).trigger('mousemove');\n });\n const editNode = this.editNode.get(0); // Non-JQuerry node.\n observer.observe(editNode);\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","getSession","setValue","val","fixSlowLoad","theme","setTheme","matchMedia","matches","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","failed","failMessage","sync","syncIntervalSecs","language","session","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","observer","IntersectionObserver","trigger","observe","getValue","container","addEventListener","e","undefined","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_ace_gapfiller.min.js.map b/amd/build/ui_ace_gapfiller.min.js.map
new file mode 100644
index 000000000..4b640e3ad
--- /dev/null
+++ b/amd/build/ui_ace_gapfiller.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_ace_gapfiller.min.js","sources":["../src/ui_ace_gapfiller.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 util.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 * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","matchMedia","matches","setTheme","theme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_gapfiller.min.js.map b/amd/build/ui_gapfiller.min.js.map
new file mode 100644
index 000000000..5dcfd71fe
--- /dev/null
+++ b/amd/build/ui_gapfiller.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_gapfiller.min.js","sources":["../src/ui_gapfiller.js"],"sourcesContent":["/**\n * 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 util.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/**\n * Implementation of the gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer box with a div\n * consisting of pre-formatted text supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra). HTML\n * entry or textarea elements are then inserted at\n * specified points. It is intended primarily for use with coding questions\n * where the answerbox presents the students with code that has smallish bits\n * missing.\n *\n * The locations within the globalextra text at which the input elements are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * for an HTML input element\n *\n * or\n *\n * {[ rows, columns ]}\n *\n * for a textarea element\n *\n * where size, rows and column are integer literals. These respectively\n * inject an HTML input element or a textarea element of the\n * specified size.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the input element insertion tags are by default '{[' and\n * ']}', but can be changed by an optional ui parameter gap_filler_delimiters,\n * which must be a 2-element array of strings. For example:\n *\n * {\"gap_filler_delimiters\": [\"{{\", \"}}\"]}\n *\n * Note that the double-brace delimiters in that example are the same as those\n * used by Twig, so using them instead of the default would prevent you from\n * ever adding Twig expansion (e.g. for randomisation) to the question.\n *\n * @module qtype_coderunner/ui_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n /**\n * Constructor for UI. Source html comes from data-globalextra by default,\n * else from whatever source is specified by the uiParams parameter.\n * @param {string} textareaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\n function GapfillerUi(textareaId, width, height, uiParams) {\n var html;\n this.textArea = $(document.getElementById(textareaId));\n this.readOnly = this.textArea.prop('readonly');\n this.uiParams = uiParams;\n this.fail = false;\n this.htmlDiv = null;\n this.source = uiParams.ui_source || 'globalextra';\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for HTML in ui_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n html = this.textArea.attr('data-globalextra');\n } else {\n html = this.textArea.attr('data-test0');\n }\n this.html = html.replace('<', '<');\n this.reload();\n }\n\n GapfillerUi.prototype.failed = function() {\n return this.fail; // Currently always true. See reload function.\n };\n\n /**\n * Copy the serialised version of the HTML UI area to the TextArea.\n */\n GapfillerUi.prototype.sync = function() {\n var\n serialisation = [], // A list of field values.\n empty = true;\n\n this.getFields().each(function() {\n var name, value;\n name = $(this).attr('name');\n if (name !== 'cr_gapfiller_field') {\n alert('Unexpected UI element found in answer box');\n } else {\n value = $(this).val();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n });\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n GapfillerUi.prototype.getElement = function() {\n return this.htmlDiv;\n };\n\n GapfillerUi.prototype.getFields = function() {\n return $(this.htmlDiv).find('.coderunner-ui-element');\n };\n\n /**\n * Set the value of the jQuery field to the given value.\n * If the field is a radio button or a checkbox,\n * the checked attribute is set. Otherwise the field's\n * val() function is called to set the value.\n * @param {object} field The JQuery field elemetn whose value is to be set.\n * @param {string} value The value to be used.\n */\n GapfillerUi.prototype.setField = function(field, value) {\n if (field.attr('type') === 'checkbox' || field.attr('type') === 'radio') {\n field.prop('checked', field.val() === value);\n } else {\n field.val(value);\n }\n };\n\n /**\n * Process the supplied HTML, HTML-escaping existing HTML\n * and inserting the input and textarea elements\n * at the marked locations.\n */\n GapfillerUi.prototype.markedUpHtml = function() {\n\n /**\n * Prefix any regular expression special chars in s with a backslash.\n * @param {string} s The string whose special values are to be escaped.\n * @return {string} The escaped string.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n var sepLeft = reEscape('{['),\n sepRight = reEscape(']}'),\n splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+, *\\\\d+)) *' + sepRight),\n bits = this.html.split(splitter),\n result = '
' + bits[0],\n i;\n\n for (i = 1; i < bits.length; i += 2) {\n result += this.markUp(bits[i]);\n if (i + 1 < bits.length) {\n result += bits[i + 1];\n }\n }\n\n result = result + '
';\n return result;\n };\n\n\n /**\n * Return the HTML element to insert given the tag contents, which\n * should be either a single integer (size of input element) or\n * two integers separated by a comma (rows and cols of textarea).\n * @param {string} tagContents The text between the delimiters of a gap specifier.\n * @return {string} The HTML for an input or textarea element build\n * according to the given tagContents.\n */\n GapfillerUi.prototype.markUp = function(tagContents) {\n var numbers, result='';\n\n /**\n * The function to handle an 'input' tag.\n * @param {int} size The size of the input element to return.\n * @return {string} The html for a text area to the given specs.\n */\n function input(size) {\n return '';\n }\n\n /**\n * The function to handle a 'textarea' tag.\n * @param {int} rows The number of rows of text required.\n * @param {int} cols The number of columns of text required.\n * @return The HTML for a textarea to the given specs.\n */\n function textarea(rows, cols) {\n return '';\n }\n\n numbers = tagContents.split(',');\n if (numbers.length == 1) {\n result = input(parseInt(numbers[0]));\n } else {\n result = textarea(parseInt(numbers[0]), parseInt(numbers[1]));\n }\n\n return result;\n };\n\n /**\n * Reload the HTML fields from the given serialisation.\n * Unlike other plugins, we don't actually fail the load if, for example\n * the number of fields doesn't match the number of values in the\n * serialisation. We simply set any excess fields for which data\n * in unavailable to '???' or discard extra values. This ensures\n * that at least the unfilled content is presented to the question author\n * when the number of fields is altered during editing.\n */\n GapfillerUi.prototype.reload = function() {\n var\n content = $(this.textArea).val(), // JSON-encoded HTML element settings.\n value,\n values,\n i,\n fields,\n outerDiv = \"
\");\n if (content) {\n try {\n values = JSON.parse(content);\n fields = this.getFields();\n for (i = 0; i < fields.length; i++) {\n value = i < values.length ? values[i] : '???';\n this.setField($(fields[i]), value);\n }\n } catch(e) {\n /**\n * Just ignore errors\n */\n }\n }\n };\n\n GapfillerUi.prototype.resize = function() {}; // Nothing to see here. Move along please.\n\n GapfillerUi.prototype.hasFocus = function() {\n var focused = false;\n this.getFields().each(function() {\n if (this === document.activeElement) {\n focused = true;\n }\n });\n return focused;\n };\n\n /**\n * Destroy the GapFiller UI and serialise the result into the original text area.\n */\n GapfillerUi.prototype.destroy = function() {\n this.sync();\n $(this.htmlDiv).remove();\n this.htmlDiv = null;\n };\n\n return {\n Constructor: GapfillerUi\n };\n});\n"],"names":["define","$","GapfillerUi","textareaId","width","height","uiParams","html","textArea","document","getElementById","readOnly","this","prop","fail","htmlDiv","source","ui_source","alert","attr","replace","reload","prototype","failed","sync","serialisation","empty","getFields","each","value","val","push","JSON","stringify","getElement","find","setField","field","markedUpHtml","reEscape","s","c","result","i","length","j","sepLeft","sepRight","splitter","RegExp","bits","split","markUp","tagContents","numbers","rows","cols","parseInt","values","fields","content","parse","e","resize","hasFocus","focused","activeElement","destroy","remove","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_graph.min.js.map b/amd/build/ui_graph.min.js.map
new file mode 100644
index 000000000..707291cfd
--- /dev/null
+++ b/amd/build/ui_graph.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_graph.min.js","sources":["../src/ui_graph.js"],"sourcesContent":["/**\n * This file is part of Moodle - http:moodle.org/\n *\n * Much of this code is from Finite State Machine Designer:\n */\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n/**\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 util.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/**\n * JavaScript to interface to the Graph editor, which is used both in\n * the author editing page and by the student question submission page.\n *\n * @module qtype_coderunner/ui_graph\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/graphutil', 'qtype_coderunner/graphelements'], function($, util, elements) {\n\n /**\n * Constructor for a GraphCanvas object, which is a wrapper for a Graph's HTML canvas\n * object.\n * @param {object} parent The Graph that owns this object.\n * @param {string} canvasId The ID of the HTML canvas to be wrapped by this object.\n * @param {int} w The required width of the wrapper.\n * @param {int} h The required height of the wrapper.\n */\n function GraphCanvas(parent, canvasId, w, h) {\n this.HANDLE_SIZE = 10;\n\n this.parent = parent;\n this.canvas = $(document.createElement(\"canvas\"));\n this.canvas.attr({\n id: canvasId,\n class: \"coderunner_graphcanvas\",\n tabindex: 1 // So canvas can get focus.\n });\n this.canvas.css({'background-color': 'white'});\n\n this.canvas.on('mousedown', function(e) {\n return parent.mousedown(e);\n });\n\n this.canvas.on('mouseup', function(e) {\n return parent.mouseup(e);\n });\n\n this.canvas.on('dblclick', function(e) {\n return parent.dblclick(e);\n });\n\n this.canvas.on('keydown', function(e) {\n return parent.keydown(e);\n });\n\n this.canvas.on('mousemove', function(e) {\n return parent.mousemove(e);\n });\n\n this.canvas.on('keypress', function(e) {\n return parent.keypress(e);\n });\n\n /**\n * Resize this object to then given dimensions.\n * @param {int} w Required width.\n * @param {int} h Required height.\n */\n this.resize = function(w, h) {\n this.canvas.attr(\"width\", w);\n this.canvas.attr(\"height\", h);\n };\n\n this.resize(w, h);\n }\n\n /**\n * Constructor for the Graph object.\n * This is the ui component for a graph-drawing coderunner question.\n *\n * Relevant ui parameters:\n *\n * isfsm. True if the graph is of a Finite State Machine.\n * If true, the graph can contain an incoming edge from nowhere\n * (the start edge). Default: true.\n * isdirected. True if edges are directed. Default: true.\n * noderadius. The radius of a node, in pixels. Default: 26.\n * fontsize. The font size used for node and edge labels. Default: 20 points.\n * textoffset. An offset in pixels used to determine how far from the link\n * a label is initially positioned. Default 5. Largely defunct\n * now that link text can be dragged.\n * helpmenutext. A string to be used in lieu of the default Help info, if supplied.\n * No default.\n * locknodepositions. True to prevent the user from moving nodes. Useful when the\n * answer box is preloaded with a graph that the student has to\n * annotate by changing node or edge labels or by\n * adding/removing edges. Note, though that nodes can still be\n * added and deleted. See locknodeset.\n * locknodeset. True to prevent the user from adding or deleting nodes, or\n * toggling node types to/from acceptors.\n * locknodelabels: True to prevent the user from editing node labels. This\n * will also prevent any new nodes having non-empty labels.\n * lockedgepositions. True to prevent the user from dragging edges to change\n * their curvature. Possibly useful if the answer box is\n * preloaded with a graph that the student has to annotate by\n * changing node or edge labels or by adding/removing edges.\n * Also ensures that edges added by a student are straight, e.g.\n * to draw a polygon on a set of given points. Note, though that\n * edges can still be added and deleted. See lockedgeset.\n * lockedgeset. True to prevent the user from adding or deleting edges.\n * lockedgelabels: True to prevent the user from editing edge labels. This\n * also prevents any new edges from having labels.\n * @param {string} textareaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\n function Graph(textareaId, width, height, uiParams) {\n /**\n * Constructor.\n */\n var save_this = this;\n\n this.SNAP_TO_PADDING = 6;\n this.DUPLICATE_LINK_OFFSET = 16; // Pixels offset for a duplicate link\n this.HIT_TARGET_PADDING = 6; // Pixels.\n this.DEFAULT_NODE_RADIUS = 26; // Pixels. UI parameter noderadius can override this.\n this.DEFAULT_FONT_SIZE = 20; // px. UI parameter fontsize can override this.\n this.DEFAULT_TEXT_OFFSET = 5; // Link label tweak. UI params can override.\n this.DEFAULT_LINK_LABEL_REL_DIST = 0.5; // Relative distance along link to place labels\n this.MAX_VERSIONS = 30; // Maximum number of versions saved for undo/redo\n\n this.canvasId = 'graphcanvas_' + textareaId;\n this.textArea = $(document.getElementById(textareaId));\n this.helpText = ''; // Obtained by JSON - see below.\n this.readOnly = this.textArea.prop('readonly');\n this.uiParams = uiParams;\n this.graphCanvas = new GraphCanvas(this, this.canvasId, width, height);\n this.caretVisible = true;\n this.caretTimer = 0; // Need global so we can kill a running timer.\n this.originalClick = null;\n this.nodes = [];\n this.links = [];\n this.selectedObject = null; // Either a elements.Link or a elements.Node or a elements.Button.\n this.currentLink = null;\n this.movingObject = false;\n this.fail = false; // Will be set true if reload fails (can't deserialise).\n this.failString = null; // Language string key for fail error message.\n this.versions = [];\n this.versionIndex = -1; //Index of current state of graph in versions list\n\n this.helpBox = new elements.HelpBox(this, 0, 0); // Button that opens a help text box\n this.clearButton = new elements.Button(this, 60, 0, \"Clear\"); // Button that clears the canvas\n this.clearButton.onClick = function() {\n if (confirm(\"Are you sure you want to clear the diagram?\")) {\n this.parent.clear();\n }\n };\n this.buttons = [this.helpBox, this.clearButton];\n\n /**\n * Legacy support for locknodes and lockedges.\n */\n if ('locknodes' in uiParams) {\n uiParams.locknodepositions = uiParams.locknodes;\n }\n if ('lockedges' in uiParams) {\n uiParams.lockedgepositions = uiParams.lockedges;\n }\n\n if ('helpmenutext' in uiParams) {\n this.helpText = uiParams.helpmenutext;\n } else {\n require(['core/str'], function(str) {\n /**\n * Get help text via AJAX.\n */\n var helpPresent = str.get_string('graphhelp', 'qtype_coderunner');\n $.when(helpPresent).done(function(graphhelp) {\n save_this.helpText = graphhelp;\n });\n });\n }\n this.reload();\n if (!this.fail) {\n this.draw();\n }\n }\n\n Graph.prototype.failed = function() {\n return this.fail;\n };\n\n Graph.prototype.failMessage = function() {\n return this.failString;\n };\n\n Graph.prototype.getElement = function() {\n return this.getCanvas();\n };\n\n Graph.prototype.hasFocus = function() {\n return document.activeElement == this.getCanvas();\n };\n\n Graph.prototype.getCanvas = function() {\n var canvas = this.graphCanvas.canvas[0];\n return canvas;\n };\n\n Graph.prototype.nodeRadius = function() {\n return this.uiParams.noderadius ? this.uiParams.noderadius : this.DEFAULT_NODE_RADIUS;\n };\n\n Graph.prototype.fontSize = function() {\n return this.uiParams.fontsize ? this.uiParams.fontsize : this.DEFAULT_FONT_SIZE;\n };\n\n Graph.prototype.isFsm = function() {\n return this.uiParams.isfsm !== undefined ? this.uiParams.isfsm : true;\n };\n\n\n Graph.prototype.textOffset = function() {\n return this.uiParams.textoffset ? this.uiParams.textoffset : this.DEFAULT_TEXT_OFFSET;\n };\n\n /**\n * Draw an arrow head if this is a directed graph. Otherwise do nothing.\n * @param {object} c The graphic context.\n * @param {int} x The x location of the arrow head.\n * @param {int} y The y location of the arrow head.\n * @param {float} angle The angle of the arrow.\n */\n Graph.prototype.arrowIfReqd = function(c, x, y, angle) {\n if (this.uiParams.isdirected === undefined || this.uiParams.isdirected) {\n util.drawArrow(c, x, y, angle);\n }\n };\n\n /**\n * Copy the serialised version of the graph to the TextArea.\n */\n Graph.prototype.sync = function() {\n /**\n * Nothing to do ... always sync'd.\n */\n };\n\n /**\n * Disable autosync, too.\n */\n Graph.prototype.syncIntervalSecs = function() {\n return 0;\n };\n\n Graph.prototype.keypress = function(e) {\n var key = util.crossBrowserKey(e);\n\n if (this.readOnly) {\n return;\n }\n\n if(key >= 0x20 &&\n key <= 0x7E &&\n !e.metaKey &&\n !e.altKey &&\n !e.ctrlKey &&\n key !== 37 && //Don't register arrow keys\n key !== 39 &&\n this.selectedObject !== null &&\n this.canEditText()) {\n if (this.selectedObject.justMoved) {\n this.saveVersion();\n }\n this.selectedObject.justMoved = false;\n this.selectedObject.textBox.insertChar(String.fromCharCode(key));\n this.resetCaret();\n this.draw();\n\n /**\n * Don't let keys do their actions (like space scrolls down the page).\n */\n return false;\n } else if(key === 8 || key === 0x20 || key === 9) {\n /**\n * Disable scrolling on backspace, tab and space.\n */\n return false;\n }\n };\n\n Graph.prototype.mousedown = function(e) {\n var mouse = util.crossBrowserRelativeMousePos(e);\n\n if (this.readOnly) {\n return;\n }\n\n this.selectedObject = this.selectObject(mouse.x, mouse.y);\n this.movingObject = false;\n this.movingGraph = false;\n this.movingText = false;\n this.originalClick = mouse;\n\n this.saveVersion();\n\n if (this.selectedObject !== this.helpBox){\n this.helpBox.helpOpen = false;\n }\n\n if(this.selectedObject !== null) {\n if(this.selectedObject instanceof elements.Button){\n this.selectedObject.onClick();\n } else if(e.shiftKey && this.selectedObject instanceof elements.Node) {\n if (!this.uiParams.lockedgeset) {\n this.currentLink = new elements.SelfLink(this, this.selectedObject, mouse);\n }\n } else if (e.altKey && this.selectedObject instanceof elements.Node) {\n /**\n * Moving an entire connected graph component.\n */\n if (!this.uiParams.locknodepositions) {\n this.movingGraph = true;\n this.movingNodes = this.selectedObject.traverseGraph(this.links, []);\n for (var i = 0; i < this.movingNodes.length; i++) {\n this.movingNodes[i].setMouseStart(mouse.x, mouse.y);\n }\n }\n } else if (this.selectedObject instanceof elements.TextBox){\n if (!this.uiParams.lockedgelabels) {\n this.movingText = true;\n this.selectedObject.setMouseStart(mouse.x, mouse.y);\n this.selectedObject = this.selectedObject.parent;\n }\n } else if (!(this.uiParams.locknodepositions && this.selectedObject instanceof elements.Node) &&\n !(this.uiParams.lockedgepositions && this.selectedObject instanceof elements.Link)){\n this.movingObject = true;\n if(this.selectedObject.setMouseStart) {\n this.selectedObject.setMouseStart(mouse.x, mouse.y);\n }\n }\n this.selectedObject.justMoved = true;\n this.resetCaret();\n } else if(e.shiftKey && this.isFsm()) {\n this.currentLink = new elements.TemporaryLink(this, mouse, mouse);\n }\n\n this.draw();\n\n if(this.hasFocus()) {\n /**\n * Disable drag-and-drop only if the canvas is already focused.\n */\n return false;\n } else {\n /**\n * Otherwise, let the browser switch the focus away from wherever it was.\n */\n this.resetCaret();\n return true;\n }\n };\n\n /**\n * Return true if currently selected object has text that we are allowed\n * to edit.\n */\n Graph.prototype.canEditText = function() {\n var isNode = this.selectedObject instanceof elements.Node,\n isLink = (this.selectedObject instanceof elements.Link ||\n this.selectedObject instanceof elements.SelfLink);\n return 'textBox' in this.selectedObject &&\n ((isNode && !this.uiParams.locknodelabels) ||\n (isLink && !this.uiParams.lockedgelabels));\n };\n\n Graph.prototype.keydown = function(e) {\n var key = util.crossBrowserKey(e), i, nodeDeleted=false;\n\n if (this.readOnly) {\n return;\n }\n\n if(key === 8) { // Backspace key.\n if(this.selectedObject !== null && this.canEditText()) {\n this.selectedObject.textBox.deleteChar();\n this.resetCaret();\n this.draw();\n }\n /**\n * Backspace is a shortcut for the back button, but do NOT want to change pages.\n */\n return false;\n } else if(key === 46 && this.selectedObject !== null) { // Delete key\n this.saveVersion();\n for (i = 0; i < this.nodes.length; i++) {\n if (this.nodes[i] === this.selectedObject && !this.uiParams.locknodeset) {\n this.nodes.splice(i--, 1);\n nodeDeleted = true;\n }\n }\n for (i = 0; i < this.links.length; i++) {\n if((this.links[i] === this.selectedObject && !this.uiParams.lockedgeset) ||\n nodeDeleted && (\n this.links[i].node === this.selectedObject ||\n this.links[i].nodeA === this.selectedObject ||\n this.links[i].nodeB === this.selectedObject)) {\n this.links.splice(i--, 1);\n }\n }\n this.selectedObject = null;\n this.draw();\n } else if(key === 13) { // Enter key.\n if(this.selectedObject !== null) {\n /**\n * Deselect the object.\n */\n this.selectedObject = null;\n this.draw();\n }\n } else if(key === 37) { // Left arrow key\n if(this.selectedObject !== null && this.canEditText()) {\n this.selectedObject.textBox.caretLeft();\n this.resetCaret();\n this.draw();\n }\n } else if(key === 39) { // Right arrow key\n if(this.selectedObject !== null && this.canEditText()) {\n this.selectedObject.textBox.caretRight();\n this.resetCaret();\n this.draw();\n }\n } else if ((e.keyCode == 90 && e.ctrlKey && e.shiftKey) || (e.keyCode == 89 && e.ctrlKey)) { //CTRL+SHIFT+z or CTRL+y\n this.redo();\n } else if (e.keyCode == 90 && e.ctrlKey) { //CTRL+z\n this.undo();\n }\n };\n\n Graph.prototype.dblclick = function(e) {\n var mouse = util.crossBrowserRelativeMousePos(e);\n\n if (this.readOnly || this.uiParams.locknodeset) {\n return;\n }\n\n this.selectedObject = this.selectObject(mouse.x, mouse.y);\n\n this.saveVersion();\n\n if(this.selectedObject === null) {\n this.selectedObject = new elements.Node(this, mouse.x, mouse.y);\n this.nodes.push(this.selectedObject);\n this.selectedObject.justMoved = true;\n this.resetCaret();\n this.draw();\n } else {\n if(this.selectedObject instanceof elements.Node && this.isFsm()) {\n this.selectedObject.isAcceptState = !this.selectedObject.isAcceptState;\n this.draw();\n }\n }\n };\n\n Graph.prototype.resize = function(w, h) {\n this.graphCanvas.resize(w, h);\n this.draw();\n };\n\n Graph.prototype.mousemove = function(e) {\n var mouse = util.crossBrowserRelativeMousePos(e),\n closestPoint;\n\n if (this.readOnly) {\n return;\n }\n\n for (i = 0; i < this.buttons.length; i++){\n if (this.buttons[i].containsPoint(mouse.x, mouse.y)){\n this.buttons[i].highLighted = true;\n }else{\n this.buttons[i].highLighted = false;\n }\n this.draw();\n }\n\n if(this.currentLink !== null) {\n var targetNode = this.selectObject(mouse.x, mouse.y);\n if(!(targetNode instanceof elements.Node)) {\n targetNode = null;\n }\n\n if(this.selectedObject === null) {\n if(targetNode !== null) {\n this.currentLink = new elements.StartLink(this, targetNode, this.originalClick);\n } else {\n this.currentLink = new elements.TemporaryLink(this, this.originalClick, mouse);\n }\n } else {\n if(targetNode === this.selectedObject) {\n this.currentLink = new elements.SelfLink(this, this.selectedObject, mouse);\n } else if(targetNode !== null) {\n this.currentLink = new elements.Link(this, this.selectedObject, targetNode);\n } else {\n closestPoint = this.selectedObject.closestPointOnCircle(mouse.x, mouse.y);\n this.currentLink = new elements.TemporaryLink(this, closestPoint, mouse);\n }\n }\n this.draw();\n }\n if (this.movingGraph) {\n var nodes = this.movingNodes;\n for (var i = 0; i < nodes.length; i++) {\n nodes[i].trackMouse(mouse.x, mouse.y);\n this.snapNode(nodes[i]);\n }\n this.draw();\n } else if(this.movingText){\n this.selectedObject.textBox.setAnchorPoint(mouse.x, mouse.y);\n this.draw();\n } else if(this.movingObject) {\n this.selectedObject.setAnchorPoint(mouse.x, mouse.y);\n if(this.selectedObject instanceof elements.Node) {\n this.snapNode(this.selectedObject);\n }\n this.draw();\n }\n };\n\n Graph.prototype.mouseup = function() {\n\n if (this.readOnly) {\n return;\n }\n\n this.movingObject = false;\n this.movingGraph = false;\n this.movingText = false;\n\n if(this.currentLink !== null) {\n if(!(this.currentLink instanceof elements.TemporaryLink)) {\n this.selectedObject = this.currentLink;\n this.addLink(this.currentLink);\n this.resetCaret();\n }\n this.currentLink = null;\n this.draw();\n }\n };\n\n Graph.prototype.selectObject = function(x, y) {\n for (i = 0; i < this.buttons.length; i++){\n if (this.buttons[i].containsPoint(x, y)){\n return this.buttons[i];\n }\n }\n var i;\n for(i = 0; i < this.nodes.length; i++) {\n if(this.nodes[i].containsPoint(x, y)) {\n return this.nodes[i];\n }\n }\n for(i = 0; i < this.links.length; i++) {\n if(this.links[i].containsPoint(x, y)) {\n return this.links[i];\n }else if ('textBox' in this.links[i] && this.links[i].textBox.containsPoint(x, y)){\n return this.links[i].textBox;\n }\n }\n return null;\n };\n\n Graph.prototype.snapNode = function(node) {\n for(var i = 0; i < this.nodes.length; i++) {\n if(this.nodes[i] === node){\n continue;\n }\n\n if(Math.abs(node.x - this.nodes[i].x) < this.SNAP_TO_PADDING) {\n node.x = this.nodes[i].x;\n }\n\n if(Math.abs(node.y - this.nodes[i].y) < this.SNAP_TO_PADDING) {\n node.y = this.nodes[i].y;\n }\n }\n };\n\n /**\n * Add a new link (always 'this.currentLink') to the set of links.\n * If the link connects two nodes already linked, the angle of the new link\n * is tweaked so it is distinguishable from the existing links.\n * @param {object} newLink The link to be added.\n */\n Graph.prototype.addLink = function(newLink) {\n var maxPerpRHS = null;\n for (var i = 0; i < this.links.length; i++) {\n var link = this.links[i];\n if (link.nodeA === newLink.nodeA && link.nodeB === newLink.nodeB) {\n if (maxPerpRHS === null || link.perpendicularPart > maxPerpRHS) {\n maxPerpRHS = link.perpendicularPart;\n }\n }\n if (link.nodeA === newLink.nodeB && link.nodeB === newLink.nodeA) {\n if (maxPerpRHS === null || -link.perpendicularPart > maxPerpRHS ) {\n maxPerpRHS = -link.perpendicularPart;\n }\n }\n }\n if (maxPerpRHS !== null) {\n newLink.perpendicularPart = maxPerpRHS + this.DUPLICATE_LINK_OFFSET;\n }\n this.links.push(newLink);\n };\n\n Graph.prototype.reload = function() {\n var content = $(this.textArea).val();\n if (content) {\n try {\n /**\n * Load up the student's previous answer if non-empty.\n */\n var backup = JSON.parse(content), i;\n\n for(i = 0; i < backup.nodes.length; i++) {\n var backupNode = backup.nodes[i];\n var backupNodeLayout = backup.nodeGeometry[i];\n var node = new elements.Node(this, backupNodeLayout[0], backupNodeLayout[1]);\n node.isAcceptState = backupNode[1];\n node.textBox = new elements.TextBox(backupNode[0].toString(), node);\n this.nodes.push(node);\n }\n\n for(i = 0; i < backup.edges.length; i++) {\n var backupLink = backup.edges[i];\n var backupLinkLayout = backup.edgeGeometry[i];\n var link = null;\n if(backupLink[0] === backupLink[1]) {\n /**\n * Self link has two identical nodes.\n */\n link = new elements.SelfLink(this, this.nodes[backupLink[0]]);\n link.anchorAngle = backupLinkLayout.anchorAngle;\n link.textBox = new elements.TextBox(backupLink[2].toString(), link);\n if (backupLink.length > 3) {\n link.textBox.setAnchorPoint(backupLink[3].x, backupLink[3].y);\n }\n } else if(backupLink[0] === -1) {\n link = new elements.StartLink(this, this.nodes[backupLink[1]]);\n link.deltaX = backupLinkLayout.deltaX;\n link.deltaY = backupLinkLayout.deltaY;\n } else {\n link = new elements.Link(this, this.nodes[backupLink[0]], this.nodes[backupLink[1]]);\n link.parallelPart = backupLinkLayout.parallelPart;\n link.perpendicularPart = backupLinkLayout.perpendicularPart;\n link.lineAngleAdjust = backupLinkLayout.lineAngleAdjust;\n link.textBox = new elements.TextBox(backupLink[2].toString(), link);\n if (backupLink.length > 3) {\n link.textBox.setAnchorPoint(backupLink[3].x, backupLink[3].y);\n }\n }\n if(link !== null) {\n this.links.push(link);\n }\n }\n } catch(e) {\n this.fail = true;\n this.failString = 'graph_ui_invalidserialisation';\n }\n }\n };\n\n Graph.prototype.save = function() {\n\n var backup = {\n 'edgeGeometry': [],\n 'nodeGeometry': [],\n 'nodes': [],\n 'edges': [],\n };\n var i;\n\n if(!JSON || (this.textArea.val().trim() === '' && this.nodes.length === 0)) {\n return; // Don't save if we have an empty textbox and no graphic content.\n }\n\n for(i = 0; i < this.nodes.length; i++) {\n var node = this.nodes[i];\n\n var nodeData = [node.textBox.text, node.isAcceptState];\n var nodeLayout = [node.x, node.y];\n\n backup.nodeGeometry.push(nodeLayout);\n backup.nodes.push(nodeData);\n }\n\n for(i = 0; i < this.links.length; i++) {\n var link = this.links[i],\n linkData = null,\n linkLayout = null;\n\n if(link instanceof elements.SelfLink) {\n linkLayout = {\n 'anchorAngle': link.anchorAngle,\n };\n linkData = [this.nodes.indexOf(link.node), this.nodes.indexOf(link.node), link.textBox.text];\n if (link.textBox.dragged) {\n linkData.push(link.textBox.position);\n }\n } else if(link instanceof elements.StartLink) {\n linkLayout = {\n 'deltaX': link.deltaX,\n 'deltaY': link.deltaY\n };\n linkData = [-1, this.nodes.indexOf(link.node), \"\"];\n } else if(link instanceof elements.Link) {\n linkLayout = {\n 'lineAngleAdjust': link.lineAngleAdjust,\n 'parallelPart': link.parallelPart,\n 'perpendicularPart': link.perpendicularPart,\n };\n linkData = [this.nodes.indexOf(link.nodeA), this.nodes.indexOf(link.nodeB), link.textBox.text];\n if (link.textBox.dragged) {\n linkData.push(link.textBox.position);\n }\n }\n if (linkData !== null && linkLayout !== null) {\n backup.edges.push(linkData);\n backup.edgeGeometry.push(linkLayout);\n }\n }\n this.textArea.val(JSON.stringify(backup));\n };\n\n Graph.prototype.saveVersion = function () {\n var curState = this.textArea.val();\n if (this.versions.length == 0 || curState.localeCompare(this.versions[this.versionIndex]) != 0){\n this.versionIndex++;\n while (this.versionIndex < this.versions.length){ //Clear newer versions that have been overwritten by this save\n this.versions.pop();\n }\n this.versions.push(curState);\n if (this.versions.length > this.MAX_VERSIONS){ //Limit the size of this.versions\n this.versions.shift();\n this.versionIndex--;\n }\n }\n };\n\n Graph.prototype.undo = function () {\n this.saveVersion();\n if (this.versionIndex > 0){\n this.versionIndex--;\n this.textArea.val(this.versions[this.versionIndex]);\n /**\n * Clear graph nodes and links\n */\n this.nodes = [];\n this.links = [];\n /**\n * Reload graph from serialisation\n */\n this.reload();\n this.draw();\n }\n };\n\n Graph.prototype.redo = function() {\n if (this.versionIndex < this.versions.length - 1){\n this.versionIndex++;\n this.textArea.val(this.versions[this.versionIndex]);\n /**\n * Clear graph nodes and links\n */\n this.nodes = [];\n this.links = [];\n /**\n * Reload graph from serialisation\n */\n this.reload();\n this.draw();\n }\n };\n\n Graph.prototype.clear = function () {\n this.saveVersion();\n this.nodes = [];\n this.links = [];\n this.save();\n this.draw();\n };\n\n Graph.prototype.destroy = function () {\n clearInterval(this.caretTimer); // Stop the caret timer.\n this.graphCanvas.canvas.off(); // Stop all events.\n this.graphCanvas.canvas.remove();\n };\n\n Graph.prototype.resetCaret = function () {\n var t = this; // For embedded function to access this.\n\n clearInterval(this.caretTimer);\n this.caretTimer = setInterval(function() {\n t.caretVisible = !t.caretVisible;\n t.draw();\n }, 500);\n this.caretVisible = true;\n };\n\n Graph.prototype.draw = function () {\n var canvas = this.getCanvas(),\n c = canvas.getContext('2d'),\n i;\n\n c.clearRect(0, 0, this.getCanvas().width, this.getCanvas().height);\n c.save();\n c.translate(0.5, 0.5);\n\n for (i = 0; i < this.buttons.length; i++){\n this.buttons[i].draw(c);\n }\n\n if (!this.helpBox.helpOpen) { // Only proceed if help info not showing.\n\n for(i = 0; i < this.nodes.length; i++) {\n c.lineWidth = 1;\n c.fillStyle = c.strokeStyle = (this.nodes[i] === this.selectedObject) ? 'blue' : 'black';\n this.nodes[i].draw(c);\n }\n for(i = 0; i < this.links.length; i++) {\n c.lineWidth = 1;\n c.fillStyle = c.strokeStyle = (this.links[i] === this.selectedObject\n || this.links[i].textBox === this.selectedObject) ? 'blue' : 'black';\n this.links[i].draw(c);\n }\n if(this.currentLink !== null) {\n c.lineWidth = 1;\n c.fillStyle = c.strokeStyle = 'black';\n this.currentLink.draw(c);\n }\n }\n\n c.restore();\n this.save();\n };\n\n return {\n Constructor: Graph\n };\n});\n"],"names":["define","$","util","elements","GraphCanvas","parent","canvasId","w","h","HANDLE_SIZE","canvas","document","createElement","attr","id","class","tabindex","css","on","e","mousedown","mouseup","dblclick","keydown","mousemove","keypress","resize","Graph","textareaId","width","height","uiParams","save_this","this","SNAP_TO_PADDING","DUPLICATE_LINK_OFFSET","HIT_TARGET_PADDING","DEFAULT_NODE_RADIUS","DEFAULT_FONT_SIZE","DEFAULT_TEXT_OFFSET","DEFAULT_LINK_LABEL_REL_DIST","MAX_VERSIONS","textArea","getElementById","helpText","readOnly","prop","graphCanvas","caretVisible","caretTimer","originalClick","nodes","links","selectedObject","currentLink","movingObject","fail","failString","versions","versionIndex","helpBox","HelpBox","clearButton","Button","onClick","confirm","clear","buttons","locknodepositions","locknodes","lockedgepositions","lockedges","helpmenutext","require","str","helpPresent","get_string","when","done","graphhelp","reload","draw","prototype","failed","failMessage","getElement","getCanvas","hasFocus","activeElement","nodeRadius","noderadius","fontSize","fontsize","isFsm","undefined","isfsm","textOffset","textoffset","arrowIfReqd","c","x","y","angle","isdirected","drawArrow","sync","syncIntervalSecs","key","crossBrowserKey","metaKey","altKey","ctrlKey","canEditText","justMoved","saveVersion","textBox","insertChar","String","fromCharCode","resetCaret","mouse","crossBrowserRelativeMousePos","selectObject","movingGraph","movingText","helpOpen","shiftKey","Node","lockedgeset","SelfLink","movingNodes","traverseGraph","i","length","setMouseStart","TextBox","lockedgelabels","Link","TemporaryLink","isNode","isLink","locknodelabels","nodeDeleted","deleteChar","locknodeset","splice","node","nodeA","nodeB","caretLeft","caretRight","keyCode","redo","undo","push","isAcceptState","closestPoint","containsPoint","highLighted","targetNode","StartLink","closestPointOnCircle","trackMouse","snapNode","setAnchorPoint","addLink","Math","abs","newLink","maxPerpRHS","link","perpendicularPart","content","val","backup","JSON","parse","backupNode","backupNodeLayout","nodeGeometry","toString","edges","backupLink","backupLinkLayout","edgeGeometry","anchorAngle","deltaX","deltaY","parallelPart","lineAngleAdjust","save","trim","nodeData","text","nodeLayout","linkData","linkLayout","indexOf","dragged","position","stringify","curState","localeCompare","pop","shift","destroy","clearInterval","off","remove","t","setInterval","getContext","clearRect","translate","lineWidth","fillStyle","strokeStyle","restore","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_html.min.js.map b/amd/build/ui_html.min.js.map
new file mode 100644
index 000000000..9d3bb59cf
--- /dev/null
+++ b/amd/build/ui_html.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_html.min.js","sources":["../src/ui_html.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 util.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 * Implementation of the html_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a div\n * containing the author-supplied HTML. The serialisation of that HTML,\n * which is what is essentially copied back into the textarea for submissions\n * as the answer, is a JSON object. The fields of that object are the names\n * of all author-supplied HTML elements with a class 'coderunner-ui-element';\n * all such objects are expected to have a 'name' attribute as well. The\n * associated field values are lists. Each list contains all the values, in\n * document order, of the results of calling the jquery val() method in turn\n * on each of the UI elements with that name.\n * This means that at least input, select and textarea\n * elements are supported. The author is responsible for checking the\n * compatibility of other elements with jquery's val() method.\n *\n * The HTML to use in the answer area must be provided as the contents of\n * either the globalextra field or the prototypeextra field in the question\n * authoring form. The choice of which is set by the html_src UI parameter, which\n * must be either 'globalextra' or 'prototypeextra'.\n *\n * If any fields of the answer html are to be preloaded, these should be specified\n * in the answer preload with json of the form '{\"\": \"\",...}'\n * where fieldValueList is a list of all the values to be assigned to the fields\n * with the given name, in document order.\n *\n * To accommodate the possibility of dynamic HTML, any leftover preload values,\n * that is, values that cannot be positioned within the HTML either because\n * there is no field of the required name or because, in the case of a list,\n * there are insufficient elements, are assigned to the data['leftovers']\n * attribute of the outer html div, as a sub-object of the original object.\n * This outer div can be located as the 'closest' (in a jQuery sense)\n * div.qtype-coderunner-html-outer-div. The author-supplied HTML must include\n * JavaScript to make use of the 'leftovers'.\n *\n * As a special case of the serialisation, if all values in the serialisation\n * are either empty strings or a list of empty strings, the serialisation is\n * itself the empty string.\n *\n * @module coderunner/ui_html\n * @copyright Richard Lobb, 2018, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the HtmlUi object.\n * @param {string} textareaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\n function HtmlUi(textareaId, width, height, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n this.textareaId = textareaId;\n var srcField = uiParams.html_src || 'globalextra';\n this.html = this.textArea.attr('data-' + srcField);\n this.html = this.html.replace(/___textareaId___/gm, textareaId);\n this.readOnly = this.textArea.prop('readonly');\n this.uiParams = uiParams;\n this.fail = false;\n this.htmlDiv = null;\n this.reload();\n }\n\n HtmlUi.prototype.failed = function() {\n return this.fail;\n };\n\n\n HtmlUi.prototype.failMessage = function() {\n return 'htmluiloadfail';\n };\n\n\n // Copy the serialised version of the HTML UI area to the TextArea.\n HtmlUi.prototype.sync = function() {\n var\n serialisation = {},\n name,\n empty = true;\n\n this.getFields().each(function() {\n var value, type;\n type = $(this).attr('type');\n name = $(this).attr('name');\n if ((type === 'checkbox' || type === 'radio') && !($(this).is(':checked'))) {\n value = '';\n } else {\n value = $(this).val();\n }\n if (serialisation.hasOwnProperty(name)) {\n serialisation[name].push(value);\n } else {\n serialisation[name] = [value];\n }\n if (value !== '') {\n empty = false;\n }\n });\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n\n HtmlUi.prototype.getElement = function() {\n return this.htmlDiv;\n };\n\n HtmlUi.prototype.getFields = function() {\n return $(this.htmlDiv).find('.coderunner-ui-element');\n };\n\n // Set the value of the jQuery field to the given value.\n // If the field is a radio button or a checkbox and its name matches\n // the given value, the checked attribute is set. Otherwise the field's\n // val() function is called to set the value.\n HtmlUi.prototype.setField = function(field, value) {\n if (field.attr('type') === 'checkbox' || field.attr('type') === 'radio') {\n field.prop('checked', field.val() === value);\n } else {\n field.val(value);\n }\n };\n\n HtmlUi.prototype.reload = function() {\n var\n content = $(this.textArea).val(), // JSON-encoded HTML element settings.\n valuesToLoad,\n values,\n i,\n fields,\n leftOvers,\n outerDivId = 'qtype-coderunner-outer-div-' + this.textareaId.toString(),\n outerDiv = \"
\");\n this.htmlDiv.data('uiparams', this.uiParams); // For use by scripts embedded in html.\n this.htmlDiv.data('templateparams', this.uiParams); // Legacy support only. DEPRECATED.\n if (content) {\n try {\n valuesToLoad = JSON.parse(content);\n leftOvers = {};\n for (var name in valuesToLoad) {\n values = valuesToLoad[name];\n fields = this.getFields().filter(\"[name='\" + name + \"']\");\n leftOvers[name] = [];\n for (i = 0; i < values.length; i++) {\n if (i < fields.length) {\n this.setField($(fields[i]), values[i]);\n } else {\n leftOvers[name].push(values[i]);\n }\n }\n if (leftOvers[name].length === 0) {\n delete leftOvers[name];\n }\n }\n\n if (!$.isEmptyObject(leftOvers)) {\n this.htmlDiv.data('leftovers', leftOvers);\n }\n\n } catch(e) {\n this.fail = true;\n }\n }\n };\n\n HtmlUi.prototype.resize = function() {}; // Nothing to see here. Move along please.\n\n HtmlUi.prototype.hasFocus = function() {\n var focused = false;\n this.getFields().each(function() {\n if (this === document.activeElement) {\n focused = true;\n }\n });\n return focused;\n };\n\n // Destroy the HTML UI and serialise the result into the original text area.\n HtmlUi.prototype.destroy = function() {\n this.sync();\n $(this.htmlDiv).remove();\n this.htmlDiv = null;\n };\n\n return {\n Constructor: HtmlUi\n };\n});\n"],"names":["define","$","HtmlUi","textareaId","width","height","uiParams","textArea","document","getElementById","srcField","html_src","html","this","attr","replace","readOnly","prop","fail","htmlDiv","reload","prototype","failed","failMessage","sync","name","serialisation","empty","getFields","each","value","type","is","val","hasOwnProperty","push","JSON","stringify","getElement","find","setField","field","valuesToLoad","values","i","fields","leftOvers","content","outerDiv","toString","data","parse","filter","length","isEmptyObject","e","resize","hasFocus","focused","activeElement","destroy","remove","Constructor"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DAA,kCAAO,CAAC,WAAW,SAASC,YAQfC,OAAOC,WAAYC,MAAOC,OAAQC,eAClCC,SAAWN,EAAEO,SAASC,eAAeN,kBACrCA,WAAaA,eACdO,SAAWJ,SAASK,UAAY,mBAC/BC,KAAOC,KAAKN,SAASO,KAAK,QAAUJ,eACpCE,KAAOC,KAAKD,KAAKG,QAAQ,qBAAsBZ,iBAC/Ca,SAAWH,KAAKN,SAASU,KAAK,iBAC9BX,SAAWA,cACXY,MAAO,OACPC,QAAU,UACVC,gBAGTlB,OAAOmB,UAAUC,OAAS,kBACfT,KAAKK,MAIhBhB,OAAOmB,UAAUE,YAAc,iBACpB,kBAKXrB,OAAOmB,UAAUG,KAAO,eAGhBC,KADAC,cAAgB,GAEhBC,OAAQ,OAEPC,YAAYC,MAAK,eACdC,MAAOC,KACXA,KAAO9B,EAAEY,MAAMC,KAAK,QACpBW,KAAOxB,EAAEY,MAAMC,KAAK,QAIhBgB,MAHU,aAATC,MAAgC,UAATA,MAAuB9B,EAAEY,MAAMmB,GAAG,YAGlD/B,EAAEY,MAAMoB,MAFR,GAIRP,cAAcQ,eAAeT,MAC7BC,cAAcD,MAAMU,KAAKL,OAEzBJ,cAAcD,MAAQ,CAACK,OAEb,KAAVA,QACAH,OAAQ,MAGZA,WACKpB,SAAS0B,IAAI,SAEb1B,SAAS0B,IAAIG,KAAKC,UAAUX,iBAKzCxB,OAAOmB,UAAUiB,WAAa,kBACnBzB,KAAKM,SAGhBjB,OAAOmB,UAAUO,UAAY,kBAClB3B,EAAEY,KAAKM,SAASoB,KAAK,2BAOhCrC,OAAOmB,UAAUmB,SAAW,SAASC,MAAOX,OACb,aAAvBW,MAAM3B,KAAK,SAAiD,UAAvB2B,MAAM3B,KAAK,QAChD2B,MAAMxB,KAAK,UAAWwB,MAAMR,QAAUH,OAEtCW,MAAMR,IAAIH,QAIlB5B,OAAOmB,UAAUD,OAAS,eAGlBsB,aACAC,OACAC,EACAC,OACAC,UALAC,QAAU9C,EAAEY,KAAKN,UAAU0B,MAO3Be,SAAW,gFADE,8BAAgCnC,KAAKV,WAAW8C,YAC4C,aAExG9B,QAAUlB,EAAE+C,SAAWnC,KAAKD,KAAO,eACnCO,QAAQ+B,KAAK,WAAYrC,KAAKP,eAC9Ba,QAAQ+B,KAAK,iBAAkBrC,KAAKP,UACrCyC,gBAIS,IAAItB,QAFTiB,aAAeN,KAAKe,MAAMJ,SAC1BD,UAAY,GACKJ,aAAc,KAC3BC,OAASD,aAAajB,MACtBoB,OAAShC,KAAKe,YAAYwB,OAAO,UAAY3B,KAAO,MACpDqB,UAAUrB,MAAQ,GACbmB,EAAI,EAAGA,EAAID,OAAOU,OAAQT,IACvBA,EAAIC,OAAOQ,YACNb,SAASvC,EAAE4C,OAAOD,IAAKD,OAAOC,IAEnCE,UAAUrB,MAAMU,KAAKQ,OAAOC,IAGL,IAA3BE,UAAUrB,MAAM4B,eACTP,UAAUrB,MAIpBxB,EAAEqD,cAAcR,iBACZ3B,QAAQ+B,KAAK,YAAaJ,WAGrC,MAAMS,QACCrC,MAAO,IAKxBhB,OAAOmB,UAAUmC,OAAS,aAE1BtD,OAAOmB,UAAUoC,SAAW,eACnBC,SAAU,cACV9B,YAAYC,MAAK,WACdhB,OAASL,SAASmD,gBAClBD,SAAU,MAGXA,SAIXxD,OAAOmB,UAAUuC,QAAU,gBAClBpC,OACLvB,EAAEY,KAAKM,SAAS0C,cACX1C,QAAU,MAGZ,CACH2C,YAAa5D"}
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js b/amd/build/ui_scratchpad.min.js
index ce4cbf97a..1efe992e7 100644
--- a/amd/build/ui_scratchpad.min.js
+++ b/amd/build/ui_scratchpad.min.js
@@ -1,4 +1,4 @@
-define("qtype_coderunner/ui_scratchpad",["exports","core/ajax","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_ajax,_templates,_userinterfacewrapper,_outputdisplayarea){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_templates,_userinterfacewrapper,_outputdisplayarea){var obj;
/**
* Implementation of the scratchpad_ui user interface plugin. For overall details
* of the UI plugin architecture, see userinterfacewrapper.js.
@@ -6,7 +6,7 @@ define("qtype_coderunner/ui_scratchpad",["exports","core/ajax","core/templates",
* This plugin replaces the usual textarea answer element with a UI is designed to
* allow the execution of code in the CodeRunner question in a manner similar to an IDE.
* It contains two editor boxes, one on top of another, allowing users to enter and
- * edit code in both.
+ * edit code in both. It contains two embedded Ace UIs.
* By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,
* clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,
* a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that
@@ -27,7 +27,6 @@ define("qtype_coderunner/ui_scratchpad",["exports","core/ajax","core/templates",
* test_code: [""] A list containing a string with containing answer code from the second editor;
* show_hide: ["1"] when scratchpad is visible, otherwise [""];
* prefix_ans: ["1"] when Prefix with Answer is checked, otherwise [""].
- * . The fields of that object are the names
*
* UI Parameters:
* - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.
@@ -39,16 +38,20 @@ define("qtype_coderunner/ui_scratchpad",["exports","core/ajax","core/templates",
* - wrapper_src: location of wrapper code to be used by the run button, if applicable:
* setting to globalextra will use text in global extra field,
* - prototypeextra will use the prototype extra field.
- * - html_output: when true, the output from run will be displayed as raw HTML instead of text.
- * - disable_scratchpad: disable the scratchpad, effectively returning back to Ace UI
+ * - output_display_mode: control how program output is displayed on runs, there are three modes:
+ * - text: display program output as text, html escaped;
+ * - json: display program output, when it is json,
+ * - html: display program output as raw html.
+ * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...
+ * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI
* from student perspective.
* - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.
* This can be used to swap the default state.
*
- * @module coderunner/ui_scratchpad
+ * @module qtype_coderunner/ui_scratchpad
* @copyright Richard Lobb, 2022, The University of Canterbury
* @copyright James Napier, 2022, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Constructor=void 0,_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates);const invertSerial=current=>"1"==current[0]?[""]:["1"],overwriteValues=(defaults,prescribed)=>{let overwritten={...defaults};if(prescribed)for(const[key,value]of Object.entries(defaults))overwritten[key]=prescribed[key]||value;return overwritten};_exports.Constructor=class{constructor(textAreaId,width,height,uiParams){const DEF_UI_PARAMS={scratchpad_name:"",button_name:"",prefix_name:"",help_text:"",run_lang:uiParams.lang,html_output:!1,disable_scratchpad:!1,wrapper_src:null};this.textArea=document.getElementById(textAreaId),this.textAreaId=textAreaId,this.height=height,this.readOnly=this.textArea.readonly,this.fail=!1,this.outerDiv=null,this.outputDisplay=null,this.invertPreload=uiParams.invert_prefix,this.lang=uiParams.lang,this.numRows=this.textArea.rows,this.uiParams=overwriteValues(DEF_UI_PARAMS,uiParams),this.runWrapper=this.getRunWrapper(),this.reload()}getRunWrapper(){const wrapperSrc=this.uiParams.wrapper_src;let runWrapper=null;return wrapperSrc&&("globalextra"===wrapperSrc||"prototypeextra"===wrapperSrc?runWrapper=this.textArea.dataset[wrapperSrc]:(this.fail=!0,this.failString="scratchpad_ui_badrunwrappersrc")),runWrapper}failed(){return this.fail}failMessage(){return this.failString}sync(){if(!this.context)return;const serialisation=this.getSerialisation();this.setSerialisation(serialisation)}getSerialisation(){const prefixAns=document.getElementById(this.context.prefix_ans.id),showHide=document.getElementById(this.context.show_hide.id);let serialisation={answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:[""]};return this.answerTextarea&&(serialisation.answer_code=[this.answerTextarea.value]),this.testTextarea&&(serialisation.test_code=[this.testTextarea.value]),showHide&&!(el=>{if(!el.classList.contains("collapse"))throw Error("Element does not have collapse class");return!el.classList.contains("show")})(showHide)&&(serialisation.show_hide=["1"]),(null!=prefixAns&&prefixAns.checked||this.context.disable_scratchpad)&&(serialisation.prefix_ans=["1"]),this.invertPreload&&(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans)),serialisation}setSerialisation(serialisation){serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),Object.values(serialisation).some((val=>1===val.length&&val[0].length>0))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}getElement(){return this.outerDiv}handleRunButtonClick(){this.sync();const preloadString=this.textArea.value,serial=this.readJson(preloadString),sandboxParams=this.uiParams.params,language=this.lang,code=(answerCode=serial.answer_code,testCode=serial.test_code,prefixAns=serial.prefix_ans[0],(template=this.runWrapper)||(template="{{ ANSWER_CODE }}\n{{ SCRATCHPAD_CODE }}"),prefixAns||(answerCode=""),(template=template.replaceAll("{{ ANSWER_CODE }}",answerCode)).replaceAll("{{ SCRATCHPAD_CODE }}",testCode));var answerCode,testCode,prefixAns,template;this.outputDisplay.handleRunButtonClick(code,language,sandboxParams)}updateContext(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}readJson(preloadString){let serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch{serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}async reload(){const preloadString=this.textArea.value;let preload;try{preload=this.readJson(preloadString)}catch(error){return this.fail=!0,void(this.failString="scratchpad_ui_invalidserialisation")}this.updateContext(preload);try{const{html:html}=await _templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context),outputMode=this.uiParams.html_output?"html":"text";this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,outputMode),this.addEventListeners()}catch(e){return this.fail=!0,void(this.failString="scratchpad_ui_templateloadfail")}}drawUi(html){const wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}addAceUis(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}addEventListeners(){const runButton=document.getElementById(this.textAreaId+"_run-btn"),outputDisplayarea=document.getElementById(this.context.output_display.id);runButton&&runButton.addEventListener("click",(()=>this.handleRunButtonClick(_ajax.default,outputDisplayarea)))}resize(){}hasFocus(){var _this$answerCodeUi,_this$testCodeUi;let focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}destroy(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Constructor=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};const invertSerial=current=>"1"===current[0]?[""]:["1"],overwriteValues=(defaults,prescribed)=>{let overwritten={...defaults};if(prescribed)for(const[key,value]of Object.entries(defaults))overwritten[key]=prescribed[key]||value;return overwritten};_exports.Constructor=class{constructor(textAreaId,width,height,uiParams){const DEF_UI_PARAMS={scratchpad_name:"",button_name:"",prefix_name:"",help_text:"",params:{},run_lang:uiParams.lang,output_display_mode:"text",disable_scratchpad:!1,wrapper_src:null};this.textArea=document.getElementById(textAreaId),this.textAreaId=textAreaId,this.height=height,this.readOnly=this.textArea.readonly,this.fail=!1,this.outerDiv=null,this.outputDisplay=null,this.invertPreload=uiParams.invert_prefix,this.lang=uiParams.lang,this.numRows=this.textArea.rows,this.uiParams=overwriteValues(DEF_UI_PARAMS,uiParams),this.runWrapper=this.getRunWrapper(),this.reload()}getRunWrapper(){const wrapperSrc=this.uiParams.wrapper_src;let runWrapper=null;return wrapperSrc&&("globalextra"===wrapperSrc||"prototypeextra"===wrapperSrc?runWrapper=this.textArea.dataset[wrapperSrc]:(this.fail=!0,this.failString="scratchpad_ui_badrunwrappersrc")),runWrapper}failed(){return this.fail}failMessage(){return this.failString}sync(){if(!this.context)return;const serialisation=this.getSerialisation();this.setSerialisation(serialisation)}getSerialisation(){const prefixAns=document.getElementById(this.context.prefix_ans.id),showHide=document.getElementById(this.context.show_hide.id);let serialisation={answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:[""]};return this.answerTextarea&&(serialisation.answer_code=[this.answerTextarea.value]),this.testTextarea&&(serialisation.test_code=[this.testTextarea.value]),showHide&&!(el=>{if(!el.classList.contains("collapse"))throw Error("Element does not have collapse class");return!el.classList.contains("show")})(showHide)&&(serialisation.show_hide=["1"]),(null!=prefixAns&&prefixAns.checked||this.context.disable_scratchpad)&&(serialisation.prefix_ans=["1"]),this.invertPreload&&(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans)),serialisation}setSerialisation(serialisation){serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),Object.values(serialisation).some((val=>1===val.length&&val[0].length>0))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}getElement(){return this.outerDiv}handleRunButtonClick(){if(null===this.outputDisplay)return;this.sync();const preloadString=this.textArea.value,serial=this.readJson(preloadString),code=(answerCode=serial.answer_code,testCode=serial.test_code,prefixAns=serial.prefix_ans[0],(template=this.runWrapper)||(template="{{ ANSWER_CODE }}\n{{ SCRATCHPAD_CODE }}"),prefixAns||(answerCode=""),(template=template.replaceAll("{{ ANSWER_CODE }}",answerCode)).replaceAll("{{ SCRATCHPAD_CODE }}",testCode));var answerCode,testCode,prefixAns,template;this.outputDisplay.runCode(code,"",!0)}updateContext(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}readJson(preloadString){let serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch{serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}async reload(){const preloadString=this.textArea.value;let preload;try{preload=this.readJson(preloadString)}catch(error){return this.fail=!0,void(this.failString="scratchpad_ui_invalidserialisation")}this.updateContext(preload);try{const{html:html}=await _templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners()}catch(e){this.fail=!0,this.failString="scratchpad_ui_templateloadfail"}}drawUi(html){const wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}addAceUis(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}addEventListeners(){const runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(()=>this.handleRunButtonClick()))}resize(){}hasFocus(){var _this$answerCodeUi,_this$testCodeUi;let focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}destroy(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}}));
//# sourceMappingURL=ui_scratchpad.min.js.map
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js.map b/amd/build/ui_scratchpad.min.js.map
new file mode 100644
index 000000000..9b516018b
--- /dev/null
+++ b/amd/build/ui_scratchpad.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text.\n * @param {string} testCode text.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template) => {\n if (!template) {\n template = '{{ ANSWER_CODE }}\\n' +\n '{{ SCRATCHPAD_CODE }}';\n }\n if (!prefixAns) {\n answerCode = '';\n }\n template = template.replaceAll('{{ ANSWER_CODE }}', answerCode);\n template = template.replaceAll('{{ SCRATCHPAD_CODE }}', testCode);\n return template;\n};\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the defualt value.\n * Does not add keys/values to the result if that key is not in defualts.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!el.classList.contains('collapse')) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n let serialisation = {\n answer_code: [''],\n test_code: [''],\n show_hide: [''],\n prefix_ans: ['']\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const code = fillWrapper(\n serial.answer_code,\n serial.test_code,\n serial.prefix_ans[0],\n this.runWrapper\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","overwriteValues","defaults","prescribed","overwritten","key","value","Object","entries","constructor","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","reload","wrapperSrc","dataset","failString","failed","failMessage","sync","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","test_code","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","checked","values","some","val","length","JSON","stringify","getElement","handleRunButtonClick","preloadString","serial","readJson","code","answerCode","testCode","template","replaceAll","runCode","updateContext","preload","text","render","CSS","escape","parse","hasOwnProperty","TypeError","error","html","Templates","renderForPromise","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","e","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","hasFocus","focused","_this$answerCodeUi","uiInstance","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_table.min.js.map b/amd/build/ui_table.min.js.map
new file mode 100644
index 000000000..0c2069ba6
--- /dev/null
+++ b/amd/build/ui_table.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_table.min.js","sources":["../src/ui_table.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 util.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 * Implementation of the table_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a div\n * containing an HTML table. The number of columns, and\n * the initial number of rows are specified by required UI parameters\n * num_columns and num_rows respectively.\n * Optional additional UI parameters are:\n * 1. column_headers: a list of strings that can be used to provide a\n * fixed header row at the top.\n * 2. row_labels: a list of strings that can be used to provide a\n * fixed row label column at the left.\n * 3. dynamic_rows, which, if true, allows the user to add rows.\n * 4. locked_cells: a list of [row, column] pairs, being the coordinates\n * of table cells that cannot be changed by the user. row and column numbers\n * are zero origin and do not include the header row or the row labels.\n * 5. width_percents: a list of the percentages of the width occupied\n * by each column. This list must include a value for the row labels, if present.\n *\n * The serialisation of the table, which is what is essentially copied back\n * into the textarea for submissions as the answer, is a JSON array. Each\n * element in the array is itself an array containing the values of one row\n * of the table. Empty cells are empty strings. The table header row and row\n * label columns are not provided in the serialisation.\n *\n * To preload the table with data, simply set the answer_preload of the question\n * to a json array of row values (each itself an array). If the number of rows\n * in the preload exceeds the number set by num_rows, extra rows are\n * added. If the number is less than num_rows, or if there is no\n * answer preload, undefined rows are simply left blank.\n *\n * As a special case of the serialisation, if all cells in the serialisation\n * are empty strings, the serialisation is itself the empty string.\n *\n * @module qtype_coderunner/ui_table\n * @copyright Richard Lobb, 2018, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the TableUI object.\n * @param {string} textareaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\n function TableUi(textareaId, width, height, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n this.readOnly = this.textArea.prop('readonly');\n this.tableDiv = null;\n this.uiParams = uiParams;\n if (!uiParams.num_columns ||\n !uiParams.num_rows) {\n this.fail = true;\n this.failString = 'table_ui_missingparams';\n return; // We're dead, fred.\n }\n\n this.fail = false;\n this.lockedCells = uiParams.locked_cells || [];\n this.hasHeader = uiParams.column_headers && uiParams.column_headers.length > 0 ? true : false;\n this.hasRowLabels = uiParams.row_labels && uiParams.row_labels.length > 0 ? true : false;\n this.numDataColumns = uiParams.num_columns;\n this.rowsPerCell = uiParams.lines_per_cell || 2;\n this.totNumColumns = this.numDataColumns + (this.hasRowLabels ? 1 : 0);\n this.columnWidths = this.computeColumnWidths();\n this.reload();\n }\n\n // Return an array of the percentage widths required for each of the\n // totNumColumns columns.\n TableUi.prototype.computeColumnWidths = function() {\n var defaultWidth = Math.trunc(100 / this.totNumColumns),\n columnWidths = [];\n if (this.uiParams.column_width_percents && this.uiParams.column_width_percents.length > 0) {\n return this.uiParams.column_width_percents;\n } else if (Array.prototype.fill) { // Anything except bloody IE.\n return new Array(this.totNumColumns).fill(defaultWidth);\n } else { // IE. What else?\n for (var i = 0; i < this.totNumColumns; i++) {\n columnWidths.push(defaultWidth);\n }\n return columnWidths;\n }\n };\n\n // Return True if the cell at the given row and column is locked.\n // The given row and column numbers exclude column headers and row labels.\n TableUi.prototype.isLockedCell = function(row, col) {\n for (var i = 0; i < this.lockedCells.length; i++) {\n if (this.lockedCells[i][0] == row && this.lockedCells[i][1] == col) {\n return true;\n }\n }\n return false;\n };\n\n TableUi.prototype.getElement = function() {\n return this.tableDiv;\n };\n\n TableUi.prototype.failed = function() {\n return this.fail;\n };\n\n TableUi.prototype.failMessage = function() {\n return this.failString;\n };\n\n // Copy the serialised version of the Table UI area to the TextArea.\n TableUi.prototype.sync = function() {\n var\n serialisation = [],\n empty = true,\n tableRows = $(this.tableDiv).find('table tbody tr');\n\n tableRows.each(function () {\n var rowValues = [];\n $(this).find('textarea').each(function () {\n var cellVal = $(this).val();\n rowValues.push(cellVal);\n if (cellVal) {\n empty = false;\n }\n });\n serialisation.push(rowValues);\n });\n\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Return the HTML for row number iRow.\n TableUi.prototype.tableRow = function(iRow, preload) {\n var html = '
', widthIndex = 0, width;\n\n // Insert the row label if required.\n if (this.hasRowLabels) {\n width = this.columnWidths[0];\n widthIndex = 1;\n html += \"
\";\n if (iRow < this.uiParams.row_labels.length) {\n html += this.uiParams.row_labels[iRow];\n }\n html += \"
\";\n }\n\n for (var iCol = 0; iCol < this.numDataColumns; iCol++) {\n width = this.columnWidths[widthIndex++];\n html += \"
\";\n html += '';\n html += \"
\";\n }\n html += '
';\n return html;\n };\n\n // Return the HTML for the table's head section.\n TableUi.prototype.tableHeadSection = function() {\n var html = \"\\n\",\n colIndex = 0; // Column index including row label if present.\n\n if (this.hasHeader) {\n html += \"
\";\n if (iCol < this.uiParams.column_headers.length) {\n html += this.uiParams.column_headers[iCol];\n }\n colIndex++;\n html += \"
\";\n }\n html += \"
\\n\";\n }\n html += \"\\n\";\n return html;\n };\n\n // Build the HTML table, filling it with the data from the serialisation\n // currently in the textarea (if there is any).\n TableUi.prototype.reload = function() {\n var\n preloadJson = $(this.textArea).val(), // JSON-encoded table values.\n preload = [],\n divHtml = \"
\\n\" +\n \"
\\n\";\n\n if (preloadJson) {\n try {\n preload = JSON.parse(preloadJson);\n } catch(error) {\n this.fail = true;\n this.failString = 'table_ui_invalidjson';\n return;\n }\n }\n\n try {\n // Build the table head section.\n divHtml += this.tableHeadSection();\n\n // Build the table body. Each table cell has a textarea inside it,\n // except for row labels (if present).\n divHtml += \"\\n\";\n var num_rows_required = Math.max(this.uiParams.num_rows, preload.length);\n for (var iRow = 0; iRow < num_rows_required; iRow++) {\n divHtml += this.tableRow(iRow, preload);\n }\n\n divHtml += '\\n
\\n
';\n this.tableDiv = $(divHtml);\n if (this.uiParams.dynamic_rows) {\n this.addButtons();\n }\n } catch (error) {\n this.fail = true;\n this.failString = 'table_ui_invalidserialisation';\n }\n };\n\n // Add 'Add row' and 'Delete row' buttons at the end of the table.\n TableUi.prototype.addButtons = function() {\n var deleteButtonHtml = '',\n deleteButton = $(deleteButtonHtml),\n t = this;\n this.tableDiv.append(deleteButton);\n deleteButton.click(function() {\n var numRows = t.tableDiv.find('table tbody tr').length,\n lastRow = t.tableDiv.find('tr:last');\n if (numRows > t.uiParams.num_rows) {\n lastRow.remove();\n }\n lastRow = t.tableDiv.find('tr:last'); // New last row.\n if (numRows == t.uiParams.num_rows + 1) {\n $(this).prop('disabled', true);\n }\n });\n\n var addButtonHtml = '',\n addButton = $(addButtonHtml);\n t.tableDiv.append(addButton);\n addButton.click(function() {\n var lastRow, newRow;\n lastRow = t.tableDiv.find('table tbody tr:last');\n newRow = lastRow.clone(); // Copy the last row of the table.\n newRow.find('textarea').each(function() { // Clear all td elements in it.\n $(this).val('');\n });\n lastRow.after(newRow);\n $(this).prev().prop('disabled', false);\n });\n };\n\n TableUi.prototype.resize = function() {}; // Nothing to see here. Move along please.\n\n TableUi.prototype.hasFocus = function() {\n var focused = false;\n $(this.tableDiv).find('textarea').each(function() {\n if (this === document.activeElement) {\n focused = true;\n }\n });\n return focused;\n };\n\n // Destroy the HTML UI and serialise the result into the original text area.\n TableUi.prototype.destroy = function() {\n this.sync();\n $(this.tableDiv).remove();\n this.tableDiv = null;\n };\n\n return {\n Constructor: TableUi\n };\n});\n"],"names":["define","$","TableUi","textareaId","width","height","uiParams","textArea","document","getElementById","readOnly","this","prop","tableDiv","num_columns","num_rows","fail","failString","lockedCells","locked_cells","hasHeader","column_headers","length","hasRowLabels","row_labels","numDataColumns","rowsPerCell","lines_per_cell","totNumColumns","columnWidths","computeColumnWidths","reload","prototype","defaultWidth","Math","trunc","column_width_percents","Array","fill","i","push","isLockedCell","row","col","getElement","failed","failMessage","sync","serialisation","empty","find","each","rowValues","cellVal","val","JSON","stringify","tableRow","iRow","preload","html","widthIndex","iCol","tableHeadSection","colIndex","preloadJson","divHtml","parse","error","num_rows_required","max","dynamic_rows","addButtons","deleteButton","t","append","click","numRows","lastRow","remove","addButton","newRow","clone","after","prev","resize","hasFocus","focused","activeElement","destroy","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/userinterfacewrapper.min.js.map b/amd/build/userinterfacewrapper.min.js.map
new file mode 100644
index 000000000..f46f952fc
--- /dev/null
+++ b/amd/build/userinterfacewrapper.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"userinterfacewrapper.min.js","sources":["../src/userinterfacewrapper.js"],"sourcesContent":["/******************************************************************************\n *\n * This module provides a wrapper for user-interface modules, handling hiding\n * of the textArea that is being replaced by the UI element,\n * resizing of the UI component, and support of such usability functions as\n * ctrl-alt-M to disable/re-enable the entire user interface, including the\n * wrapper.\n *\n * @module coderunner/userinterfacewrapper\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * The InterfaceWrapper class is constructed either by Moodle PHP calls of\n * the form\n *\n * $PAGE->requires->js_call_amd($modulename, $functionname, $params)\n *\n * (e.g. from within render.php) or by JavaScript require calls, e.g. from\n * authorform.js when the question author changes UI type.\n *\n * The InterfaceWrapper provides:\n *\n * 1. A constructor InterfaceWrapper(uiname, textareaId) which\n * hides the given text area, replaces it with a wrapper div (resizable in\n * height by the user but with width resizing managed by changes in window\n * width), created an instance of nameInstance as defined in the file\n * ui_name.js (see below).\n * params is a record containing the decoded value of\n *\n * 2. A stop() method that destroys the embedded UI and hides the wrapper.\n *\n * 3. A restart() method that shows the wrapper again and re-creates the prior\n * embedded UI component within it.\n *\n * 4. A loadUi(uiname, params) method that kills any currently running UI element\n * (if there is one) and (re)loads the specified one. The params parameter\n * is a record that allows additional parameters to be passed in, such as\n * those from the question's uiParams field and, in the case of the\n * Ace UI, the 'lang' (language) that the editor is editing. This data\n * is supplied by the PHP via the data-params attribute of the answer's\n * base textarea.\n *\n * 5. Regular checking for any resizing of the wrapper, which are passed on to\n * the embedded UI element's resize() method.\n *\n * 6. Monitoring of alt-ctrl-M key presses which toggle the visibility of the\n * wrapper plus UI element and the syncronised textArea by calls to stop()\n * and restart\n *\n * =========================================================================\n *\n * The embedded user-interface module must be defined in a JavaScript file\n * of the form ui_name.js which must define a class nameInstance with\n * the following functionality:\n *\n * 1. A constructor SomeUiName(textareaId, width, height, params) that\n * builds an HTML component of the given width and height. textareaId is the\n * ID of the textArea from which the UI element should obtain its initial\n * serialisation and to which it should write the serialisation when its save\n * or destroy methods are called. params is a JavaScript object,\n * decoded from the JSON uiParams defined by the question plus any\n * additional data required, such as the 'lang' in the case of Ace.\n *\n * 2. A getElement() method that returns the HTML element that the\n * InterfaceWrapper is to insert into the document tree.\n *\n * 3. A method failed() that should return true unless the constructor\n * failed (e.g. because it was not able to de-serialise the text area's\n * contents). The wrapper will call destroy() on the object if failed()\n * returns true and abort the use of the UI element. The text area will\n * have the uiloadfailed class added, which CSS will display in some\n * error mode (e.g. a red border).\n *\n * 4. A method failMessage() that will be called only when failed() returns\n * True. It should be a defined CodeRunner language string key.\n *\n * 5. A sync() method that copies the serialised represention of the UI plugin's\n * data to the related TextArea. This is used when submit is clicked.\n *\n * 6. A destroy() method that should sync the contents to the text area then\n * destroy any HTML elements or other created content. This method is called\n * when CTRL-ALT-M is typed by the user to turn off all UI plugins\n *\n * 7. A resize(width, height) method that should resize the entire UI element\n * to the given dimensions.\n *\n * 8. A hasFocus() method that returns true if the UI element has focus.\n *\n * 9. A syncIntervalSecs() method that returns the time interval between\n * calls to the sync() method. 0 for no sync calls. The userinterfacewrapper\n * provides all instances with a generic (base-class) version that returns\n * the value of a UI parameter sync_interval_secs if given else uses the\n * UI interface wrapper default (currently 10).\n *\n * The return value from the module define is a record with a single field\n * 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)\n *\n *****************************************************************************/\n\n/**\n * 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 util.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\ndefine(['jquery'], function($) {\n /**\n * Constructor for a new user interface.\n * @param {string} uiname The name of the interface element (e.g. ace, graph, etc)\n * which should be in file ui_ace.js, ui_graph.js etc.\n * @param {string} textareaId The id of the text area that the UI is to manage.\n * The text area should have an attribute data-params, which is a\n * JSON encoded record containing whatever additional parameters might\n * be needed by the User interface. As a minimum it should contain all\n * the parameters from the uiparameters field of\n * the question so that question authors can pass in additional data\n * such as whether graph edges are bidirectional or not in the case of\n * the graph UI. Additionally the Ace editor requires a 'lang' field\n * to specify what language the editor is editing.\n * When the wrapper has been set up on a text area, the text area's\n * data attribute contains an entry for 'current-ui-wrapper' that is\n * a reference to the wrapper ('this').\n */\n function InterfaceWrapper(uiname, textareaId) {\n let t = this; // For use by embedded functions.\n\n this.GUTTER = 14; // Size of gutter at base of wrapper Node (pixels)\n this.DEFAULT_SYNC_INTERVAL_SECS = 5;\n\n const PIXELS_PER_ROW = 19; // For estimating height of textareas.\n const MAX_GROWN_ROWS = 50; // Upper limit to artifically grown textarea rows.\n const MIN_WRAPPER_HEIGHT = 50;\n\n this.taId = textareaId;\n this.loadFailId = textareaId + '_loadfailerr';\n const ta = document.getElementById(textareaId);\n this.textArea = $(ta);\n const params = this.textArea.attr('data-params');\n if (params) {\n this.uiParams = JSON.parse(params);\n } else {\n this.uiParams = {};\n }\n this.uiParams.lang = this.textArea.attr('data-lang');\n this.readOnly = this.textArea.prop('readonly');\n this.isLoading = false; // True if we're busy loading a UI element.\n this.loadFailed = false; // True if UI failed to initialise properly.\n this.retries = 0; // Number of failed attempts to load a UI component.\n\n let h = parseInt(this.textArea.css(\"height\"));\n let content_lines = this.textArea.val().split('\\n').length;\n let rows = ta.rows;\n if (content_lines > rows) {\n // Allow reloaded text areas with lots of text to grow bigger, within limits.\n rows = Math.min(content_lines, MAX_GROWN_ROWS);\n }\n h = Math.max(h, rows * PIXELS_PER_ROW, MIN_WRAPPER_HEIGHT);\n\n /**\n * Construct an empty hidden wrapper div, inserted directly after the\n * textArea, ready to contain the actual UI.\n */\n this.wrapperNode = $(\"\");\n this.textArea.after(this.wrapperNode);\n this.wrapperNode.hide();\n this.wrapperNode.css({\n resize: 'vertical',\n overflow: 'hidden',\n minHeight: h,\n width: \"100%\",\n border: \"1px solid darkgrey\"\n });\n\n /**\n * Record a reference to this wrapper in the text area's data attribute\n * for use by external javascript that needs to interact with the\n * wrapper, e.g. the multilanguage.js module.\n */\n this.textArea.data('current-ui-wrapper', this);\n\n /**\n * Load the UI into the wrapper (aysnchronous).\n */\n this.uiInstance = null; // Defined by loadUi asynchronously\n this.loadUi(uiname, this.uiParams); // Load the required UI element\n\n /**\n * Add event handlers\n */\n $(document).mousemove(function() {\n t.checkForResize();\n });\n $(window).resize(function() {\n t.checkForResize();\n });\n this.textArea.closest('form').submit(function() {\n if (t.uiInstance !== null) {\n t.uiInstance.sync();\n }\n });\n $(document.body).on('keydown', function(e) {\n const KEY_M = 77;\n if (e.keyCode === KEY_M && e.ctrlKey && e.altKey) {\n if (t.uiInstance !== null || t.loadFailed) {\n t.stop();\n } else {\n t.restart(); // Reactivate\n }\n }\n });\n }\n\n /**\n * Load the specified UI element (which in the case of Ace will need\n * to know the language, lang, as well - this must be supplied as\n * a 'lang' attribute of the record params.\n * When ui is up and running, this.uiInstance will reference it.\n * To avoid a potential race problem, if this method is already busy\n * with a load, try again in 200 msecs.\n * @param {string} uiname The name of the User Interface to be used.\n * @param {object} params The UI parameters object that passes parameters\n * to the actual UI object.\n */\n InterfaceWrapper.prototype.loadUi = function(uiname, params) {\n const t = this,\n errPart1 = 'Failed to load ',\n errPart2 = ' UI component. If this error persists, please report it to the forum on coderunner.org.nz';\n\n /**\n * Get the given language string and plug it into the given jQuery\n * div element as its html, plus a 'fallback' message on a separate line.\n * @param {string} langString The language string specifier for the error message,\n * to be loaded by AJAX.\n * @param {object} errorDiv The div object into which the error message\n * is to be inserted.\n */\n function setLoadFailMessage(langString, errorDiv) {\n require(['core/str'], function(str) {\n /**\n * Get langString text via AJAX\n */\n const\n s = str.get_string(langString, 'qtype_coderunner'),\n fallback = str.get_string('ui_fallback', 'qtype_coderunner');\n $.when(s, fallback).done(function(s, fallback) {\n errorDiv.html(s + ' ' + fallback);\n });\n });\n }\n\n /**\n * The default method for a UIs sync_interval_secs method.\n * Returns the sync_interval_secs parameter if given, else\n * DEFAULT_SYNC_INTERVAL_SECS.\n */\n function syncIntervalSecsBase() {\n if (params.hasOwnProperty('sync_interval_secs')) {\n return parseInt(params.sync_interval_secs);\n } else {\n return t.DEFAULT_SYNC_INTERVAL_SECS;\n }\n }\n\n if (this.isLoading) { // Oops, we're loading a UI element already\n this.retries += 1;\n if (this.retries > 20) {\n alert(errPart1 + uiname + errPart2);\n this.retries = 0;\n this.loading = 0;\n } else {\n setTimeout(function() {\n t.loadUi(uiname, params);\n }, 200); // Try again in 200 msecs\n }\n return;\n }\n this.retries = 0;\n this.params = params; // Save in case need to restart\n\n this.stop(); // Kill any active UI first\n this.uiname = uiname;\n\n if (this.uiname === '' || this.uiname === 'none' || sessionStorage.getItem('disableUis')) {\n this.uiInstance = null;\n } else {\n this.isLoading = true;\n require(['qtype_coderunner/ui_' + this.uiname],\n function(ui) {\n const h = t.wrapperNode.innerHeight() - t.GUTTER;\n const w = t.wrapperNode.innerWidth();\n const uiInstance = new ui.Constructor(t.taId, w, h, params);\n if (uiInstance.failed()) {\n /*\n * Constructor failed to load serialisation.\n * Set uiloadfailed class on text area.\n */\n t.loadFailed = true;\n t.wrapperNode.hide();\n uiInstance.destroy();\n t.uiInstance = null;\n t.textArea.addClass('uiloadfailed');\n const loadFailDiv = '';\n let jqLoadFailDiv = $(loadFailDiv);\n jqLoadFailDiv.insertBefore(t.textArea);\n setLoadFailMessage(uiInstance.failMessage(), jqLoadFailDiv); // Insert error by AJAX\n } else {\n t.hLast = 0; // Force resize (and hence redraw)\n t.wLast = 0; // ... on first call to checkForResize\n t.textArea.hide();\n t.wrapperNode.show();\n t.wrapperNode.append(uiInstance.getElement());\n t.uiInstance = uiInstance;\n t.loadFailed = false;\n t.checkForResize();\n\n /*\n * Set a default syncIntervalSecs method if uiInstance lacks one.\n */\n let uiInstancePrototype = Object.getPrototypeOf(uiInstance);\n uiInstancePrototype.syncIntervalSecs = uiInstancePrototype.syncIntervalSecs || syncIntervalSecsBase;\n t.startSyncTimer(uiInstance);\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Start a sync timer on the given uiInstance, unless its time interval is 0.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.startSyncTimer = function(uiInstance) {\n const timeout = uiInstance.syncIntervalSecs();\n if (timeout) {\n this.uiInstance.timer = setInterval(function () {\n uiInstance.sync();\n }, timeout * 1000);\n } else {\n this.uiInstance.timer = null;\n }\n };\n\n\n /**\n * Stop the sync timer on the given uiInstance, if running.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.stopSyncTimer = function(uiInstance) {\n if (uiInstance.timer) {\n clearTimeout(uiInstance.timer);\n }\n };\n\n\n InterfaceWrapper.prototype.stop = function() {\n /*\n * Disable (shutdown) the embedded ui component.\n * The wrapper remains active for ctrl-alt-M events, but is hidden.\n */\n if (this.uiInstance !== null) {\n this.stopSyncTimer(this.uiInstance);\n this.textArea.show();\n if (this.uiInstance.hasFocus()) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n this.uiInstance.destroy();\n this.uiInstance = null;\n this.wrapperNode.hide();\n }\n this.loadFailed = false;\n this.textArea.removeClass('uiloadfailed'); // Just in case it failed before\n $(document.getElementById(this.loadFailId)).remove();\n };\n\n /*\n * Re-enable the ui element (e.g. after alt-cntrl-M). This is\n * a full re-initialisation of the ui element.\n */\n InterfaceWrapper.prototype.restart = function() {\n if (this.uiInstance === null) {\n /**\n * Restart the UI component in the textarea\n */\n this.loadUi(this.uiname, this.params);\n }\n };\n\n\n /**\n * Check for wrapper resize - propagate to ui element.\n */\n InterfaceWrapper.prototype.checkForResize = function() {\n const SIZE_HACK = 25; // Horrible but best I can do. TODO: FIXME\n\n if (this.uiInstance) {\n const h = this.wrapperNode.innerHeight();\n const w = this.wrapperNode.innerWidth();\n if (h != this.hLast || w != this.wLast) {\n const xLeft = this.wrapperNode.offset().left;\n const maxWidth = $(window).innerWidth() - xLeft - SIZE_HACK;\n const hAdjusted = h - this.GUTTER;\n const wAdjusted = Math.min(maxWidth, w);\n this.uiInstance.resize(wAdjusted, hAdjusted);\n this.hLast = this.wrapperNode.innerHeight();\n this.wLast = this.wrapperNode.innerWidth();\n }\n }\n };\n\n /**\n * The external entry point from the PHP.\n * @param {string} uiname The name of the User Interface to use e.g. 'ace'\n * @param {string} textareaId The ID of the textarea to be wrapped.\n */\n function newUiWrapper(uiname, textareaId) {\n if (uiname) {\n return new InterfaceWrapper(uiname, textareaId);\n } else {\n return null;\n }\n }\n\n\n return {\n newUiWrapper: newUiWrapper,\n InterfaceWrapper: InterfaceWrapper\n };\n});\n"],"names":["define","$","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","taId","loadFailId","ta","document","getElementById","textArea","params","attr","uiParams","JSON","parse","lang","readOnly","prop","isLoading","loadFailed","retries","h","parseInt","css","content_lines","val","split","length","rows","Math","min","max","wrapperNode","after","hide","resize","overflow","minHeight","width","border","data","uiInstance","loadUi","mousemove","checkForResize","window","closest","submit","sync","body","on","e","keyCode","ctrlKey","altKey","stop","restart","prototype","syncIntervalSecsBase","hasOwnProperty","sync_interval_secs","alert","loading","setTimeout","sessionStorage","getItem","require","ui","innerHeight","w","innerWidth","Constructor","failed","destroy","addClass","loadFailDiv","jqLoadFailDiv","insertBefore","langString","failMessage","errorDiv","str","s","get_string","fallback","when","done","html","hLast","wLast","show","append","getElement","uiInstancePrototype","Object","getPrototypeOf","syncIntervalSecs","startSyncTimer","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","remove","xLeft","offset","left","maxWidth","hAdjusted","wAdjusted","newUiWrapper"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/outputdisplayarea.js b/amd/src/outputdisplayarea.js
index 247fca51f..c297da893 100644
--- a/amd/src/outputdisplayarea.js
+++ b/amd/src/outputdisplayarea.js
@@ -13,49 +13,76 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
/**
- * @module coderunner/outputdisplayarea
+ * A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally
+ * developed for use in the Scratchpad UI. It has three modes of operation:
+ * - 'text': Just display the output as text, html escaped.
+ * - 'json': The recommended way to display programs that use stdin or output images (or both).
+ * - Accepts JSON in the CRWS response output with fields:
+ * - "returncode": Error/return code from running program.
+ * - "stdout": Stdout text from running program.
+ * - "stderr": Error text from running program.
+ * - "files": An object containing filenames mapped to base64 encoded images.
+ * These will be displayed below any stdout text.
+ * - When input from stdin is required the returncode 42 should be returned, raise this
+ * any time the program asks for input. An (html) input will be added after the last stdout received.
+ * When enter is pressed, runCode is called with value of the input added to the stdin string.
+ * This repeats until returncode is no longer 42.
+ * - 'html': Display program output as raw html inside the output area.
+ * - This can be used to show images and insert other HTML tags (and beyond).
+ * - Giving an tag the class 'coderunner-run-input' will add an event that
+ * on pressing enter will call the runCode method again with the value of that input field added to stdin.
+ * This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.
+ *
+ * @module qtype_coderunner/outputdisplayarea
* @copyright James Napier, 2023, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-import $ from 'jquery';
-
import ajax from 'core/ajax';
import {get_string as getLangString} from 'core/str';
+const ENTER_KEY = 13;
+const INPUT_INTERRUPT = 42;
+const RESULT_SUCCESS = 15;
+const INPUT_CLASS = 'coderunner-run-input';
+const JSON_DISPLAY_PROPS = ['returncode', 'stdout', 'stderr', 'files'];
+
+
/**
* Get the specified language string using
* AJAX and plug it into the given textarea
* @param {string} langStringName The language string name.
- * @param {DOMnode} textarea The textarea into which the error message
+ * @param {DOMnode} display area into which the error message
* should be plugged.
* @param {string} additionalText Extra text to follow the result code.
*/
-const setLangString = async(langStringName, textarea, additionalText) => {
- const message = await getLangString(langStringName, 'filter_ace_inline');
- textarea.show();
- textarea.html(escapeHtml("*** " + message + " ***\n" + additionalText));
+const setLangString = async(langStringName, display, additionalText) => {
+ const message = await getLangString(langStringName, 'qtype_coderunner');
+ display.innerText = "*** " + message + " ***\n";
+ if (additionalText) {
+ display.innerText += additionalText;
+ }
};
-const diagnoseWebserviceResponse = response => {
+const diagnoseWebserviceResponse = (response) => {
// Table of error conditions.
// Each row is response.error, response.result, langstring
// response.result is ignored if response.error is non-zero.
// Any condition not in the table is deemed an "Unknown runtime error".
const ERROR_RESPONSES = [
- [1, 0, 'Sandbox access denied.'], // Sandbox AUTH_ERROR
+ [1, 0, 'error_access_denied'], // Sandbox AUTH_ERROR
[2, 0, 'error_unknown_language'], // Sandbox WRONG_LANG_ID
- [3, 0, 'Sandbox access denied.'], // Sandbox ACCESS_DENIED
+ [3, 0, 'error_access_denied'], // Sandbox ACCESS_DENIED
[4, 0, 'error_submission_limit_reached'], // Sandbox SUBMISSION_LIMIT_EXCEEDED
- [5, 0, 'Sandbox overload. Please wait and try again later'], // Sandbox SERVER_OVERLOAD
+ [5, 0, 'error_sandbox_server_overload'], // Sandbox SERVER_OVERLOAD
[0, 11, ''], // RESULT_COMPILATION_ERROR
- [0, 12, 'Scratchpad crashed. Out of memory, perhaps?'], // RESULT_RUNTIME_ERROR (supervisor process broke)
- [0, 13, 'Scratchpad time limit error. Please report'], // RESULT TIME_LIMIT (supervisor process broke)
- [0, 15, ''], // RESULT_SUCCESS
- [0, 17, 'Scratchpad memory limit error. Please report'], // RESULT_MEMORY_LIMIT
- [0, 21, 'Sandbox overload. Please wait and try again later'], // RESULT_SERVER_OVERLOAD
- [0, 30, 'Excessive output.'] // RESULT OUTPUT_LIMIT
+ [0, 12, ''], // RESULT_RUNTIME_ERROR
+ [0, 13, 'error_timeout'], // RESULT TIME_LIMIT
+ [0, RESULT_SUCCESS, ''], // RESULT_SUCCESS
+ [0, 17, 'error_memory_limit'], // RESULT_MEMORY_LIMIT
+ [0, 21, 'error_sandbox_server_overload'], // RESULT_SERVER_OVERLOAD
+ [0, 30, 'error_excessive_output'] // RESULT OUTPUT_LIMIT
];
for (let i = 0; i < ERROR_RESPONSES.length; i++) {
let row = ERROR_RESPONSES[i];
@@ -66,83 +93,224 @@ const diagnoseWebserviceResponse = response => {
return 'error_unknown_runtime'; // We're dead, Fred.
};
-const getImage = base64 => {
- let image = $(``);
+/**
+ * Concatenates the cmpinfo, stdout and stderr fields of the sandbox
+ * response, truncating both stdout and stderr to a given maximum length
+ * if necessary (in which case '... (truncated)' is appended.
+ * @param {object} response Sandbox response object
+ */
+const combinedOutput = (response) => {
+ return response.cmpinfo + response.output + response.stderr;
+};
+
+/**
+ * Check whether obj has the properties in props, returns missing properties.
+ * @param {object} obj to check properties of
+ * @param {array} props to check for.
+ * @returns {array} of missing properties.
+ */
+const missingProperties = (obj, props) => {
+ return props.filter(prop => !obj.hasOwnProperty(prop));
+};
+
+/**
+ * Insert a base64 encoded string into HTML image.
+ * @param {string} base64 encoded string.
+ * @param {string} type of encoded image file.
+ * @returns {*|jQuery|HTMLElement} image tag containing encoded image from string.
+ */
+const getImage = (base64, type = 'png') => {
+ const image = document.createElement('img');
+ image.src = `data:image/${type};base64,${base64}`;
return image;
};
/**
- *
+ * Constructor for OutputDisplayArea object. For use with the output_displayarea template.
+ * @param {string} displayAreaId The id of the display area div, this should match the 'id'
+ * from the template.
+ * @param {string} outputMode The mode being used for output, must be text, html or json.
+ * @param {string} lang The language to run code with.
+ * @param {string} sandboxParams The sandbox params to run code with.
*/
class OutputDisplayArea {
- constructor(displayAreaId, outputMode) {
+ constructor(displayAreaId, outputMode, lang, sandboxParams) {
this.displayAreaId = displayAreaId;
- this.displayArea = document.getElementById(displayAreaId);
+ this.lang = lang;
+ this.mode = outputMode;
+ this.sandboxParams = sandboxParams;
+
this.textDisplay = document.getElementById(displayAreaId + '-text');
- this.htmlDisplay = document.getElementById(displayAreaId + '-html');
this.imageDisplay = document.getElementById(displayAreaId + '-images');
- this.mode = outputMode;
- this.stdIn = [];
+
+ this.prevRunSettings = null;
}
+ /**
+ * Clear the display of any images and text.
+ */
clearDisplay() {
this.textDisplay.innerHTML = "";
- this.htmlDisplay.innerHTML = "";
this.imageDisplay.innerHTML = "";
}
+ /**
+ * Display text from a CRWS response to the display (escaped).
+ * @param {object} response Coderunner webservice response JSON.
+ */
displayText(response) {
- const output = response.output;
- const error = response.stderr;
- this.textDisplay.innerText = output + error;
+ this.textDisplay.innerText = combinedOutput(response);
}
+ /**
+ * Display HTML from a CRWS response to the display (un-escaped).
+ * Find the first HTML input element with the input class and
+ * add event listeners to handle reading stdin.
+ * @param {object} response Coderunner webservice response JSON,
+ * with output field containing HTML.
+ */
displayHtml(response) {
- const output = response.output;
- const error = response.stderr;
- this.textDisplay.innerHTML = output + error;
+ this.textDisplay.innerHTML = combinedOutput(response);
+ const inputEl = this.textDisplay.querySelector('.' + INPUT_CLASS);
+ if (inputEl) {
+ this.addInputEvents(inputEl);
+ }
+ }
+
+ /**
+ * Display JSON from a CRWS response to the display.
+ * Assumes response.output will be a JSON with the fields:
+ * - "returncode": Error/return code from running program.
+ * - "stdout": Stdout text from running program.
+ * - "stderr": Error text from running program.
+ * - "files": An object containing filenames mapped to base64 encoded images.
+ * These will be displayed below any stdout text.
+ * NOTE: See file header/readme for more info.
+ * @param {object} response Coderunner webservice response JSON,
+ * with output field containing JSON string.
+ */
+ displayJson(response) {
+ const result = this.validateJson(response.output);
+ let text = result.stdout;
+
+ if (result.returncode !== INPUT_INTERRUPT) {
+ text += result.stderr;
+ }
+ if (result.returncode == 13) { // Timeout
+ text += "\n*** Timeout error ***\n";
+ } // TODO: Langstring
+
+ const numImages = this.displayImages(result.files);
+
+ if (text.trim() === '' && result.returncode !== INPUT_INTERRUPT) {
+ if (numImages == 0) {
+ this.displayNoOutput(null);
+ }
+ } else {
+ this.textDisplay.innerText = text;
+ }
+
+ if (result.returncode === INPUT_INTERRUPT) {
+ this.addInput();
+ }
+ }
+
+ /**
+ * Validate JSON to display, make sure it is valid json and has required fields.
+ * @param {string} jsonString string of JSON to be displayed.
+ * @returns {object} JSON as object
+ */
+ validateJson(jsonString) {
+ let result = null;
+ try {
+ result = JSON.parse(jsonString);
+ } catch (e) {
+ window.alert(
+ `Error parsing display JSON output:
+ ${jsonString}
+ JSON Parsing error Msg:
+ ${e.message}
+ The question author must fix this!`
+ );
+ }
+
+ const missing = missingProperties(result, JSON_DISPLAY_PROPS);
+ if (missing.length > 0) {
+ window.alert(
+ `Display JSON (in response.result) is missing the following fields:
+ ${missing.join()}
+ The question author must fix this!`
+ );
+ }
+ return result;
}
+ /**
+ * Display no output message if no output to display or response is null.
+ * @param {object} response Coderunner webservice response JSON, set to null to force
+ * display of no output message.
+ */
displayNoOutput(response) {
- const isNoOutput = response.output === '' && response.stderr === '';
- if (isNoOutput) {
- this.textDisplay.innerHTML = '< No output! >';
+ const isNoOutput = response ? combinedOutput(response).length === 0 : true;
+ if (isNoOutput || response === null) {
+ const span = document.createElement('span');
+ span.style.color = 'red';
+ span.innerText = '< No output! >';
+ this.clearDisplay();
+ this.textDisplay.append(span);
}
return isNoOutput;
}
-
+ /**
+ * Display response using the current display mode.
+ * @param {object} response Coderunner webservice response JSON.
+ */
display(response) {
+ const error = diagnoseWebserviceResponse(response);
+ if (error !== '') {
+ setLangString(error, this.textDisplay);
+ return;
+ }
if (this.displayNoOutput(response)) {
return;
}
+
if (this.mode === 'json') {
- // TODO: error handling.
- const json = JSON.parse(response);
- this.displayJson(json);
+ this.displayJson(response);
} else if (this.mode === 'html') {
this.displayHtml(response);
} else if (this.mode === 'text') {
- const text = response;
- this.displayText(text);
+ this.displayText(response);
} else {
throw Error(`Invalid outputMode given: "${this.mode}"`);
}
}
- async handleRunButtonClick(code, lang, sandboxParams) {
- this.clearDisplay();
+ /**
+ * Run code using the Coderunner webservice and then display the output
+ * using the selected mode. This function uses AJAX to asynchronously run and
+ * display code.
+ * @param {string} code to be run.
+ * @param {string} stdin to be fed into the program.
+ * @param {boolean} shouldClearDisplay will reset the display before displaying.
+ * Use false when doing stdin runs.
+ */
+ runCode(code, stdin, shouldClearDisplay = false) {
+ this.prevRunSettings = [code, stdin];
+ if (shouldClearDisplay) {
+ this.clearDisplay();
+ }
ajax.call([{
methodname: 'qtype_coderunner_run_in_sandbox',
args: {
contextid: M.cfg.contextid, // Moodle context ID
sourcecode: code,
- language: lang,
- params: JSON.stringify(sandboxParams) // Sandbox params
+ language: this.lang,
+ stdin: stdin,
+ params: JSON.stringify(this.sandboxParams) // Sandbox params
},
done: (responseJson) => {
- // TODO: error handling.
const response = JSON.parse(responseJson);
- // Const error = diagnoseWebserviceResponse(response);
this.display(response);
},
fail: (error) => {
@@ -151,53 +319,64 @@ class OutputDisplayArea {
}]);
}
- displayJson(json) {
- var result = json;
- var text = result.stdout;
+ /**
+ * Add an input field with event listeners to support running again
+ * with new stdin entered by user.
+ */
+ addInput() {
+ const inputId = `${this.displayAreaId}-input-field`;
+ this.textDisplay.innerHTML += ``;
+ const inputEl = document.getElementById(inputId);
+ this.addInputEvents(inputEl);
+ }
- if (result.returncode !== 42) {
- text += result.stderr;
- }
- if (result.returncode == 13) { // Timeout
- text += "\n*** Timeout error ***\n";
- }
+ /**
+ * Add event listeners to inputEl overriding enter key to:
+ * - Prevent form-submit.
+ * - Call runCode again, adding value in inputEl to stdin.
+ * @param {node} inputEl to add event listeners to.
+ */
+ addInputEvents(inputEl) {
+ inputEl.focus();
- var numImages = 0;
- if (result.files) {
- $(this.imageDisplay).empty();
- for (var prop in result.files) {
- var image = getImage(result.files[prop]);
- $(this.imageDisplay).append(image);
- numImages += 1;
+ inputEl.addEventListener('keydown', (e) => {
+ if (e.keyCode === ENTER_KEY) {
+ e.preventDefault(); // Do NOT form submit.
}
- }
-
- if (text.trim() === '' && result.returncode !== 42) {
- if (numImages == 0) {
- $(this.textDisplay).html('< No output! >');
+ });
+ inputEl.addEventListener('keyup', (e) => {
+ if (e.keyCode === ENTER_KEY) {
+ const line = inputEl.value;
+ inputEl.remove();
+ this.textDisplay.innterHTML += line; // Perhaps this should be sanitized.
+ this.prevRunSettings[1] += line + '\n';
+ this.runCode(...this.prevRunSettings, false);
}
- } else {
- $(this.textDisplay).text(text);
- }
+ });
+ }
- if (result.returncode === 42) {
- var inputId = `${this.displayAreaId}-input-field`;
- $(this.textDisplay).html($(this.textDisplay).html() + ``);
- var inputEl = $(document.getElementById(inputId));
- inputEl.focus();
-
- inputEl.on('keyup', (e) => {
- if (e.keyCode === 13) {
- const line = inputEl.val();
- inputEl.remove();
- $(this.textDisplay).html($(this.textDisplay).html() + line);
- this.handleRunButtonClick();
- }
- });
+ /**
+ * Take the files from a JSON response and display them.
+ * @param {object} files from response, in filename: filecontents pairs.
+ * @returns {number} number of images displayed.
+ */
+ displayImages(files) {
+ let numImages = 0;
+ for (const [fname, fcontents] of Object.entries(files)) {
+ const fileType = fname.split('.')[1];
+ if (fileType) {
+ const image = getImage(fcontents, fileType);
+ this.imageDisplay.append(image);
+ numImages += 1;
+ } else {
+ window.alert(`Could not read filename correctly: ${fname}`);
+ }
}
+ return numImages;
}
}
+
export {
OutputDisplayArea
};
diff --git a/amd/src/ui_scratchpad.js b/amd/src/ui_scratchpad.js
index 64808a1ca..a9c76a3bf 100644
--- a/amd/src/ui_scratchpad.js
+++ b/amd/src/ui_scratchpad.js
@@ -20,7 +20,7 @@
* This plugin replaces the usual textarea answer element with a UI is designed to
* allow the execution of code in the CodeRunner question in a manner similar to an IDE.
* It contains two editor boxes, one on top of another, allowing users to enter and
- * edit code in both.
+ * edit code in both. It contains two embedded Ace UIs.
* By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,
* clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,
* a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that
@@ -41,7 +41,6 @@
* test_code: [""] A list containing a string with containing answer code from the second editor;
* show_hide: ["1"] when scratchpad is visible, otherwise [""];
* prefix_ans: ["1"] when Prefix with Answer is checked, otherwise [""].
- * . The fields of that object are the names
*
* UI Parameters:
* - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.
@@ -53,20 +52,23 @@
* - wrapper_src: location of wrapper code to be used by the run button, if applicable:
* setting to globalextra will use text in global extra field,
* - prototypeextra will use the prototype extra field.
- * - html_output: when true, the output from run will be displayed as raw HTML instead of text.
- * - disable_scratchpad: disable the scratchpad, effectively returning back to Ace UI
+ * - output_display_mode: control how program output is displayed on runs, there are three modes:
+ * - text: display program output as text, html escaped;
+ * - json: display program output, when it is json,
+ * - html: display program output as raw html.
+ * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...
+ * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI
* from student perspective.
* - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.
* This can be used to swap the default state.
*
- * @module coderunner/ui_scratchpad
+ * @module qtype_coderunner/ui_scratchpad
* @copyright Richard Lobb, 2022, The University of Canterbury
* @copyright James Napier, 2022, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-import ajax from 'core/ajax';
import Templates from 'core/templates';
import {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';
@@ -78,11 +80,11 @@ import {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';
* @param {string} current serialisation.
* @returns {string} inverted serialisation.
*/
-const invertSerial = current => current[0] == '1' ? [''] : ['1'];
+const invertSerial = (current) => current[0] === '1' ? [''] : ['1'];
/**
* Insert the answer code and test code into the wrapper. This may
- * defined by the user, in UI Params or globalextra. If prefixAns is
+ * be defined by the user, in UI Params or globalextra. If prefixAns is
* false: do not include answerCode in final wrapper.
* @param {string} answerCode text.
* @param {string} testCode text.
@@ -109,7 +111,7 @@ const fillWrapper = (answerCode, testCode, prefixAns, template) => {
* Does not add keys/values to the result if that key is not in defualts.
* @param {object} defaults object with values to be overwritten.
* @param {object} prescribed settings, typically set by a user.
- * @returns {object} filled with defualt values, overwritten by their prescribed value (iff included).
+ * @returns {object} filled with default values, overwritten by their prescribed value (iff included).
*/
const overwriteValues = (defaults, prescribed) => {
let overwritten = {...defaults};
@@ -148,8 +150,9 @@ class ScratchpadUi {
button_name: '',
prefix_name: '',
help_text: '',
+ params: {},
run_lang: uiParams.lang, // Use answer's ace language if not specified.
- html_output: false,
+ output_display_mode: 'text',
disable_scratchpad: false,
wrapper_src: null
};
@@ -161,7 +164,7 @@ class ScratchpadUi {
this.outerDiv = null;
this.outputDisplay = null;
this.invertPreload = uiParams.invert_prefix;
- this.lang = uiParams.lang; // Todo: this vs this.ui params
+ this.lang = uiParams.lang;
this.numRows = this.textArea.rows;
this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);
this.runWrapper = this.getRunWrapper();
@@ -240,19 +243,19 @@ class ScratchpadUi {
}
handleRunButtonClick() {
+ if (this.outputDisplay === null) {
+ return;
+ }
this.sync(); // Use up-to-date serialization.
const preloadString = this.textArea.value;
const serial = this.readJson(preloadString);
- const sandboxParams = this.uiParams.params;
- const language = this.lang;
const code = fillWrapper(
serial.answer_code,
serial.test_code,
serial.prefix_ans[0],
this.runWrapper
);
- // TODO: handle case where no output display area exists...
- this.outputDisplay.handleRunButtonClick(code, language, sandboxParams);
+ this.outputDisplay.runCode(code, '', true); // Call with no stdin.
}
updateContext(preload) {
@@ -288,7 +291,7 @@ class ScratchpadUi {
"output_display": {
"id": this.textAreaId + '_run-output'
},
- // Bootstrap collapse requires jQuerry friendly ids to work...
+ // Bootstrap collapse requires jQuery friendly ids to work...
"jquery_escape": function() {
return function(text, render) {
return CSS.escape(render(text));
@@ -338,16 +341,18 @@ class ScratchpadUi {
this.updateContext(preload);
try {
const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);
- const outputMode = this.uiParams.html_output ? 'html' : 'text';
this.drawUi(html);
this.addAceUis();
- this.outputDisplay = new OutputDisplayArea(this.context.output_display.id, outputMode); // TODO: change!
+ this.outputDisplay = new OutputDisplayArea(
+ this.context.output_display.id,
+ this.uiParams.output_display_mode,
+ this.uiParams.run_lang,
+ this.uiParams.params
+ );
this.addEventListeners();
} catch (e) {
this.fail = true;
this.failString = "scratchpad_ui_templateloadfail";
-
- return;
}
}
@@ -371,9 +376,8 @@ class ScratchpadUi {
addEventListeners() {
const runButton = document.getElementById(this.textAreaId + '_run-btn');
- const outputDisplayarea = document.getElementById(this.context.output_display.id);
if (runButton) {
- runButton.addEventListener('click', () => this.handleRunButtonClick(ajax, outputDisplayarea));
+ runButton.addEventListener('click', () => this.handleRunButtonClick());
}
}
diff --git a/amd/src/ui_scratchpad.json b/amd/src/ui_scratchpad.json
index b3f0262e9..568e9677a 100644
--- a/amd/src/ui_scratchpad.json
+++ b/amd/src/ui_scratchpad.json
@@ -25,14 +25,14 @@
"type": "string",
"default": null
},
+ "output_display_mode": {
+ "type": "string",
+ "default": "text"
+ },
"params": {
"type": "object",
"default": null
},
- "html_output": {
- "type": "boolean",
- "default": false
- },
"disable_scratchpad": {
"type": "boolean",
"default": false
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index 3c73a0f00..86e1a28ab 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -1265,3 +1265,14 @@ function should be applied, e.g. {{STUDENT_ANSWER | e(\'py\')}} is
$string['scratchpad_ui_invalidserialisation'] = 'Invalid JSON serialisation provided, must include \"answer_code\" field.';
$string['scratchpad_ui_templateloadfail'] = 'Scratchpad UI template failed to load, please refresh the page.';
$string['scratchpad_ui_badrunwrappersrc'] = 'Invalid run wrapper source given, please contact question author.';
+
+$string['error_access_denied'] = 'Sandbox server access denied';
+$string['error_excessive_output'] = 'Excessive output';
+$string['error_json_params'] = 'Params set are not in correct JSON format';
+$string['error_jobe_unknown'] = 'Unknown error from Jobe server';
+$string['error_memory_limit'] = 'Memory limit exceeded';
+$string['error_sandbox_server_overload'] = 'Jobe server overload';
+$string['error_submission_limit_reached'] = 'Jobe sandbox submission limit reached';
+$string['error_timeout'] = 'Time limit exceeded';
+$string['error_unknown_language'] = 'Unknown language requested';
+$string['error_unknown_runtime'] = 'Unknown runtime error';
diff --git a/templates/help_icon.mustache b/templates/help_icon.mustache
index 8b602566b..2a9c1f823 100644
--- a/templates/help_icon.mustache
+++ b/templates/help_icon.mustache
@@ -17,7 +17,7 @@
{{!
@template qtype_coderunner/help_icon
- Help icon.
+ Help icon. A copy of Core's to allow for langauge string insertion...
Example context (json):
{
diff --git a/templates/output_displayarea.mustache b/templates/output_displayarea.mustache
index 2a1f946a0..526c2c2af 100644
--- a/templates/output_displayarea.mustache
+++ b/templates/output_displayarea.mustache
@@ -28,8 +28,9 @@
{{ text }}
-
-
+
diff --git a/tests/behat/behat_coderunner.php b/tests/behat/behat_coderunner.php
index b666ea11a..1cdb10ced 100644
--- a/tests/behat/behat_coderunner.php
+++ b/tests/behat/behat_coderunner.php
@@ -37,14 +37,14 @@ public function the_coderunner_sandbox_is_enabled() {
set_config('jobesandbox_enabled', 1, 'qtype_coderunner');
set_config('jobe_host', '172.17.0.1:4000', 'qtype_coderunner');
}
-
+
/**
* Checks that a given string appears within answer textarea.
* Intended for checking UI serialization
- * @Then /^I should see in answer field "(?P(?:[^"]|\\")*)"$/
+ * @Then /^I should see in answer field "(?P(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @param string $expected The string that we expect to find
- */
+ */
public function i_should_see_in_answer($expected) {
$xpath = '//textarea[contains(@class, "coderunner-answer")]';
$driver = $this->getSession()->getDriver();
@@ -59,13 +59,13 @@ public function i_should_see_in_answer($expected) {
throw new ExpectationException($error, $this->getSession());
}
}
-
+
/**
* Sets answer textarea (seen after presing ctrl+m) to a value
- * @Then /^I set answer field to "(?P(?:[^"]|\\")*)"$/
+ * @Then /^I set answer field to "(?P(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @param string $expected The string that we expect to find
- */
+ */
public function i_set_answer($value) {
$xpath = '//textarea[contains(@class, "coderunner-answer")]';
$driver = $this->getSession()->getDriver();
@@ -76,35 +76,35 @@ public function i_set_answer($value) {
$page = $this->getSession()->getPage();
$val = $page->find('xpath',$xpath)->setValue($value);
}
-
+
/**
* Sets answer textarea (seen after presing ctrl+m) to a value
* @Then /^I set answer field to:$/
* @throws ExpectationException
* @param string $expected The string that we expect to find
- */
+ */
public function i_set_answer_pystring($pystring) {
$this->i_set_answer($pystring->getRaw());
}
-
+
/**
* Checks that a given string appears within answer textarea.
* Intended for checking UI serialization
* @Then /^I should see in answer field:$/
- */
+ */
public function i_should_see_in_answer_pystring(Behat\Gherkin\Node\PyStringNode $pystring) {
$this->i_should_see_in_answer($pystring->getRaw());
}
-
+
/**
- * Sets the ace editor content to provided string, using name of associated textarea.
- * NOTE: this assumes the existence of a text area next to a
+ * Sets the ace editor content to provided string, using name of associated textarea.
+ * NOTE: this assumes the existence of a text area next to a
* UI wrapper div containing the Ace div!
* Intended as a replacement for I set field to , for ace fields.
* @Then /^I set the ace field "(?P(?:[^"]|\\")*)" to "(?P(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @param string $expected The string that we expect to find
- */
+ */
public function i_set_ace_field($elname, $value) {
$xpath = "//textarea[@name='$elname']/../div/div";
$driver = $this->getSession()->getDriver();
@@ -114,7 +114,7 @@ public function i_set_ace_field($elname, $value) {
throw new ExpectationException($error, $this->getSession());
}
// We inject JS into the browser to set the Ace editor contents...
- // (Gross) JS to take the x-path for the div managed by Ace,
+ // (Gross) JS to take the x-path for the div managed by Ace,
// open editor for that div, and set the editors value.
$javascript = "const editorNode = document.evaluate("
. "`$xpath`,"
@@ -126,7 +126,21 @@ public function i_set_ace_field($elname, $value) {
. "editor.setValue(`$value`);";
$this->getSession()->executeScript($javascript);
}
-
+
+ /**
+ * Sets the ace editor content to provided string, using name of associated textarea.
+ * NOTE: this assumes the existence of a text area next to a
+ * UI wrapper div containing the Ace div!
+ * Intended as a replacement for I set field to , for ace fields.
+ * @Then /^I set the ace field "(?P(?:[^"]|\\")*)" to:$/
+ * @throws ExpectationException
+ * @param string $expected The string that we expect to find
+ */
+ public function i_set_ace_field_pystring($elname, $pystring) {
+ $this->i_set_ace_field($elname, $pystring->getRaw());
+ }
+
+
/**
* Checks that a given string appears within a visible ins or del element
* that has a background-color attribute that is not 'inherit'.
diff --git a/tests/behat/scratchpad_ui_params.feature b/tests/behat/scratchpad_ui_params.feature
index 52d1b82e1..c3dbd6748 100644
--- a/tests/behat/scratchpad_ui_params.feature
+++ b/tests/behat/scratchpad_ui_params.feature
@@ -1,4 +1,4 @@
-@qtype @qtype_coderunner @javascript @scratchpad @scratchpaduiparam
+@qtype @javascript @qtype_coderunner @scratchpad
Feature: Test the Scratchpad UI, UI Params
In order to use the Scratchpad UI
As a teacher
@@ -125,22 +125,6 @@ Feature: Test the Scratchpad UI, UI Params
And I should not see "Run"
And I should not see "Prefix Answer?"
- Scenario: Set HTML output to true, 'print' a button to output area
- And I set the field "id_answer" to ""
- And I set the following fields to these values:
- | id_customise | 1 |
- | id_uiplugin | Scratchpad |
- | id_uiparameters | {"html_output": true} |
-
- And I press "id_submitbutton"
- Then I should see "Print answer"
-
- When I choose "Preview" action for "Print answer" in the question bank
- And I click on "Scratchpad" "button"
- And I set the ace field "test_code" to "print('')"
- Then I press "Run"
- And I press "Hi"
-
Scenario: Define wrapper in UI params and click run, insert both answer and Scratchpad code, NO prefix with answer, click run
And I set the field "id_answer" to ""
And I set the following fields to these values:
@@ -385,3 +369,186 @@ Feature: Test the Scratchpad UI, UI Params
And I set the field "prefix_ans" to ""
And I press the tab key
Then I should see "superusefulhelptext"
+
+ Scenario: Set output_display_mode to text, 'print' a hidden div to output area
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"output_display_mode": "text"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "test_code" to "print('
Hi
')"
+ Then I press "Run"
+ And I should see "
Hi
"
+
+ Scenario: Set output_display_mode to text, get no output.
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"output_display_mode": "text"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I press "Run"
+ And I press "Run"
+ Then I should see "< No output! >"
+ And I should not see "< No output! >< No output! >"
+
+ Scenario: Set output_display_mode to text, get a runtime error, get a compile error.
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"output_display_mode": "text"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "test_code" to "d"
+ And I press "Run"
+ Then I should see "NameError: name 'd' is not defined."
+
+ When I set the ace field "test_code" to "'d'd'd'"
+ And I press "Run"
+ Then I should see "SyntaxError: invalid syntax"
+
+ Scenario: Set output_display_mode to json, 'print' a hidden div to output area
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"output_display_mode": "json"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "test_code" to:
+ """
+ import json
+ output = {'returncode': 0,
+ 'stdout' : '
Hi
',
+ 'stderr' : '',
+ 'files' : ''
+ }
+ print(json.dumps(output))
+ """
+ Then I press "Run"
+ And I should see "
Hi
"
+
+ Scenario: Set output_display_mode to json, get no output.
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"output_display_mode": "json"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "test_code" to:
+ """
+ import json
+ output = {'returncode': 0,
+ 'stdout' : '',
+ 'stderr' : '',
+ 'files' : ''
+ }
+ print(json.dumps(output))
+ """
+ And I press "Run"
+ And I press "Run"
+ Then I should see "< No output! >"
+ And I should not see "< No output! >< No output! >"
+
+ Scenario: Set output_display_mode to json, get a runtime error, get a compile error.
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"output_display_mode": "json"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "test_code" to:
+ """
+ import json
+ output = {'returncode': 0,
+ 'stdout' : 'hello wolrd',
+ 'stderr' : 'i am error',
+ 'files' : ''
+ }
+ print(json.dumps(output))
+ """
+ And I press "Run"
+ Then I should see "hello wolrdi am error"
+
+ Scenario: Set HTML output_display_mode to html, 'print' a button to output area
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"output_display_mode": "html"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "test_code" to "print('')"
+ Then I press "Run"
+ And I press "Hi"
+
+ Scenario: Set HTML output_display_mode to html, get no output.
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"output_display_mode": "html"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I press "Run"
+ And I press "Run"
+ Then I should see "< No output! >"
+ And I should not see "< No output! >< No output! >"
+
+ Scenario: Set output_display_mode to html, get a runtime error, get a compile error.
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"output_display_mode": "html"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "test_code" to "d"
+ And I press "Run"
+ Then I should see "NameError: name 'd' is not defined."
+
+ When I set the ace field "test_code" to "'d'd'd'"
+ And I press "Run"
+ Then I should see "SyntaxError: invalid syntax"
diff --git a/tests/fixtures/input_mpl_wrapper_html.py b/tests/fixtures/input_mpl_wrapper_html.py
new file mode 100644
index 000000000..cd58d026f
--- /dev/null
+++ b/tests/fixtures/input_mpl_wrapper_html.py
@@ -0,0 +1,129 @@
+import ast, traceback, sys, io, subprocess, base64, os, ast, traceback, html
+MAX_OUTPUT_CHARS = 30000
+
+student_code = """
+{{ ANSWER_CODE }}
+{{ SCRATCHPAD_CODE }}
+"""
+
+uses_matplotlib = 'matplotlib' in student_code
+
+subproc_code = """
+import sys
+MAX_OUTPUT_CHARS = 30000
+__saved_input__ = input
+
+def input(prompt=''):
+ try:
+ line = __saved_input__()
+ except EOFError:
+ print(prompt, end = '')
+ sys.stderr.flush()
+ sys.stdout.flush()
+ sys.exit(42)
+ print(prompt, end='')
+ print(line)
+ return line
+
+__saved_print__ = print
+__output_chars__ = 0
+
+def print(*params, **keyparams):
+ global __output_chars__
+ for param in params:
+ try:
+ __output_chars__ += len(str(param))
+ except:
+ pass
+ if __output_chars__ > 2 * MAX_OUTPUT_CHARS:
+ __saved_print__("\\\\n*** Excessive output. Job aborted ***", file=sys.stderr)
+ sys.exit(1)
+ else:
+ __saved_print__(*params, **keyparams)
+"""
+
+if uses_matplotlib:
+ subproc_code += """
+import os, tempfile, sys
+os.environ["MPLCONFIGDIR"] = tempfile.mkdtemp()
+import matplotlib as _mpl
+_mpl.use("Agg")
+"""
+
+subproc_code += student_code
+
+if uses_matplotlib:
+ subproc_code += """
+figs = _mpl.pyplot.get_fignums()
+for i, fig in enumerate(figs):
+ _mpl.pyplot.figure(fig)
+ filename = f'image{i}.png'
+ _mpl.pyplot.savefig(filename, bbox_inches='tight')
+"""
+
+
+
+
+
+def b64encode(filename):
+ """Return the contents of the given file in base64"""
+ with open(filename, "br") as fin:
+ contents = fin.read()
+ contents_b64 = base64.b64encode(contents).decode("utf8")
+ return contents_b64
+
+def truncated(s):
+ return s if len(s) < MAX_OUTPUT_CHARS else s[:MAX_OUTPUT_CHARS] + ' ... (truncated)'
+
+def check_syntax():
+ try:
+ ast.parse(student_code)
+ return ''
+ except SyntaxError:
+ catcher = io.StringIO()
+ traceback.print_exc(limit=0, file=catcher)
+ return catcher.getvalue()
+
+stdout = ''
+stderr = check_syntax()
+if stderr == '': # No syntax errors
+ program_code = subproc_code
+ with open('prog.py', 'w') as outfile:
+ outfile.write(program_code)
+ proc = subprocess.Popen([sys.executable, 'prog.py'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+ try:
+ stdout, stderr = proc.communicate(timeout=3)
+ returncode = proc.returncode
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ stdout, stderr = proc.communicate()
+ returncode = 13
+
+
+else:
+ returncode = 1 # Syntax errors
+
+# Pick up any .png or .jpg image files.
+image_extensions = ['png', 'jpg', 'jpeg']
+image_files = [fname for fname in os.listdir() if fname.lower().split('.')[-1] in image_extensions]
+files = {fname: b64encode(fname) for fname in image_files}
+
+output = {
+ 'returncode': returncode,
+ 'stdout' : truncated(stdout),
+ 'stderr' : truncated(stderr),
+ 'files' : files,
+}
+
+
+
+if output:
+ text_output = output['stdout'] + output['stderr']
+ text_output = html.escape(text_output).replace(' ', ' ').replace('\n', ' ')
+ if output['returncode'] == 42:
+ text_output += ''
+ print(text_output)
+
+for fname, contents in files.items():
+ if fname.endswith('png'):
+ print(f'')
diff --git a/tests/fixtures/input_mpl_wrapper_json.py b/tests/fixtures/input_mpl_wrapper_json.py
new file mode 100644
index 000000000..05398c149
--- /dev/null
+++ b/tests/fixtures/input_mpl_wrapper_json.py
@@ -0,0 +1,118 @@
+import ast, traceback, sys, io, subprocess, base64, os, ast, traceback, json
+MAX_OUTPUT_CHARS = 30000
+
+student_code = """
+{{ ANSWER_CODE }}
+{{ SCRATCHPAD_CODE }}
+"""
+
+uses_matplotlib = 'matplotlib' in student_code
+
+subproc_code = """
+import sys
+MAX_OUTPUT_CHARS = 30000
+__saved_input__ = input
+
+def input(prompt=''):
+ try:
+ line = __saved_input__()
+ except EOFError:
+ print(prompt, end = '')
+ sys.stderr.flush()
+ sys.stdout.flush()
+ sys.exit(42)
+ print(prompt, end='')
+ print(line)
+ return line
+
+__saved_print__ = print
+__output_chars__ = 0
+
+def print(*params, **keyparams):
+ global __output_chars__
+ for param in params:
+ try:
+ __output_chars__ += len(str(param))
+ except:
+ pass
+ if __output_chars__ > 2 * MAX_OUTPUT_CHARS:
+ __saved_print__("\\\\n*** Excessive output. Job aborted ***", file=sys.stderr)
+ sys.exit(1)
+ else:
+ __saved_print__(*params, **keyparams)
+"""
+
+if uses_matplotlib:
+ subproc_code += """
+import os, tempfile, sys
+os.environ["MPLCONFIGDIR"] = tempfile.mkdtemp()
+import matplotlib as _mpl
+_mpl.use("Agg")
+"""
+
+subproc_code += student_code
+
+if uses_matplotlib:
+ subproc_code += """
+figs = _mpl.pyplot.get_fignums()
+for i, fig in enumerate(figs):
+ _mpl.pyplot.figure(fig)
+ filename = f'image{i}.png'
+ _mpl.pyplot.savefig(filename, bbox_inches='tight')
+"""
+
+
+
+
+
+def b64encode(filename):
+ """Return the contents of the given file in base64"""
+ with open(filename, "br") as fin:
+ contents = fin.read()
+ contents_b64 = base64.b64encode(contents).decode("utf8")
+ return contents_b64
+
+def truncated(s):
+ return s if len(s) < MAX_OUTPUT_CHARS else s[:MAX_OUTPUT_CHARS] + ' ... (truncated)'
+
+def check_syntax():
+ try:
+ ast.parse(student_code)
+ return ''
+ except SyntaxError:
+ catcher = io.StringIO()
+ traceback.print_exc(limit=0, file=catcher)
+ return catcher.getvalue()
+
+stdout = ''
+stderr = check_syntax()
+if stderr == '': # No syntax errors
+ program_code = subproc_code
+ with open('prog.py', 'w') as outfile:
+ outfile.write(program_code)
+ proc = subprocess.Popen([sys.executable, 'prog.py'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+ try:
+ stdout, stderr = proc.communicate(timeout=3)
+ returncode = proc.returncode
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ stdout, stderr = proc.communicate()
+ returncode = 13
+
+
+else:
+ returncode = 1 # Syntax errors
+
+# Pick up any .png or .jpg image files.
+image_extensions = ['png', 'jpg', 'jpeg']
+image_files = [fname for fname in os.listdir() if fname.lower().split('.')[-1] in image_extensions]
+files = {fname: b64encode(fname) for fname in image_files}
+
+output = {
+ 'returncode': returncode,
+ 'stdout' : truncated(stdout),
+ 'stderr' : truncated(stderr),
+ 'files' : files,
+}
+
+print(json.dumps(output))
\ No newline at end of file
From 7bf4982b9e0ca6674762148f201cae1d1b2220a6 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Thu, 2 Feb 2023 11:33:15 +1300
Subject: [PATCH 050/188] Bug fix: shouldn't try to merge prototype's UI
parameters with the question's UI parameters when the UI plugin has just been
changed.
---
question.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/question.php b/question.php
index c9f98e84b..38560c2db 100644
--- a/question.php
+++ b/question.php
@@ -288,7 +288,7 @@ private function twig_render_with_seed($text, $seed) {
// Get the default ui parameters for the ui plugin and merge in
- // both the prototype's and this question's parameters.
+ // both the prototypes and this questions parameters.
// In order to support the legacy method of including ui parameters
// within the template parameters, we need to filter out only the
// valid ui parameters, so need to load the uiplugin json file to find
From 82502e87417f08b0e5a7f7a02c8f39d4241e96a0 Mon Sep 17 00:00:00 2001
From: Michelle Hsieh
Date: Thu, 2 Feb 2023 11:46:50 +1300
Subject: [PATCH 051/188] Bulk tester now works with sample files
---
amd/build/authorform.min.js.map | 2 +-
amd/build/ui_scratchpad.min.js | 57 +++++++++++++++++++++++
amd/build/ui_scratchpad.min.js.map | 1 +
classes/bulk_tester.php | 3 +-
classes/coderunner_files.php | 74 ++++++++++++++++++++++++++++++
question.php | 10 +++-
questiontestrun.php | 4 +-
7 files changed, 144 insertions(+), 7 deletions(-)
create mode 100644 amd/build/ui_scratchpad.min.js
create mode 100644 amd/build/ui_scratchpad.min.js.map
create mode 100644 classes/coderunner_files.php
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
index a44df6300..02b9de9e9 100644
--- a/amd/build/authorform.min.js.map
+++ b/amd/build/authorform.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n */\n function setUis() {\n var uiname = uiplugin.val();\n var enableUi = true;\n if (uiname === 'html' && uiparameters.val().trim() !== '') {\n try {\n var uiparams = JSON.parse(uiparameters.val());\n if (uiparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","enableUi","trim","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * The sample answer and answer preload have the data-params attribute which contains\n * the UI params in a JSON from the current question merged with the prototype.\n * Both of them are identical and are changed simultaneously; only checking\n * answer as state is identical.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n *\n */\n function setUis() {\n let uiname = uiplugin.val();\n let answer = $('#id_answer');\n let enableUi = true;\n if (uiname === 'html' && answer.attr('data-params') !== '') {\n try {\n let answerparams = JSON.parse(answer.attr('data-params'));\n if (answerparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","answer","enableUi","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","trim","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":";;;;;;;AAuBAA,qCAAO,CAAC,SAAU,wCAAyC,aAAa,SAASC,EAAGC,GAAIC,SAGhFC,aAAe,OASfC,iBAAmB,CACnBC,SAAqB,CAAC,eAAgB,QAAS,IAC/CC,qBAAqB,CAAC,2BAA4B,UAAW,GACrC,SAAUC,aACW,MAAVA,QAEnCC,iBAAqB,CAAC,uBAAwB,QAAS,IACvDC,WAAqB,CAAC,iBAAkB,QAAS,IACjDC,QAAqB,CAAC,cAAe,QAAS,WAC9CC,cAAqB,CAAC,oBAAqB,QAAS,IACpDC,eAAqB,CAAC,qBAAsB,QAAS,GAC7B,SAAUC,iBACCA,SAASC,QAAQ,KAAM,SAE1DC,oBAAqB,CAAC,0BAA2B,UAAW,GACpC,SAAUR,aACW,MAAVA,QAEnCS,OAAqB,CAAC,aAAc,QAAS,kBAC7CC,cAAqB,CAAC,oBAAqB,QAAS,IACpDC,SAAqB,CAAC,eAAgB,QAAS,IAC/CC,QAAqB,CAAC,cAAe,QAAS,IAC9CC,SAAqB,CAAC,eAAgB,QAAS,cAomB5C,CAACC,4BA1lBAC,UAAYtB,EAAE,sBACduB,iBAAmBvB,EAAE,mBACrBK,SAAWL,EAAE,gBACbwB,mBAAqBxB,EAAE,gCACvByB,YAAczB,EAAE,mBAChB0B,eAAiB1B,EAAE,sBACnB2B,OAAS3B,EAAE,cACXkB,SAAWlB,EAAE,gBACbmB,QAAUnB,EAAE,eACZ4B,UAAY5B,EAAE,iBACd6B,aAAe7B,EAAE,4BACjB8B,eAAiB9B,EAAE,sBACnB+B,oBAAsB/B,EAAE,2BACxBgC,sBAAwBhC,EAAE,2BAC1BiC,sBAAwBjC,EAAE,mCAC1BkC,aAAeN,UAAUO,KAAK,WAC9BC,cAAgBpC,EAAE,qBAClBqC,WAAarC,EAAE,wBACfsC,SAAWtC,EAAE,0BAA0BmC,KAAK,SAC5CI,oBAAsBvC,EAAE,eACxBwC,SAAWxC,EAAE,sBACbyC,aAAezC,EAAE,gBACjB0C,eAAiB1C,EAAE,uBACnB2C,gBAAkB3C,EAAE,yBACpBoB,SAAWpB,EAAE,gBACb4C,aAAe5C,EAAE,6BAWZ6C,MAAMC,KAAMC,YAEbC,KAIAC,UALAC,GAAKlD,EAAEmD,SAASC,eAAeN,OAE/BO,YAAcH,GAAGI,KAAK,aACtBC,WAAaL,GAAGI,KAAK,eACrBE,OAAS,GAKbN,GAAGI,KAAK,sBAAuB5B,eAAe+B,OAC9CP,GAAGI,KAAK,mBAAoB7B,YAAYgC,OACxCP,GAAGI,KAAK,aAActD,EAAE,kBAAkByD,WAEtCD,OAASE,KAAKC,MAAMJ,YACtB,MAAMK,MAEO,UADfb,OAASA,OAAOc,iBAEZd,OAAS,IAGD,qBAARD,MAAuC,mBAARA,KAC/BE,KAAO,IAEPA,KAAO9B,SAASiB,KAAK,SACR,gBAATW,MAA0B3B,QAAQgB,KAAK,WACvCa,cA2Mc7B,aAClB2C,MAAOC,KACP5C,QAAQ6C,QAAQ,KAAO,SAChB7C,YAEP2C,MAAQ3C,QAAQ8C,MAAM,KACjBF,EAAI,EAAGA,EAAID,MAAMI,OAAQH,OACtBD,MAAMC,GAAGI,SAAS,YACXL,MAAMC,GAAGK,OAAO,EAAGN,MAAMC,GAAGG,OAAS,UAG7CJ,MAAMI,OAAS,EAAIJ,MAAM,GAAK,GAtN1BO,CAAiBlD,QAAQgB,KAAK,aAI7Cc,UAAYC,GAAGoB,KAAK,wBAEHrB,UAAUF,SAAWA,QAAUM,aAAeL,OAI/DE,GAAGI,KAAK,YAAaN,MAEhBC,WAIDO,OAAOR,KAAOA,KACdC,UAAUsB,OAAOxB,OAAQS,SAJzBP,UAAY,IAAIhD,GAAGuE,iBAAiBzB,OAAQD,gBAoB3C2B,aACD1B,OAAS3B,SAASqC,MAClBiB,OAAS1E,EAAE,cACX2E,UAAW,KACA,SAAX5B,QAAoD,KAA/B2B,OAAOpB,KAAK,oBAGS,IADnBI,KAAKC,MAAMe,OAAOpB,KAAK,gBACzBsB,mBACbD,UAAW,GAEjB,MAAOE,OACLC,MAAM,0BAGVH,WACA9B,MAAM,YAAaE,QACnBF,MAAM,mBAAoBE,kBAQzBgC,2BAA2BC,eAC5BC,QAAUD,UAAY,QAAU,OACpChD,sBAAsBkD,IAAI,UAAWD,SACrChD,sBAAsBiD,IAAI,UAAWD,SACjCD,WAAarD,OAAOQ,KAAK,YACzBU,MAAM,cAAe,gBA+CpBsC,2BAA2BC,QAASC,cACrCC,cAAeC,QAZfC,wBAeC,IAAIC,gBAxCwBC,aAE7BzC,UADA0C,MAAQ,CAAC,cAAe,sBAExBhE,OAAOQ,KAAK,eACR,IAAI4B,EAAI,EAAGA,EAAI4B,MAAMzB,OAAQH,KAE7Bd,UADKjD,EAAEmD,SAASC,eAAeuC,MAAM5B,KACtBO,KAAK,wBACHoB,QACbzC,UAAU2C,UACH3C,YAAcyC,SACrBzC,UAAU4C,OA6BtBC,EAA4B,GACZ1F,iBACZkF,cAAgBlF,iBAAiBqF,KACjCF,QAAUF,SAASI,KAAOJ,SAASI,KAAOH,cAAc,GACpDA,cAAcpB,OAAS,IAEvBqB,SADAQ,EAAST,cAAc,IACNC,UAErBvF,EAAEsF,cAAc,IAAInD,KAAKmD,cAAc,GAAIC,SAG/C3D,UAAUO,KAAK,WAAW,GAC1BjC,IAAI8F,WAAW,2BAA4B,oBAAoBC,MAAK,SAAUC,OA2C7DC,MAAOC,iBAAkBC,KAEtCC,WA5CA/D,oBAAoB8D,MA0CPF,MA1CwBf,QA0CjBgB,iBA1C0BF,EA0CRG,KA1CWhB,SAASkB,aA4C1DD,WAAa,2CACjBA,YAAcF,iBACdE,YAAcH,MAAQ,SAAWE,UA3CjCtB,4BAA2B,GA9BvBS,oBAAsB3D,aAAaM,KAAK,WAE5CL,eAAeK,KAAK,YAAaqD,qBACjCzD,oBAAoBI,KAAK,YAAaqD,8BAiFjCgB,gBAAgBf,IAAKgB,OACtBC,OAAOC,eAAe,iBAAmBD,OAAOE,cAGpD1G,IAAI8F,WAAWP,IAAK,oBAAoBQ,MAAK,SAASC,OAC9CW,QAAUX,EAAEpF,QAAQ,MAAO,KAC3B2F,QACAI,SAAW,KAAOJ,OAEtB3B,MAAM+B,qBAgCLC,8BACD1B,QAAU9D,UAAUyF,SAAS,mBAAmBC,OAEpC,KAAZ5B,SAA8B,cAAZA,UAElB9D,UAAUyF,SAAS,sBAAsB5E,KAAK,WAAY,YAG1DnC,EAAEiH,QAAQC,EAAEC,IAAIC,QAAU,qCACtB,CACIC,MAAOjC,QACPkC,SAAUhF,SACViF,QAASL,EAAEC,IAAII,UAEnB,SAAUC,YAENxH,EAAE,oCAAoCyH,QAClCD,QAAQE,QACRvC,2BAA2BC,QAASoC,SACpC/C,SAEAtE,aAAeiF,QACfpF,EAAE,kCAAkCyH,YAEnC,OACKE,qBAzGLC,aAAc/C,aACzB8C,YAAcjE,KAAKC,MAAMkB,cAC/B3E,IAAI8F,WAAW,kBAAmB,oBAAoBC,MAAK,SAASC,GAChEhG,IAAI8F,WAAW2B,YAAY7C,MAAO,mBAAoB8C,cAAc3B,MAAK,SAAS/F,KAC9EsG,gBAAgB,yBAA0BtG,SACtC2H,aAAe3B,EAAI,KACvB2B,cAAgB3H,IAAM,KACtB2H,cAAgB,aAAevF,SAAW,YAAcsF,aACxDvH,SAAS8B,KAAK,QAAS0F,oBAGxBF,YA8F6BG,CAAY1C,QAASoC,QAAQ3C,OAG7C1E,eAAiBiF,SAAiC,uBAAtBuC,YAAY9C,kBA4IrCkD,YAAaJ,YAAavC,SACjDlF,IAAI8F,WAAW,qBAAsB,mBACjC,CAAEgC,QAAUD,YAAaE,OAAS7C,QAAS8C,aAAeP,YAAYQ,SAC/DlC,MAAK,SAAS/F,KACrBF,EAAE,oCAAoCoI,OAAOpI,EAAE,MAAQE,IAAM,YA/I7CmI,CAAkBlI,aAAcwH,YAAavC,SAC7CpF,EAAE,sBAAsByD,IAAItD,mBAI1CmI,MAAK,WAIH9B,gBAAgB,2BAChBnG,SAAS8B,KAAK,QAAS,wCACvBjC,IAAI8F,WAAW,aAAc,oBAAoBC,MAAK,SAASC,GAC3D7F,SAAS8B,KAAK,QAAS+D,mBA4B9BqC,kCACDC,MAAQpH,SAAS2F,SAAS,mBAAmBC,OACjDhH,EAAEiH,QAAQC,EAAEC,IAAIC,QAAU,qCACtB,CACIhG,SAAUoH,MACVlB,SAAUhF,SACViF,QAASL,EAAEC,IAAII,UAEnB,SAAUkB,YAIFC,MAHAC,oBAAsB/F,aAAaa,MACnCmF,oBAAsB5I,EAAE,wBACxB6I,eAAiB7I,EAAE,iDAAmDyI,OAAOK,YAAc,aAE/FF,oBAAoBnB,QACpBmB,oBAAoBR,OAAOK,OAAOM,QACC,GAA/BN,OAAOO,cAAc9E,QAA8C,KAA/ByE,oBAAoBM,QACxDrG,aAAaa,IAAI,IACjBzD,EAAE,+BAA+BkJ,SAEE,GAA/BT,OAAOO,cAAc9E,SACrB0E,oBAAoBR,OAAOS,gBAC3BH,MAAQ1I,WArCSmJ,iBAEKC,MAAOrF,EADzCsC,KAAO,8DACPgD,KAAOF,YAAYG,kBACvBjD,MAAQ,WAAagD,KAAK,GAAK,YAAcA,KAAK,GAAK,YAAcA,KAAK,GAAK,eAC1EtF,EAAI,EAAGA,EAAIoF,YAAYH,cAAc9E,OAAQH,IAE9CsC,MAAQ,YADR+C,MAAQD,YAAYH,cAAcjF,IACP,GAAK,YAAcqF,MAAM,GAAK,YAAcA,MAAM,GAAK,sBAEtF/C,KAAQ,mBA6BkBkD,CAA4Bd,SACtCG,oBAAoBR,OAAOM,OAC3BA,MAAMQ,OACNL,eAAeW,OAAM,WACbX,eAAexC,QAAUoC,OAAOK,aAChCJ,MAAMe,OACNZ,eAAexC,KAAKoC,OAAOiB,eAE3BhB,MAAMQ,OACNL,eAAexC,KAAKoC,OAAOK,kBAIvC9I,EAAE,+BAA+ByJ,OAC7B9H,OAAOQ,KAAK,YACZU,MAAM,kBAAmB,WAIvCyF,MAAK,WAEH9B,gBAAgB,sCAQfmD,4BACkB,MAAnBnH,SAASiB,MACThB,aAAagH,OAEbhH,aAAayG,gBAQZU,iBACkB,QAAnBxI,SAASqC,OACTgB,SAiD2B,GAA/BrC,cAAcD,KAAK,WAEnBZ,iBAAiBsI,WAAW,UACO,GAA/BzH,cAAcD,KAAK,WAEnBjC,IAAI8F,WAAW,sBAAuB,oBAAoBC,MAAK,SAASC,GACpEpB,MAAMoB,MAEV9D,cAAcD,KAAK,YAAY,GAC/BP,UAAUO,KAAK,YAAY,oBArC3B2H,YAAc,KACY,KAFFpH,eAAeP,KAAK,WAG5C2H,YAAc9J,EAAE,MAAQ0C,eAAeP,KAAK,SAAW,QACvDnC,EAAE,kCAAkCoI,OAAO0B,cAsCnDC,GACApH,gBAAgBR,KAAK,UAErBhC,aAAemB,UAAUyF,SAAS,mBAAmBC,OAErDjC,2BAA2B7C,cACtBA,cAIDuC,SACAvE,IAAI8F,WAAW,mBAAoB,oBAAoBC,MAAK,SAASC,GACjE3D,oBAAoB8D,KAAK,MAAQH,EAAI,YAJzCY,0BAQJ6C,4BAEIhI,OAAOQ,KAAK,aACZU,MAAM,oBAAqB,OAC3BA,MAAM,kBAAmB,QAG7B0F,8BAIA3G,UAAUoI,GAAG,UAAU,WACApI,UAAUO,KAAK,WAG9B4C,4BAA2B,GAE3B7E,IAAI8F,WAAW,kBAAmB,oBAAoBC,MAAK,SAASC,GAC5DQ,OAAOuD,QAAQ/D,GACfnB,4BAA2B,GAE3BnD,UAAUO,KAAK,WAAW,SAM1ChB,QAAQ6I,GAAG,SAAUJ,gBACrB1I,SAAS8I,GAAG,UAAU,WAjGdrI,OAAOQ,KAAK,YACZU,MAAM,cAAe,OAkGzB+G,oBAGJtI,UAAU0I,GAAG,UAAU,WACfpI,UAAUO,KAAK,WAEfjC,IAAI8F,WAAW,wBAAyB,oBAAoBC,MAAK,SAAUC,GACnEQ,OAAOuD,QAAQ/D,IACfY,6BAIRA,6BAIRnF,OAAOqI,GAAG,UAAU,WACErI,OAAOQ,KAAK,YAE1BU,MAAM,cAAe,OACrBA,MAAM,oBAAqB,OAC3BA,MAAM,kBAAmB,SAEzBA,MAAM,cAAe,IACrBA,MAAM,oBAAqB,IAC3BA,MAAM,kBAAmB,QAIjCrB,mBAAmBwI,GAAG,UAAU,WACxBxI,mBAAmB0I,GAAG,aACtB1D,gBAAgB,iCAIxBpF,SAAS4I,GAAG,UAAU,WAClBvF,SACA8D,iCAGJ/F,SAASwH,GAAG,SAAUL,2BAGtBvH,cAAc4H,GAAG,UAAU,WACY,KAA/B5H,cAAcD,KAAK,SACnBZ,iBAAiB+B,KAAK,SAAU,KAEhC/B,iBAAiBsI,WAAW,aAOrB,IAAIM,kBAAkB,WACjC1F,YAEK2F,QAAQ/H,WAAWgI,IAAI,GAAI,aAAe,IAInDrK,EAAE,iCAAiCwJ,OAAM,eACjCc,OAAStK,EAAEuK,MAAMC,KAAK,sBACtBC,WAAaH,OAAOhH,KAAK,MAAMxC,QAAQ,UAAW,IACtDd,EAAE,gBAAkByK,YAAYhH,IAAI6G,OAAOtD,QAC3ChH,EAAE,qBAAuByK,YAAYpE,KAAKiE,OAAOtD,QACjDhH,EAAE,YAAcyK,YAAYC,SAAS,SACrC1K,EAAEuK,MAAMpI,KAAK,YAAY,MAI7BnC,EAAE,gBAAgBwJ,OAAM,WACpBlI,UAAUa,KAAK,YAAY"}
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js b/amd/build/ui_scratchpad.min.js
new file mode 100644
index 000000000..1efe992e7
--- /dev/null
+++ b/amd/build/ui_scratchpad.min.js
@@ -0,0 +1,57 @@
+define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_templates,_userinterfacewrapper,_outputdisplayarea){var obj;
+/**
+ * Implementation of the scratchpad_ui user interface plugin. For overall details
+ * of the UI plugin architecture, see userinterfacewrapper.js.
+ *
+ * This plugin replaces the usual textarea answer element with a UI is designed to
+ * allow the execution of code in the CodeRunner question in a manner similar to an IDE.
+ * It contains two editor boxes, one on top of another, allowing users to enter and
+ * edit code in both. It contains two embedded Ace UIs.
+ * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,
+ * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,
+ * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that
+ * provides information about how to use the Scratchpad.
+ * It's possible to run code 'in-browser' by clicking the Run Button,
+ * without making a submission via the Check Button:
+ * If Prefix with Answer is not checked, only the code in the Scratchpad is run --
+ * allowing for a rough working spot to quickly check the result of code.
+ * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is
+ * appended to the code in the first editor before being run.
+ * The Run Button has some limitations when using its default configuration:
+ * Does not support programs that use STDIN (by default);
+ * Only supports textual STDOUT (by default).
+ * Note: These features can be supported, see the README section on wrappers...
+ * The serialisation of this UI is a JSON object with the fields
+ * with fields:
+ * answer_code: [""] A list containing a string with answer code from the first editor;
+ * test_code: [""] A list containing a string with containing answer code from the second editor;
+ * show_hide: ["1"] when scratchpad is visible, otherwise [""];
+ * prefix_ans: ["1"] when Prefix with Answer is checked, otherwise [""].
+ *
+ * UI Parameters:
+ * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.
+ * - button_name: run button text.
+ * - prefix_name: prefix with answer check-box label text.
+ * - help_text: help text to show.
+ * - run_lang: language used to run code when the run button is clicked,
+ * this should be the language your wrapper is written in (if applicable).
+ * - wrapper_src: location of wrapper code to be used by the run button, if applicable:
+ * setting to globalextra will use text in global extra field,
+ * - prototypeextra will use the prototype extra field.
+ * - output_display_mode: control how program output is displayed on runs, there are three modes:
+ * - text: display program output as text, html escaped;
+ * - json: display program output, when it is json,
+ * - html: display program output as raw html.
+ * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...
+ * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI
+ * from student perspective.
+ * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.
+ * This can be used to swap the default state.
+ *
+ * @module qtype_coderunner/ui_scratchpad
+ * @copyright Richard Lobb, 2022, The University of Canterbury
+ * @copyright James Napier, 2022, The University of Canterbury
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Constructor=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};const invertSerial=current=>"1"===current[0]?[""]:["1"],overwriteValues=(defaults,prescribed)=>{let overwritten={...defaults};if(prescribed)for(const[key,value]of Object.entries(defaults))overwritten[key]=prescribed[key]||value;return overwritten};_exports.Constructor=class{constructor(textAreaId,width,height,uiParams){const DEF_UI_PARAMS={scratchpad_name:"",button_name:"",prefix_name:"",help_text:"",params:{},run_lang:uiParams.lang,output_display_mode:"text",disable_scratchpad:!1,wrapper_src:null};this.textArea=document.getElementById(textAreaId),this.textAreaId=textAreaId,this.height=height,this.readOnly=this.textArea.readonly,this.fail=!1,this.outerDiv=null,this.outputDisplay=null,this.invertPreload=uiParams.invert_prefix,this.lang=uiParams.lang,this.numRows=this.textArea.rows,this.uiParams=overwriteValues(DEF_UI_PARAMS,uiParams),this.runWrapper=this.getRunWrapper(),this.reload()}getRunWrapper(){const wrapperSrc=this.uiParams.wrapper_src;let runWrapper=null;return wrapperSrc&&("globalextra"===wrapperSrc||"prototypeextra"===wrapperSrc?runWrapper=this.textArea.dataset[wrapperSrc]:(this.fail=!0,this.failString="scratchpad_ui_badrunwrappersrc")),runWrapper}failed(){return this.fail}failMessage(){return this.failString}sync(){if(!this.context)return;const serialisation=this.getSerialisation();this.setSerialisation(serialisation)}getSerialisation(){const prefixAns=document.getElementById(this.context.prefix_ans.id),showHide=document.getElementById(this.context.show_hide.id);let serialisation={answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:[""]};return this.answerTextarea&&(serialisation.answer_code=[this.answerTextarea.value]),this.testTextarea&&(serialisation.test_code=[this.testTextarea.value]),showHide&&!(el=>{if(!el.classList.contains("collapse"))throw Error("Element does not have collapse class");return!el.classList.contains("show")})(showHide)&&(serialisation.show_hide=["1"]),(null!=prefixAns&&prefixAns.checked||this.context.disable_scratchpad)&&(serialisation.prefix_ans=["1"]),this.invertPreload&&(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans)),serialisation}setSerialisation(serialisation){serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),Object.values(serialisation).some((val=>1===val.length&&val[0].length>0))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}getElement(){return this.outerDiv}handleRunButtonClick(){if(null===this.outputDisplay)return;this.sync();const preloadString=this.textArea.value,serial=this.readJson(preloadString),code=(answerCode=serial.answer_code,testCode=serial.test_code,prefixAns=serial.prefix_ans[0],(template=this.runWrapper)||(template="{{ ANSWER_CODE }}\n{{ SCRATCHPAD_CODE }}"),prefixAns||(answerCode=""),(template=template.replaceAll("{{ ANSWER_CODE }}",answerCode)).replaceAll("{{ SCRATCHPAD_CODE }}",testCode));var answerCode,testCode,prefixAns,template;this.outputDisplay.runCode(code,"",!0)}updateContext(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}readJson(preloadString){let serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch{serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}async reload(){const preloadString=this.textArea.value;let preload;try{preload=this.readJson(preloadString)}catch(error){return this.fail=!0,void(this.failString="scratchpad_ui_invalidserialisation")}this.updateContext(preload);try{const{html:html}=await _templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners()}catch(e){this.fail=!0,this.failString="scratchpad_ui_templateloadfail"}}drawUi(html){const wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}addAceUis(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}addEventListeners(){const runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(()=>this.handleRunButtonClick()))}resize(){}hasFocus(){var _this$answerCodeUi,_this$testCodeUi;let focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}destroy(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}}));
+
+//# sourceMappingURL=ui_scratchpad.min.js.map
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js.map b/amd/build/ui_scratchpad.min.js.map
new file mode 100644
index 000000000..9b516018b
--- /dev/null
+++ b/amd/build/ui_scratchpad.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text.\n * @param {string} testCode text.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template) => {\n if (!template) {\n template = '{{ ANSWER_CODE }}\\n' +\n '{{ SCRATCHPAD_CODE }}';\n }\n if (!prefixAns) {\n answerCode = '';\n }\n template = template.replaceAll('{{ ANSWER_CODE }}', answerCode);\n template = template.replaceAll('{{ SCRATCHPAD_CODE }}', testCode);\n return template;\n};\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the defualt value.\n * Does not add keys/values to the result if that key is not in defualts.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!el.classList.contains('collapse')) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n let serialisation = {\n answer_code: [''],\n test_code: [''],\n show_hide: [''],\n prefix_ans: ['']\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const code = fillWrapper(\n serial.answer_code,\n serial.test_code,\n serial.prefix_ans[0],\n this.runWrapper\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","overwriteValues","defaults","prescribed","overwritten","key","value","Object","entries","constructor","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","reload","wrapperSrc","dataset","failString","failed","failMessage","sync","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","test_code","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","checked","values","some","val","length","JSON","stringify","getElement","handleRunButtonClick","preloadString","serial","readJson","code","answerCode","testCode","template","replaceAll","runCode","updateContext","preload","text","render","CSS","escape","parse","hasOwnProperty","TypeError","error","html","Templates","renderForPromise","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","e","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","hasFocus","focused","_this$answerCodeUi","uiInstance","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
diff --git a/classes/bulk_tester.php b/classes/bulk_tester.php
index 774c547ba..3c9116832 100644
--- a/classes/bulk_tester.php
+++ b/classes/bulk_tester.php
@@ -289,8 +289,7 @@ private function test_question($question) {
core_php_time_limit::raise(60); // Prevent PHP timeouts.
gc_collect_cycles(); // Because PHP's default memory management is rubbish.
$question->start_attempt(null);
- $answer = $question->answer;
- $response = array('answer' => $answer);
+ $response = $question->get_correct_answer();
// Check if it's a multilanguage question; if so need to determine
// what language (either specified by answer_language template param, or
// the AceLang default or the first).
diff --git a/classes/coderunner_files.php b/classes/coderunner_files.php
new file mode 100644
index 000000000..339365ecc
--- /dev/null
+++ b/classes/coderunner_files.php
@@ -0,0 +1,74 @@
+.
+
+/**
+ * CodeRunner files class which implements question_response_files to
+ * allow access to files.
+ *
+ * @package qtype_coderunner
+ * @copyright Richard Lobb, 2021, The University of Canterbury
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace qtype_coderunner;
+
+/**
+ * This class implements {@link question_response_files} interface which allows
+ * bulk testing and previews to use sample answer files by retrieving files associated
+ * with a particular question. Can also be constructed with a different filearea
+ * for future use.
+ */
+class coderunner_files implements \question_response_files {
+ /** @var type The contextid of the question. */
+ protected $contextid;
+ /** @var string The string name of the filearea */
+ protected $filearea;
+ /** @var int The question id number. */
+ protected $questionid;
+
+ /**
+ * Constructor.
+ *
+ * @param type $contextid The contextid of the question.
+ * @param string $filearea The name of the filearea.
+ * @param int $questionid The id of the question.
+ */
+ public function __construct($contextid, $filearea, $questionid) {
+ $this->contextid = $contextid;
+ $this->filearea = $filearea;
+ $this->questionid = $questionid;
+ }
+
+ /**
+ * Returns all the CodeRunner files in the specified filearea, questionid and
+ * contextid.
+ *
+ * @return array of files An array of all the files requested.
+ */
+ public function get_files() {
+ $fs = get_file_storage();
+ return $fs->get_area_files($this->contextid, 'qtype_coderunner', $this->filearea, $this->questionid);
+ }
+
+ /**
+ * Returns a string for the object to satisfy other Moodle checks.
+ *
+ * @return string A string for the object.
+ */
+ public function __toString() {
+ return "coderunnerquestionresponsefiles";
+ }
+}
\ No newline at end of file
diff --git a/question.php b/question.php
index 7912be98e..252ca9abc 100644
--- a/question.php
+++ b/question.php
@@ -30,6 +30,7 @@
require_once($CFG->dirroot . '/question/type/coderunner/questiontype.php');
use qtype_coderunner\constants;
+use qtype_coderunner\coderunner_files;
/**
* Represents a 'CodeRunner' question.
@@ -469,6 +470,10 @@ public function get_correct_answer() {
return null;
} else {
$answer = array('answer' => $this->answer);
+ // Get any sample question files first.
+ $context = qtype_coderunner::question_context($this);
+ $contextid = $context->id;
+ $answer['attachments'] = new coderunner_files($contextid, 'samplefile', $this->id);
// For multilanguage questions we also need to specify the language.
// Use the answer_language template parameter value if given, otherwise
// run with the default.
@@ -581,7 +586,9 @@ private function get_attached_files($response) {
if (array_key_exists('attachments', $response) && $response['attachments']) {
$files = $response['attachments']->get_files();
foreach ($files as $file) {
- $attachments[$file->get_filename()] = $file->get_content();
+ if ($file->get_filename() !== ".") {
+ $attachments[$file->get_filename()] = $file->get_content();
+ }
}
}
return $attachments;
@@ -894,7 +901,6 @@ public function get_prototype() {
}
}
-
/**
* Return an associative array mapping filename to file contents
* for all the support files for the given question.
diff --git a/questiontestrun.php b/questiontestrun.php
index dc2a0b3ab..567507304 100644
--- a/questiontestrun.php
+++ b/questiontestrun.php
@@ -95,8 +95,8 @@
$options->suppressruntestslink = true;
// Test the question with its sample answer.
-$answer = $question->answer;
-$runparams = array('-submit' => 'Submit', 'answer' => $answer);
+$response = $question->get_correct_response();
+$runparams = array('-submit' => 'Submit', 'answer' => $response['answer'], 'attachments' => $response['attachments']);
$templateparams = isset($question->templateparams) ? json_decode($question->templateparams, true) : array();
if (isset($templateparams['answer_language'])) {
$runparams['language'] = $templateparams['answer_language'];
From ec8bd3f470fddad335a0d5ea293682c3fd60d34b Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Thu, 2 Feb 2023 12:56:46 +1300
Subject: [PATCH 052/188] Update all JavaScript build files
---
amd/build/ajaxquestionloader.min.js.map | 1 +
amd/build/authorform.min.js | 2 +-
amd/build/authorform.min.js.map | 1 +
amd/build/graphelements.min.js.map | 1 +
amd/build/graphutil.min.js | 2 +-
amd/build/graphutil.min.js.map | 1 +
amd/build/multilanguagequestion.min.js.map | 1 +
amd/build/outputdisplayarea.min.js | 7 +--
amd/build/outputdisplayarea.min.js.map | 1 +
amd/build/resetbutton.min.js.map | 1 +
amd/build/showdiff.min.js.map | 1 +
amd/build/textareas.min.js | 2 +-
amd/build/textareas.min.js.map | 1 +
amd/build/ui_ace.min.js | 2 +-
amd/build/ui_ace.min.js.map | 1 +
amd/build/ui_ace_gapfiller.min.js | 2 +-
amd/build/ui_ace_gapfiller.min.js.map | 1 +
amd/build/ui_gapfiller.min.js.map | 1 +
amd/build/ui_graph.min.js.map | 1 +
amd/build/ui_html.min.js.map | 1 +
amd/build/ui_scratchpad.min.js | 53 +---------------------
amd/build/ui_scratchpad.min.js.map | 1 +
amd/build/ui_table.min.js.map | 1 +
amd/build/userinterfacewrapper.min.js | 2 +-
amd/build/userinterfacewrapper.min.js.map | 1 +
25 files changed, 25 insertions(+), 64 deletions(-)
create mode 100644 amd/build/ajaxquestionloader.min.js.map
create mode 100644 amd/build/authorform.min.js.map
create mode 100644 amd/build/graphelements.min.js.map
create mode 100644 amd/build/graphutil.min.js.map
create mode 100644 amd/build/multilanguagequestion.min.js.map
create mode 100644 amd/build/outputdisplayarea.min.js.map
create mode 100644 amd/build/resetbutton.min.js.map
create mode 100644 amd/build/showdiff.min.js.map
create mode 100644 amd/build/textareas.min.js.map
create mode 100644 amd/build/ui_ace.min.js.map
create mode 100644 amd/build/ui_ace_gapfiller.min.js.map
create mode 100644 amd/build/ui_gapfiller.min.js.map
create mode 100644 amd/build/ui_graph.min.js.map
create mode 100644 amd/build/ui_html.min.js.map
create mode 100644 amd/build/ui_scratchpad.min.js.map
create mode 100644 amd/build/ui_table.min.js.map
create mode 100644 amd/build/userinterfacewrapper.min.js.map
diff --git a/amd/build/ajaxquestionloader.min.js.map b/amd/build/ajaxquestionloader.min.js.map
new file mode 100644
index 000000000..a39d8300d
--- /dev/null
+++ b/amd/build/ajaxquestionloader.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ajaxquestionloader.min.js","sources":["../src/ajaxquestionloader.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 * JavaScript for filling in the question text with the contents of one\n * of the question's support files. Intended primarily for program contest\n * problems, where the support file is a single domjudge or ICPC problem zip,\n * with the problem spec within it.\n *\n * @module qtype_coderunner/ajaxquestionloader\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function ($) {\n /**\n * Append to the question text div in the question a data-URL containing\n * the contents of the question specification file (usu. a pdf).\n * @param {int} qid The question ID in the database.\n * @param {string} divId The ID of the question text
element.\n * @param {string} questionFilename The name of the problem spec file within\n * the problem zip file.\n */\n function loadQuestionText(qid, divId, questionFilename) {\n var questionTextDiv = $('#' + divId),\n errorDiv = '
Failed to load problem spec
';\n if (questionTextDiv.length != 1) {\n questionTextDiv.append(errorDiv);\n return;\n }\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/problemspec.php',\n {\n questionid: qid,\n sesskey: M.cfg.sesskey,\n filename: questionFilename\n },\n function (response) {\n if (response.filecontentsb64) {\n\n questionTextDiv.append(\n '
")}))):loadCustomisationFields(),set_testtype_visibilities(),useace.prop("checked")&&(setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")),loadUiParametersDescription(),customise.on("change",(function(){customise.prop("checked")?setCustomisationVisibility(!0):str.get_string("confirm_proceed","qtype_coderunner").then((function(s){window.confirm(s)?setCustomisationVisibility(!1):customise.prop("checked",!0)}))})),acelang.on("change",check_ace_lang),language.on("change",(function(){useace.prop("checked")&&setUi("id_template","ace"),check_ace_lang()})),typeCombo.on("change",(function(){customise.prop("checked")?str.get_string("question_type_changed","qtype_coderunner").then((function(s){window.confirm(s)&&loadCustomisationFields()})):loadCustomisationFields()})),useace.on("change",(function(){useace.prop("checked")?(setUi("id_template","ace"),setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")):(setUi("id_template",""),setUi("id_templateparams",""),setUi("id_uiparameters",""))})),evaluatePerStudent.on("change",(function(){evaluatePerStudent.is(":checked")&&langStringAlert("templateparamsusingsandbox")})),uiplugin.on("change",(function(){setUis(),loadUiParametersDescription()})),precheck.on("change",set_testtype_visibilities),prototypeType.on("change",(function(){"0"==prototypeType.prop("value")?prototypeDisplay.attr("hidden","1"):prototypeDisplay.removeAttr("hidden")})),new MutationObserver((function(){setUis()})).observe(preloadHdr.get(0),{attributes:!0}),$("button.replaceexpectedwithgot").click((function(){var gotPre=$(this).prev('pre[id^="id_got_"]'),testCaseId=gotPre.attr("id").replace("id_got_","");$("#id_expected_"+testCaseId).val(gotPre.text()),$("#id_fail_expected_"+testCaseId).html(gotPre.text()),$(".failrow_"+testCaseId).addClass("fixed"),$(this).prop("disabled",!0)})),$(".btn-primary").click((function(){typeCombo.prop("disabled",!1)}))}}}));
//# sourceMappingURL=authorform.min.js.map
\ No newline at end of file
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
new file mode 100644
index 000000000..3d74f47c6
--- /dev/null
+++ b/amd/build/authorform.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n */\n function setUis() {\n var uiname = uiplugin.val();\n var enableUi = true;\n if (uiname === 'html' && uiparameters.val().trim() !== '') {\n try {\n var uiparams = JSON.parse(uiparameters.val());\n if (uiparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","messagePara","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","enableUi","trim","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/graphelements.min.js.map b/amd/build/graphelements.min.js.map
new file mode 100644
index 000000000..8487227c5
--- /dev/null
+++ b/amd/build/graphelements.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"graphelements.min.js","sources":["../src/graphelements.js"],"sourcesContent":["/******************************************************************************\n *\n * A module for use by ui_graph, defining classes Node, Link, SelfLink,\n * StartLink and TemporaryLink\n *\n * @module qtype_coderunner/graphelements\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// This code is a modified version of Finite State Machine Designer\n// (http://madebyevan.com/fsm/)\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(['qtype_coderunner/graphutil'], function(util) {\n\n /**\n * Define a class Node that represents a node in a graph\n * @param {object} parent The Graph to which this node belongs.\n * @param {int} x The x-coordinate of the node.\n * @param {int} y The y-coordinate of the node.\n *\n */\n function Node(parent, x, y) {\n this.parent = parent; // The ui_graph instance.\n this.x = x;\n this.y = y;\n this.mouseOffsetX = 0;\n this.mouseOffsetY = 0;\n this.isAcceptState = false;\n this.textBox = new TextBox('', this);\n this.caretPosition = 0;\n }\n\n // At the start of a drag, record our position relative to the mouse.\n Node.prototype.setMouseStart = function(mouseX, mouseY) {\n this.mouseOffsetX = this.x - mouseX;\n this.mouseOffsetY = this.y - mouseY;\n };\n\n Node.prototype.setAnchorPoint = function(x, y) {\n this.x = x + this.mouseOffsetX;\n this.y = y + this.mouseOffsetY;\n };\n\n // Given a new mouse position during a drag, move to the appropriate\n // new position.\n Node.prototype.trackMouse = function(mouseX, mouseY) {\n this.x = this.mouseOffsetX + mouseX;\n this.y = this.mouseOffsetY + mouseY;\n };\n\n Node.prototype.draw = function(c) {\n // Draw the circle.\n c.beginPath();\n c.arc(this.x, this.y, this.parent.nodeRadius(), 0, 2 * Math.PI, false);\n c.stroke();\n\n // Draw the text.\n this.textBox.draw(this.x, this.y, null, this);\n\n // Draw a double circle for an accept state.\n if(this.isAcceptState) {\n c.beginPath();\n c.arc(this.x, this.y, this.parent.nodeRadius() - 6, 0, 2 * Math.PI, false);\n c.stroke();\n }\n };\n\n Node.prototype.closestPointOnCircle = function(x, y) {\n var dx = x - this.x;\n var dy = y - this.y;\n var scale = Math.sqrt(dx * dx + dy * dy);\n return {\n 'x': this.x + dx * this.parent.nodeRadius() / scale,\n 'y': this.y + dy * this.parent.nodeRadius() / scale,\n };\n };\n\n Node.prototype.containsPoint = function(x, y) {\n return (x - this.x) * (x - this.x) + (y - this.y) * (y - this.y) < this.parent.nodeRadius() * this.parent.nodeRadius();\n };\n\n // Method of a Node that, given a list of all links in a graph, returns\n // a list of any nodes that contain a link to this node (excluding StartLinks\n // and SelfLinks).\n Node.prototype.neighbours = function(links) {\n var neighbours = [], link;\n for (var i = 0; i < links.length; i++) {\n link = links[i];\n if (link instanceof Link) { // Exclude SelfLinks and StartLinks.\n if (link.nodeA === this && !neighbours.includes(link.nodeB)) {\n neighbours.push(link.nodeB);\n } else if (link.nodeB === this && !neighbours.includes(link.nodeA)) {\n neighbours.push(link.nodeA);\n }\n }\n }\n return neighbours;\n };\n\n // Method of Node that traverses a graph defined by a given set of links\n // starting at 'this' node and updating the visited list for each new\n // node. Returns the updated visited list, which (for the root call)\n // is a list of all nodes connected to the given start node.\n Node.prototype.traverseGraph = function(links, visited) {\n var neighbours,\n neighbour;\n if (!visited.includes(this)) {\n visited.push(this);\n neighbours = this.neighbours(links);\n for (var i = 0; i < neighbours.length; i++) {\n neighbour = neighbours[i];\n if (!visited.includes(neighbour)) {\n neighbour.traverseGraph(links, visited);\n }\n }\n }\n return visited;\n };\n\n /**\n * Define a class Link that represents a connection between two nodes.\n * @param {object} parent The graph to which this link belongs.\n * @param {object} a The node at one end of the link.\n * @param {object} b The node at the other end of the link.\n *\n */\n function Link(parent, a, b) {\n this.parent = parent; // The parent ui_digraph instance.\n this.nodeA = a;\n this.nodeB = b;\n this.textBox = new TextBox('', this);\n this.lineAngleAdjust = 0; // Value to add to textAngle when link is straight line.\n this.caretPosition = 0;\n\n // Make anchor point relative to the locations of nodeA and nodeB.\n this.parallelPart = 0.5; // Percentage from nodeA to nodeB.\n this.perpendicularPart = 0; // Pixels from line between nodeA and nodeB.\n }\n\n Link.prototype.getAnchorPoint = function() {\n var dx = this.nodeB.x - this.nodeA.x;\n var dy = this.nodeB.y - this.nodeA.y;\n var scale = Math.sqrt(dx * dx + dy * dy);\n return {\n 'x': this.nodeA.x + dx * this.parallelPart - dy * this.perpendicularPart / scale,\n 'y': this.nodeA.y + dy * this.parallelPart + dx * this.perpendicularPart / scale\n };\n };\n\n Link.prototype.setAnchorPoint = function(x, y) {\n var dx = this.nodeB.x - this.nodeA.x;\n var dy = this.nodeB.y - this.nodeA.y;\n var scale = Math.sqrt(dx * dx + dy * dy);\n this.parallelPart = (dx * (x - this.nodeA.x) + dy * (y - this.nodeA.y)) / (scale * scale);\n this.perpendicularPart = (dx * (y - this.nodeA.y) - dy * (x - this.nodeA.x)) / scale;\n // Snap to a straight line.\n if(this.parallelPart > 0 && this.parallelPart < 1 && Math.abs(this.perpendicularPart) < this.parent.SNAP_TO_PADDING) {\n this.lineAngleAdjust = (this.perpendicularPart < 0) * Math.PI;\n this.perpendicularPart = 0;\n }\n };\n\n Link.prototype.getEndPointsAndCircle = function() {\n if(this.perpendicularPart === 0) {\n var midX = (this.nodeA.x + this.nodeB.x) / 2;\n var midY = (this.nodeA.y + this.nodeB.y) / 2;\n var start = this.nodeA.closestPointOnCircle(midX, midY);\n var end = this.nodeB.closestPointOnCircle(midX, midY);\n return {\n 'hasCircle': false,\n 'startX': start.x,\n 'startY': start.y,\n 'endX': end.x,\n 'endY': end.y,\n };\n }\n var anchor = this.getAnchorPoint();\n var circle = util.circleFromThreePoints(this.nodeA.x, this.nodeA.y, this.nodeB.x, this.nodeB.y, anchor.x, anchor.y);\n var isReversed = (this.perpendicularPart > 0);\n var reverseScale = isReversed ? 1 : -1;\n var rRatio = reverseScale * this.parent.nodeRadius() / circle.radius;\n var startAngle = Math.atan2(this.nodeA.y - circle.y, this.nodeA.x - circle.x) - rRatio;\n var endAngle = Math.atan2(this.nodeB.y - circle.y, this.nodeB.x - circle.x) + rRatio;\n var startX = circle.x + circle.radius * Math.cos(startAngle);\n var startY = circle.y + circle.radius * Math.sin(startAngle);\n var endX = circle.x + circle.radius * Math.cos(endAngle);\n var endY = circle.y + circle.radius * Math.sin(endAngle);\n return {\n 'hasCircle': true,\n 'startX': startX,\n 'startY': startY,\n 'endX': endX,\n 'endY': endY,\n 'startAngle': startAngle,\n 'endAngle': endAngle,\n 'circleX': circle.x,\n 'circleY': circle.y,\n 'circleRadius': circle.radius,\n 'reverseScale': reverseScale,\n 'isReversed': isReversed,\n };\n };\n\n Link.prototype.draw = function(c) {\n var linkInfo = this.getEndPointsAndCircle(), textX, textY, textAngle, relDist;\n // Draw arc.\n c.beginPath();\n if(linkInfo.hasCircle) {\n c.arc(linkInfo.circleX,\n linkInfo.circleY,\n linkInfo.circleRadius,\n linkInfo.startAngle,\n linkInfo.endAngle,\n linkInfo.isReversed);\n } else {\n c.moveTo(linkInfo.startX, linkInfo.startY);\n c.lineTo(linkInfo.endX, linkInfo.endY);\n }\n c.stroke();\n // Draw the head of the arrow.\n if(linkInfo.hasCircle) {\n this.parent.arrowIfReqd(c,\n linkInfo.endX,\n linkInfo.endY,\n linkInfo.endAngle - linkInfo.reverseScale * (Math.PI / 2));\n } else {\n this.parent.arrowIfReqd(c,\n linkInfo.endX,\n linkInfo.endY,\n Math.atan2(linkInfo.endY - linkInfo.startY, linkInfo.endX - linkInfo.startX));\n }\n // Draw the text.\n relDist = this.textBox.relDist;\n if(linkInfo.hasCircle) {\n var startAngle = linkInfo.startAngle;\n var endAngle = linkInfo.endAngle;\n if (endAngle < startAngle) {\n endAngle += Math.PI * 2;\n }\n\n textAngle = ((1 - relDist) * startAngle + relDist * endAngle);\n if (linkInfo.isReversed){\n textAngle += (1 - relDist) * (2 * Math.PI); // Reflect text across the line between the link points\n }\n textX = linkInfo.circleX + linkInfo.circleRadius * Math.cos(textAngle);\n textY = linkInfo.circleY + linkInfo.circleRadius * Math.sin(textAngle);\n this.textBox.draw(textX, textY, textAngle, this);\n } else {\n textX = ((1 - relDist) * linkInfo.startX + relDist * linkInfo.endX);\n textY = ((1 - relDist) * linkInfo.startY + relDist * linkInfo.endY);\n textAngle = Math.atan2(linkInfo.endX - linkInfo.startX, linkInfo.startY - linkInfo.endY);\n this.textBox.draw(textX, textY, textAngle + this.lineAngleAdjust, this);\n }\n };\n\n Link.prototype.containsPoint = function(x, y) {\n var linkInfo = this.getEndPointsAndCircle(), dx, dy, distance;\n if(linkInfo.hasCircle) {\n dx = x - linkInfo.circleX;\n dy = y - linkInfo.circleY;\n distance = Math.sqrt(dx * dx + dy * dy) - linkInfo.circleRadius;\n if(Math.abs(distance) < this.parent.HIT_TARGET_PADDING) {\n var angle = Math.atan2(dy, dx);\n var startAngle = linkInfo.startAngle;\n var endAngle = linkInfo.endAngle;\n if(linkInfo.isReversed) {\n var temp = startAngle;\n startAngle = endAngle;\n endAngle = temp;\n }\n if(endAngle < startAngle) {\n endAngle += Math.PI * 2;\n }\n if(angle < startAngle) {\n angle += Math.PI * 2;\n } else if(angle > endAngle) {\n angle -= Math.PI * 2;\n }\n return (angle > startAngle && angle < endAngle);\n }\n } else {\n dx = linkInfo.endX - linkInfo.startX;\n dy = linkInfo.endY - linkInfo.startY;\n var length = Math.sqrt(dx * dx + dy * dy);\n var percent = (dx * (x - linkInfo.startX) + dy * (y - linkInfo.startY)) / (length * length);\n distance = (dx * (y - linkInfo.startY) - dy * (x - linkInfo.startX)) / length;\n return (percent > 0 && percent < 1 && Math.abs(distance) < this.parent.HIT_TARGET_PADDING);\n }\n return false;\n };\n\n /**\n * Define a class SelfLink that represents a connection from a node back\n * to itself.\n * @param {object} parent The graph to which this link belongs.\n * @param {object} node The node the link emerges from and returns to.\n * @param {object} mouse The current position of the mouse that's defining\n * the position of the self-link.\n */\n function SelfLink(parent, node, mouse) {\n this.parent = parent;\n this.node = node;\n this.anchorAngle = 0;\n this.mouseOffsetAngle = 0;\n this.textBox = new TextBox('', this);\n\n if(mouse) {\n this.setAnchorPoint(mouse.x, mouse.y);\n }\n }\n\n SelfLink.prototype.setMouseStart = function(x, y) {\n this.mouseStartX = x;\n this.mouseStartY = y;\n };\n\n SelfLink.prototype.setAnchorPoint = function(x, y) {\n this.anchorAngle = Math.atan2(y - this.node.y, x - this.node.x) + this.mouseOffsetAngle;\n // Snap to 90 degrees.\n var snap = Math.round(this.anchorAngle / (Math.PI / 2)) * (Math.PI / 2);\n if(Math.abs(this.anchorAngle - snap) < 0.1) {\n this.anchorAngle = snap;\n }\n // Keep in the range -pi to pi so our containsPoint() function always works.\n if(this.anchorAngle < -Math.PI) {\n this.anchorAngle += 2 * Math.PI;\n }\n if(this.anchorAngle > Math.PI) {\n this.anchorAngle -= 2 * Math.PI;\n }\n };\n\n SelfLink.prototype.getEndPointsAndCircle = function() {\n var circleX = this.node.x + 1.5 * this.parent.nodeRadius() * Math.cos(this.anchorAngle);\n var circleY = this.node.y + 1.5 * this.parent.nodeRadius() * Math.sin(this.anchorAngle);\n var circleRadius = 0.75 * this.parent.nodeRadius();\n var startAngle = this.anchorAngle - Math.PI * 0.8;\n var endAngle = this.anchorAngle + Math.PI * 0.8;\n var startX = circleX + circleRadius * Math.cos(startAngle);\n var startY = circleY + circleRadius * Math.sin(startAngle);\n var endX = circleX + circleRadius * Math.cos(endAngle);\n var endY = circleY + circleRadius * Math.sin(endAngle);\n return {\n 'hasCircle': true,\n 'startX': startX,\n 'startY': startY,\n 'endX': endX,\n 'endY': endY,\n 'startAngle': startAngle,\n 'endAngle': endAngle,\n 'circleX': circleX,\n 'circleY': circleY,\n 'circleRadius': circleRadius\n };\n };\n\n SelfLink.prototype.draw = function(c) {\n var linkInfo = this.getEndPointsAndCircle();\n // Draw arc.\n c.beginPath();\n c.arc(linkInfo.circleX, linkInfo.circleY, linkInfo.circleRadius, linkInfo.startAngle, linkInfo.endAngle, false);\n c.stroke();\n // Draw the text on the loop.\n var relDist = this.textBox.relDist;\n var textAngle = linkInfo.startAngle * (1 - relDist) + linkInfo.endAngle * relDist;\n var textX = linkInfo.circleX + linkInfo.circleRadius * Math.cos(textAngle);\n var textY = linkInfo.circleY + linkInfo.circleRadius * Math.sin(textAngle);\n this.textBox.draw(textX, textY, textAngle, this);\n // Draw the head of the arrow.\n this.parent.arrowIfReqd(c, linkInfo.endX, linkInfo.endY, linkInfo.endAngle + Math.PI * 0.4);\n };\n\n SelfLink.prototype.containsPoint = function(x, y) {\n var linkInfo = this.getEndPointsAndCircle();\n var dx = x - linkInfo.circleX;\n var dy = y - linkInfo.circleY;\n var distance = Math.sqrt(dx * dx + dy * dy) - linkInfo.circleRadius;\n return (Math.abs(distance) < this.parent.HIT_TARGET_PADDING);\n };\n\n /**\n * Define a class StartLink that represents a start link in a finite\n * state machine. Not useful in general digraphs.\n * @param {object} parent The graph to which this link belongs.\n * @param {node} node The node that the link leads into.\n * @param {object} start The point at the open end of the link.\n */\n function StartLink(parent, node, start) {\n this.parent = parent;\n this.node = node;\n this.deltaX = 0;\n this.deltaY = 0;\n\n if(start) {\n this.setAnchorPoint(start.x, start.y);\n }\n }\n\n StartLink.prototype.setAnchorPoint = function(x, y) {\n this.deltaX = x - this.node.x;\n this.deltaY = y - this.node.y;\n\n if(Math.abs(this.deltaX) < this.parent.SNAP_TO_PADDING) {\n this.deltaX = 0;\n }\n\n if(Math.abs(this.deltaY) < this.parent.SNAP_TO_PADDING) {\n this.deltaY = 0;\n }\n };\n\n StartLink.prototype.getEndPoints = function() {\n var startX = this.node.x + this.deltaX;\n var startY = this.node.y + this.deltaY;\n var end = this.node.closestPointOnCircle(startX, startY);\n return {\n 'startX': startX,\n 'startY': startY,\n 'endX': end.x,\n 'endY': end.y,\n };\n };\n\n StartLink.prototype.draw = function(c) {\n var endPoints = this.getEndPoints();\n\n // Draw the line.\n c.beginPath();\n c.moveTo(endPoints.startX, endPoints.startY);\n c.lineTo(endPoints.endX, endPoints.endY);\n c.stroke();\n\n // Draw the head of the arrow.\n this.parent.arrowIfReqd(c, endPoints.endX, endPoints.endY, Math.atan2(-this.deltaY, -this.deltaX));\n };\n\n StartLink.prototype.containsPoint = function(x, y) {\n var endPoints = this.getEndPoints();\n var dx = endPoints.endX - endPoints.startX;\n var dy = endPoints.endY - endPoints.startY;\n var length = Math.sqrt(dx * dx + dy * dy);\n var percent = (dx * (x - endPoints.startX) + dy * (y - endPoints.startY)) / (length * length);\n var distance = (dx * (y - endPoints.startY) - dy * (x - endPoints.startX)) / length;\n return (percent > 0 && percent < 1 && Math.abs(distance) < this.parent.HIT_TARGET_PADDING);\n };\n\n /**\n * Define a class TemporaryLink that represents a link that's in the\n * process of being created.\n * @param {object} parent The graph to which this link belongs.\n * @param {object} from The node the link starts at.\n * @param {object} to The node the link goes to.\n */\n function TemporaryLink(parent, from, to) {\n this.parent = parent;\n this.from = from;\n this.to = to;\n }\n\n TemporaryLink.prototype.draw = function(c) {\n // Draw the line.\n c.beginPath();\n c.moveTo(this.to.x, this.to.y);\n c.lineTo(this.from.x, this.from.y);\n c.stroke();\n\n // Draw the head of the arrow.\n this.parent.arrowIfReqd(c, this.to.x, this.to.y, Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x));\n };\n\n /**\n * Define a class Button for a pseudo-menu button.\n * @param {object} parent The graph to which this button belongs.\n * @param {int} topX The x coordinate of the top left corner of the menu text.\n * @param {int} topY The y coordinate of the top left corner of the menu text.\n * @param {string} text The button label text.\n */\n function Button(parent, topX, topY, text) {\n this.BUTTON_WIDTH = 60;\n this.BUTTON_HEIGHT = 25;\n this.TEXT_OFFSET_X = 30;\n this.TEXT_OFFSET_Y = 17;\n this.topX = topX;\n this.topY = topY;\n this.parent = parent;\n this.text = text;\n this.highLighted = false;\n }\n\n Button.prototype.containsPoint = function(x, y) {\n return util.isInside({x: x, y: y},\n {x: this.topX, y: this.topY, width: this.BUTTON_WIDTH, height: this.BUTTON_HEIGHT});\n };\n\n Button.prototype.draw = function(c) {\n if (this.highLighted) {\n c.fillStyle = '#FFFFFF';\n } else {\n c.fillStyle = '#F0F0F0';\n }\n c.fillRect(this.topX, this.topY,\n this.BUTTON_WIDTH, this.BUTTON_HEIGHT);\n c.lineWidth = 0.5;\n c.strokeStyle = '#000000';\n c.strokeRect(this.topX, this.topY,\n this.BUTTON_WIDTH, this.BUTTON_HEIGHT);\n\n c.font = '12pt Arial';\n c.fillStyle = '#000000';\n c.textAlign = \"center\";\n c.fillText(this.text, this.topX + this.TEXT_OFFSET_X, this.topY + this.TEXT_OFFSET_Y);\n c.textAlign = \"left\";\n };\n\n Button.prototype.onClick = function() {\n\n };\n\n /**\n * Define a class HelpBox for the help box and its pseudo-menu button.\n * @param {object} parent The graph to which this help box belongs.\n * @param {int} topX The x coordinate of the top left corner of the help box.\n * @param {int} topY The y coordinate of the top left corner of the help box.\n */\n function HelpBox(parent, topX, topY) {\n Button.call(this, parent, topX, topY, \"Help\");\n this.helpOpen = false;\n this.LINE_HEIGHT = 18;\n this.HELP_INDENT = 5;\n }\n\n HelpBox.prototype = new Button();\n\n HelpBox.prototype.draw = function(c) {\n var lines, i, y, helpText;\n\n Button.prototype.draw.call(this, c);\n\n if (this.helpOpen) {\n helpText = this.parent.helpText;\n c.font = '12pt Arial';\n lines = helpText.split('\\n');\n y = this.topY + this.BUTTON_HEIGHT;\n for (i = 0; i < lines.length; i += 1) {\n y += this.LINE_HEIGHT;\n c.fillText(lines[i], this.topX + this.HELP_INDENT, y);\n }\n }\n };\n\n HelpBox.prototype.onClick = function() {\n this.helpOpen = ! this.helpOpen;\n this.parent.draw();\n };\n\n\n /**\n * Define a class TextBox for a possibly editable text box that might\n * be contained in another element.\n * @param {string} text The text to put in the text box.\n * @param {object} parent The graph to which the text box belongs.\n **/\n function TextBox(text, parent) {\n this.text = text;\n this.parent = parent;\n this.caretPosition = text.length;\n this.relDist = 0.5;\n this.offset = parent.parent.textOffset();\n this.dragged = false;\n this.boundingBox = {};\n }\n\n // Inserts a given character into the TextBox at its current caretPosition\n TextBox.prototype.insertChar = function(char) {\n this.text = this.text.slice(0, this.caretPosition) + char + this.text.slice(this.caretPosition);\n this.caretRight();\n };\n\n // Deletes the character in the TextBox that is located behind the current caretPosition\n TextBox.prototype.deleteChar = function() {\n if (this.caretPosition > 0){\n this.text = this.text.slice(0, this.caretPosition - 1) + this.text.slice(this.caretPosition);\n this.caretLeft();\n }\n };\n\n // Moves the TextBox's caret left one character if possible\n TextBox.prototype.caretLeft = function() {\n if (this.caretPosition > 0) {\n this.caretPosition --;\n }\n };\n\n // Moves the TextBox's caret right one character if possible\n TextBox.prototype.caretRight = function() {\n if (this.caretPosition < this.text.length) {\n this.caretPosition ++;\n }\n };\n\n TextBox.prototype.containsPoint = function(x, y) {\n var point = {x: x, y: y};\n return util.isInside(point, this.boundingBox);\n };\n\n TextBox.prototype.setMouseStart = function(x, y) {\n // At the start of a drag, record our position relative to the mouse.\n this.mouseOffsetX = this.position.x - x;\n this.mouseOffsetY = this.position.y - y;\n };\n\n TextBox.prototype.setAnchorPoint = function(x, y) {\n x += (this.mouseOffsetX || 0);\n y += (this.mouseOffsetY || 0);\n var linkInfo = this.parent.getEndPointsAndCircle();\n var relDist, offset;\n //Calculate the relative distance of the dragged text along its parent link\n if (linkInfo.hasCircle){\n var textAngle = Math.atan2(y-linkInfo.circleY, x-linkInfo.circleX);\n // Ensure textAngle is either between start and end angle, or more than end angle\n if (textAngle < linkInfo.startAngle) {\n textAngle += Math.PI * 2;\n }\n if (linkInfo.endAngle < linkInfo.startAngle) {\n linkInfo.endAngle += Math.PI * 2;\n }\n // Calculate relDist from angle (inverse of angle-from-relDist calculation in Link.prototype.draw)\n if (linkInfo.isReversed){\n relDist = (textAngle - linkInfo.startAngle - Math.PI*2) / (linkInfo.endAngle - linkInfo.startAngle - Math.PI*2);\n }else{\n relDist = (textAngle - linkInfo.startAngle) / (linkInfo.endAngle - linkInfo.startAngle);\n }\n offset = util.vectorMagnitude({x: x-linkInfo.circleX, y: y-linkInfo.circleY}) - linkInfo.circleRadius;\n }\n else {\n // Calculate relative position of the mouse projected onto the link.\n var textVector = {x: x - linkInfo.startX,\n y: y - linkInfo.startY};\n var linkVector = {x: linkInfo.endX - linkInfo.startX,\n y: linkInfo.endY - linkInfo.startY};\n var projection = util.scalarProjection(textVector, linkVector);\n relDist = projection / util.vectorMagnitude(linkVector);\n // Calculate offset (closest distance) of the mouse position from the link\n offset = Math.sqrt(Math.pow(util.vectorMagnitude(textVector), 2)- Math.pow(projection, 2));\n // If the mouse is on the opposite side of the link from the default text position, negate the offset\n var ccw = util.isCCW(textVector, linkVector);\n var reversed = (this.parent.lineAngleAdjust != 0);\n if ((!ccw && reversed) || (ccw && !reversed)){\n offset *= -1;\n }\n }\n if (relDist > 0 && relDist < 1){ //Ensure text isn't dragged past end of the link\n this.relDist = relDist;\n this.offset = Math.round(offset);\n this.dragged = true;\n }\n };\n\n TextBox.prototype.draw = function(x, y, angleOrNull, parentObject) {\n var graph = parentObject.parent,\n c = graph.getCanvas().getContext('2d');\n\n c.font = graph.fontSize() + 'px Arial';\n //Text before and after caret are drawn separately to expand Latex shortcuts at the caret position\n var beforeCaretText = util.convertLatexShortcuts(this.text.slice(0, this.caretPosition));\n var afterCaretText = util.convertLatexShortcuts(this.text.slice(this.caretPosition));\n var width = c.measureText(beforeCaretText + afterCaretText).width;\n var dy = Math.round(graph.fontSize() / 2);\n\n // Position the text appropriately if it is part of a link\n if(angleOrNull !== null) {\n var cos = Math.cos(angleOrNull);\n var sin = Math.sin(angleOrNull);\n\n //Add text offset in the direction of the text angle\n x += this.offset * cos;\n y += this.offset * sin;\n\n // Position text intelligently if text has not been manually moved\n if (!this.dragged){\n var cornerPointX = (width / 2) * (cos > 0 ? 1 : -1);\n var cornerPointY = (dy / 2) * (sin > 0 ? 1 : -1);\n var slide = sin * Math.pow(Math.abs(sin), 40) * cornerPointX - cos * Math.pow(Math.abs(cos), 10) * cornerPointY;\n x += cornerPointX - sin * slide;\n y += cornerPointY + cos * slide;\n }\n this.position = {x: Math.round(x), y: Math.round(y)}; //Record the position where text is anchored to\n }\n\n x -= width / 2; // Center the text.\n\n //Round the coordinates so they fall on a pixel\n x = Math.round(x);\n y = Math.round(y);\n\n // Draw text and caret\n if('advancedFillText' in c) {\n c.advancedFillText(this.text, this.text, x + width / 2, y, angleOrNull);\n } else {\n // Draw translucent white rectangle behind text\n var prevStyle = c.fillStyle;\n c.fillStyle = \"rgba(255, 255, 255, 0.7)\";\n c.fillRect(x, y-dy, width, dy*2);\n c.fillStyle = prevStyle;\n\n // Draw text\n dy = Math.round(graph.fontSize() / 3); // Don't understand this.\n c.fillText(beforeCaretText, x, y + dy);\n var caretX = x + c.measureText(beforeCaretText).width;\n c.fillText(afterCaretText, caretX, y + dy);\n\n // Draw caret\n dy = Math.round(graph.fontSize() / 2);\n if(parentObject == graph.selectedObject && graph.caretVisible && graph.hasFocus() && document.hasFocus()) {\n c.beginPath();\n c.moveTo(caretX, y - dy);\n c.lineTo(caretX, y + dy);\n c.stroke();\n }\n }\n this.boundingBox = {x: x, y: y - dy, height: dy * 2, width: width};\n };\n\n\n return {\n Node: Node,\n Link: Link,\n SelfLink: SelfLink,\n TemporaryLink: TemporaryLink,\n StartLink: StartLink,\n Button: Button,\n HelpBox: HelpBox,\n TextBox: TextBox\n };\n});\n"],"names":["define","util","Node","parent","x","y","mouseOffsetX","mouseOffsetY","isAcceptState","textBox","TextBox","this","caretPosition","Link","a","b","nodeA","nodeB","lineAngleAdjust","parallelPart","perpendicularPart","SelfLink","node","mouse","anchorAngle","mouseOffsetAngle","setAnchorPoint","StartLink","start","deltaX","deltaY","TemporaryLink","from","to","Button","topX","topY","text","BUTTON_WIDTH","BUTTON_HEIGHT","TEXT_OFFSET_X","TEXT_OFFSET_Y","highLighted","HelpBox","call","helpOpen","LINE_HEIGHT","HELP_INDENT","length","relDist","offset","textOffset","dragged","boundingBox","prototype","setMouseStart","mouseX","mouseY","trackMouse","draw","c","beginPath","arc","nodeRadius","Math","PI","stroke","closestPointOnCircle","dx","dy","scale","sqrt","containsPoint","neighbours","links","link","i","includes","push","traverseGraph","visited","neighbour","getAnchorPoint","abs","SNAP_TO_PADDING","getEndPointsAndCircle","midX","midY","end","anchor","circle","circleFromThreePoints","isReversed","reverseScale","rRatio","radius","startAngle","atan2","endAngle","cos","sin","textX","textY","textAngle","linkInfo","hasCircle","circleX","circleY","circleRadius","moveTo","startX","startY","lineTo","endX","endY","arrowIfReqd","distance","percent","HIT_TARGET_PADDING","angle","temp","mouseStartX","mouseStartY","snap","round","getEndPoints","endPoints","isInside","width","height","fillStyle","fillRect","lineWidth","strokeStyle","strokeRect","font","textAlign","fillText","onClick","lines","helpText","split","insertChar","char","slice","caretRight","deleteChar","caretLeft","point","position","vectorMagnitude","textVector","linkVector","projection","scalarProjection","pow","ccw","isCCW","reversed","angleOrNull","parentObject","graph","getCanvas","getContext","fontSize","beforeCaretText","convertLatexShortcuts","afterCaretText","measureText","cornerPointX","cornerPointY","slide","advancedFillText","prevStyle","caretX","selectedObject","caretVisible","hasFocus","document"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/graphutil.min.js b/amd/build/graphutil.min.js
index 93b742731..feb02fc20 100644
--- a/amd/build/graphutil.min.js
+++ b/amd/build/graphutil.min.js
@@ -1,3 +1,3 @@
-define("qtype_coderunner/graphutil",(function(){function Util(){this.greekLetterNames=["Alpha","Beta","Gamma","Delta","Epsilon","Zeta","Eta","Theta","Iota","Kappa","Lambda","Mu","Nu","Xi","Omicron","Pi","Rho","Sigma","Tau","Upsilon","Phi","Chi","Psi","Omega"]}return Util.prototype.convertLatexShortcuts=function(text){for(var i=0;i16)))).replace(new RegExp("\\\\"+name.toLowerCase(),"g"),String.fromCharCode(945+i+(i>16)))}for(i=0;i<10;i++)text=text.replace(new RegExp("_"+i,"g"),String.fromCharCode(8320+i));return text=text.replace(new RegExp("_a","g"),String.fromCharCode(8336))},Util.prototype.drawArrow=function(c,x,y,angle){var dx=Math.cos(angle),dy=Math.sin(angle);c.beginPath(),c.moveTo(x,y),c.lineTo(x-8*dx+5*dy,y-8*dy-5*dx),c.lineTo(x-8*dx-5*dy,y-8*dy+5*dx),c.fill()},Util.prototype.det=function(a,b,c,d,e,f,g,h,i){return a*e*i+b*f*g+c*d*h-a*f*h-b*d*i-c*e*g},Util.prototype.vectorMagnitude=function(v){return Math.sqrt(v.x*v.x+v.y*v.y)},Util.prototype.scalarProjection=function(a,b){return(a.x*b.x+a.y*b.y)/this.vectorMagnitude(b)},Util.prototype.isCCW=function(a,b){return a.x*b.y-b.x*a.y>0},Util.prototype.circleFromThreePoints=function(x1,y1,x2,y2,x3,y3){var a=this.det(x1,y1,1,x2,y2,1,x3,y3,1),bx=-this.det(x1*x1+y1*y1,y1,1,x2*x2+y2*y2,y2,1,x3*x3+y3*y3,y3,1),by=this.det(x1*x1+y1*y1,x1,1,x2*x2+y2*y2,x2,1,x3*x3+y3*y3,x3,1),c=-this.det(x1*x1+y1*y1,x1,y1,x2*x2+y2*y2,x2,y2,x3*x3+y3*y3,x3,y3);return{x:-bx/(2*a),y:-by/(2*a),radius:Math.sqrt(bx*bx+by*by-4*a*c)/(2*Math.abs(a))}},Util.prototype.isInside=function(pos,rect){return pos.x>rect.x&&pos.xrect.y},Util.prototype.crossBrowserKey=function(e){return(e=e||window.event).which||e.keyCode},Util.prototype.crossBrowserRelativeMousePos=function(e){const rect=e.target.getBoundingClientRect();return{x:e.clientX-rect.left,y:e.clientY-rect.top}},new Util}));
+define("qtype_coderunner/graphutil",(function(){function Util(){this.greekLetterNames=["Alpha","Beta","Gamma","Delta","Epsilon","Zeta","Eta","Theta","Iota","Kappa","Lambda","Mu","Nu","Xi","Omicron","Pi","Rho","Sigma","Tau","Upsilon","Phi","Chi","Psi","Omega"]}return Util.prototype.convertLatexShortcuts=function(text){for(var i=0;i16)))).replace(new RegExp("\\\\"+name.toLowerCase(),"g"),String.fromCharCode(945+i+(i>16)))}for(i=0;i<10;i++)text=text.replace(new RegExp("_"+i,"g"),String.fromCharCode(8320+i));return text=text.replace(new RegExp("_a","g"),String.fromCharCode(8336))},Util.prototype.drawArrow=function(c,x,y,angle){var dx=Math.cos(angle),dy=Math.sin(angle);c.beginPath(),c.moveTo(x,y),c.lineTo(x-8*dx+5*dy,y-8*dy-5*dx),c.lineTo(x-8*dx-5*dy,y-8*dy+5*dx),c.fill()},Util.prototype.det=function(a,b,c,d,e,f,g,h,i){return a*e*i+b*f*g+c*d*h-a*f*h-b*d*i-c*e*g},Util.prototype.vectorMagnitude=function(v){return Math.sqrt(v.x*v.x+v.y*v.y)},Util.prototype.scalarProjection=function(a,b){return(a.x*b.x+a.y*b.y)/this.vectorMagnitude(b)},Util.prototype.isCCW=function(a,b){return a.x*b.y-b.x*a.y>0},Util.prototype.circleFromThreePoints=function(x1,y1,x2,y2,x3,y3){var a=this.det(x1,y1,1,x2,y2,1,x3,y3,1),bx=-this.det(x1*x1+y1*y1,y1,1,x2*x2+y2*y2,y2,1,x3*x3+y3*y3,y3,1),by=this.det(x1*x1+y1*y1,x1,1,x2*x2+y2*y2,x2,1,x3*x3+y3*y3,x3,1),c=-this.det(x1*x1+y1*y1,x1,y1,x2*x2+y2*y2,x2,y2,x3*x3+y3*y3,x3,y3);return{x:-bx/(2*a),y:-by/(2*a),radius:Math.sqrt(bx*bx+by*by-4*a*c)/(2*Math.abs(a))}},Util.prototype.isInside=function(pos,rect){return pos.x>rect.x&&pos.xrect.y},Util.prototype.crossBrowserKey=function(e){return(e=e||window.event).which||e.keyCode},Util.prototype.crossBrowserRelativeMousePos=function(e){var rect=e.target.getBoundingClientRect();return{x:e.clientX-rect.left,y:e.clientY-rect.top}},new Util}));
//# sourceMappingURL=graphutil.min.js.map
\ No newline at end of file
diff --git a/amd/build/graphutil.min.js.map b/amd/build/graphutil.min.js.map
new file mode 100644
index 000000000..5f11864b8
--- /dev/null
+++ b/amd/build/graphutil.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"graphutil.min.js","sources":["../src/graphutil.js"],"sourcesContent":["/***********************************************************************\n *\n * Utility functions/data/constants for the ui_graph module.\n *\n ***********************************************************************/\n// Most of this code is taken from Finite State Machine Designer:\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(function() {\n /**\n * Contstructor for the Util class.\n */\n function Util() {\n\n this.greekLetterNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',\n 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda',\n 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',\n 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega' ];\n }\n\n Util.prototype.convertLatexShortcuts = function(text) {\n // Html greek characters.\n for(var i = 0; i < this.greekLetterNames.length; i++) {\n var name = this.greekLetterNames[i];\n text = text.replace(new RegExp('\\\\\\\\' + name, 'g'), String.fromCharCode(913 + i + (i > 16)));\n text = text.replace(new RegExp('\\\\\\\\' + name.toLowerCase(), 'g'), String.fromCharCode(945 + i + (i > 16)));\n }\n\n // Subscripts.\n for(var i = 0; i < 10; i++) {\n text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));\n }\n text = text.replace(new RegExp('_a', 'g'), String.fromCharCode(8336));\n return text;\n };\n\n Util.prototype.drawArrow = function(c, x, y, angle) {\n // Draw an arrow head on the graphics context c at (x, y) with given angle.\n\n var dx = Math.cos(angle);\n var dy = Math.sin(angle);\n c.beginPath();\n c.moveTo(x, y);\n c.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);\n c.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);\n c.fill();\n };\n\n Util.prototype.det = function(a, b, c, d, e, f, g, h, i) {\n // Determinant of given matrix elements.\n return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g;\n };\n\n Util.prototype.vectorMagnitude = function(v){\n // Returns magnitude (length) of a vector v\n return Math.sqrt(v.x * v.x + v.y * v.y);\n };\n\n Util.prototype.scalarProjection = function(a, b) {\n // Returns scalar projection of vector a onto vector b\n return (a.x * b.x + a.y * b.y) / this.vectorMagnitude(b);\n };\n\n Util.prototype.isCCW = function(a, b) {\n // Returns true iff vector b is in a counter-clockwise orientation relative to a\n return (a.x * b.y) - (b.x * a.y) > 0;\n };\n\n Util.prototype.circleFromThreePoints = function(x1, y1, x2, y2, x3, y3) {\n // Return {x, y, radius} of circle through (x1, y1), (x2, y2), (x3, y3).\n var a = this.det(x1, y1, 1, x2, y2, 1, x3, y3, 1);\n var bx = -this.det(x1 * x1 + y1 * y1, y1, 1, x2 * x2 + y2 * y2, y2, 1, x3 * x3 + y3 * y3, y3, 1);\n var by = this.det(x1 * x1 + y1 * y1, x1, 1, x2 * x2 + y2 * y2, x2, 1, x3 * x3 + y3 * y3, x3, 1);\n var c = -this.det(x1 * x1 + y1 * y1, x1, y1, x2 * x2 + y2 * y2, x2, y2, x3 * x3 + y3 * y3, x3, y3);\n return {\n 'x': -bx / (2 * a),\n 'y': -by / (2 * a),\n 'radius': Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))\n };\n };\n\n Util.prototype.isInside = function(pos, rect) {\n // True iff given point pos is inside rectangle.\n return pos.x > rect.x && pos.x < rect.x + rect.width && pos.y < rect.y + rect.height && pos.y > rect.y;\n };\n\n Util.prototype.crossBrowserKey = function(e) {\n // Return which key was pressed, given the event, in a browser-independent way.\n e = e || window.event;\n return e.which || e.keyCode;\n };\n\n\n Util.prototype.crossBrowserRelativeMousePos = function(e) {\n // Earlier complex version was breaking in Moodle 4, so replaced\n // with this much simpler version that should work with all modern\n // browsers.\n const rect = e.target.getBoundingClientRect();\n const x = e.clientX - rect.left; //x position within the element.\n const y = e.clientY - rect.top; //y position within the element.\n return {'x': x, 'y': y};\n };\n\n return new Util();\n});\n"],"names":["define","Util","greekLetterNames","prototype","convertLatexShortcuts","text","i","this","length","name","replace","RegExp","String","fromCharCode","toLowerCase","drawArrow","c","x","y","angle","dx","Math","cos","dy","sin","beginPath","moveTo","lineTo","fill","det","a","b","d","e","f","g","h","vectorMagnitude","v","sqrt","scalarProjection","isCCW","circleFromThreePoints","x1","y1","x2","y2","x3","y3","bx","by","abs","isInside","pos","rect","width","height","crossBrowserKey","window","event","which","keyCode","crossBrowserRelativeMousePos","target","getBoundingClientRect","clientX","left","clientY","top"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/multilanguagequestion.min.js.map b/amd/build/multilanguagequestion.min.js.map
new file mode 100644
index 000000000..820d8aa0e
--- /dev/null
+++ b/amd/build/multilanguagequestion.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"multilanguagequestion.min.js","sources":["../src/multilanguagequestion.js"],"sourcesContent":["/******************************************************************************\n *\n * This module simply handles changes in the Language selection dropdown for\n * multilanguage questions as seen by students. It switches the Ace language\n * accordingly.\n * It should only be loaded in conjunction with the ui_ace module.\n *\n * @module qtype_coderunner/multilanguagequestion\n * @copyright Richard Lobb, 2018, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * TODO: is there a race problem here??\n */\n\ndefine(['jquery'], function($) {\n /**\n * Initialise the language selector dropdown when the document is ready.\n * @param {string} taId The ID of the student answer box text area.\n */\n function initLangSelector(taId) {\n $().ready(function() {\n initLangSelectorWhenReady(taId);\n });\n }\n\n /**\n * Initialise the language selector. Called by initLanSelector when the\n * document is ready so the name is a bit of a misnomer.\n * @param {string} taId The ID of the student answer box text area.\n */\n function initLangSelectorWhenReady(taId) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n selector = $(\".coderunner-lang-select\");\n\n /**\n * Set the language for the Ace editor.\n */\n function setAceLang() {\n // Set the language for the Ace plugin (or any other plugin if it\n // has a setLanguage method).\n var lang = selector.val(),\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on reqd ta.\n\n if (uiWrapper && uiWrapper.uiInstance && typeof uiWrapper.uiInstance.setLanguage === 'function') {\n // TODO: define setLanguage as a required method of all UI plugins.\n uiWrapper.uiInstance.setLanguage(lang);\n }\n }\n\n selector.on('change', setAceLang);\n }\n\n return {'initLangSelector' : initLangSelector};\n});"],"names":["define","$","taId","ready","ta","document","getElementById","selector","setAceLang","lang","val","uiWrapper","data","uiInstance","setLanguage","on","initLangSelectorWhenReady"],"mappings":";;;;;;;;;;;;;AAcAA,gDAAO,CAAC,WAAW,SAASC,SAsCjB,2BAjCmBC,MACtBD,IAAIE,OAAM,qBAUqBD,UAC3BE,GAAKH,EAAEI,SAASC,eAAeJ,OAC/BK,SAAWN,EAAE,oCAKRO,iBAGDC,KAAOF,SAASG,MAChBC,UAAYP,GAAGQ,KAAK,sBAEpBD,WAAaA,UAAUE,YAA0D,mBAArCF,UAAUE,WAAWC,aAEjEH,UAAUE,WAAWC,YAAYL,MAIzCF,SAASQ,GAAG,SAAUP,YA5BlBQ,CAA0Bd"}
\ No newline at end of file
diff --git a/amd/build/outputdisplayarea.min.js b/amd/build/outputdisplayarea.min.js
index 102f26059..100e4a1bd 100644
--- a/amd/build/outputdisplayarea.min.js
+++ b/amd/build/outputdisplayarea.min.js
@@ -1,8 +1,3 @@
-define("qtype_coderunner/outputdisplayarea",["exports","jquery","core/ajax","core/str"],(function(_exports,_jquery,_ajax,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
-/**
- * @module coderunner/outputdisplayarea
- * @copyright James Napier, 2023, The University of Canterbury
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.OutputDisplayArea=void 0,_jquery=_interopRequireDefault(_jquery),_ajax=_interopRequireDefault(_ajax);_exports.OutputDisplayArea=class{constructor(displayAreaId,outputMode){this.displayAreaId=displayAreaId,this.displayArea=document.getElementById(displayAreaId),this.textDisplay=document.getElementById(displayAreaId+"-text"),this.htmlDisplay=document.getElementById(displayAreaId+"-html"),this.imageDisplay=document.getElementById(displayAreaId+"-images"),this.mode=outputMode,this.stdIn=[]}clearDisplay(){this.textDisplay.innerHTML="",this.htmlDisplay.innerHTML="",this.imageDisplay.innerHTML=""}displayText(response){const output=response.output,error=response.stderr;this.textDisplay.innerText=output+error}displayHtml(response){const output=response.output,error=response.stderr;this.textDisplay.innerHTML=output+error}displayNoOutput(response){const isNoOutput=""===response.output&&""===response.stderr;return isNoOutput&&(this.textDisplay.innerHTML='< No output! >'),isNoOutput}display(response){if(!this.displayNoOutput(response))if("json"===this.mode){const json=JSON.parse(response);this.displayJson(json)}else if("html"===this.mode)this.displayHtml(response);else{if("text"!==this.mode)throw Error('Invalid outputMode given: "'.concat(this.mode,'"'));{const text=response;this.displayText(text)}}}async handleRunButtonClick(code,lang,sandboxParams){this.clearDisplay(),_ajax.default.call([{methodname:"qtype_coderunner_run_in_sandbox",args:{contextid:M.cfg.contextid,sourcecode:code,language:lang,params:JSON.stringify(sandboxParams)},done:responseJson=>{const response=JSON.parse(responseJson);this.display(response)},fail:error=>{alert(error.message)}}])}displayJson(json){var result=json,text=result.stdout;42!==result.returncode&&(text+=result.stderr),13==result.returncode&&(text+="\n*** Timeout error ***\n");var base64,numImages=0;if(result.files)for(var prop in(0,_jquery.default)(this.imageDisplay).empty(),result.files){var image=(base64=result.files[prop],(0,_jquery.default)('')));(0,_jquery.default)(this.imageDisplay).append(image),numImages+=1}if(""===text.trim()&&42!==result.returncode?0==numImages&&(0,_jquery.default)(this.textDisplay).html('< No output! >'):(0,_jquery.default)(this.textDisplay).text(text),42===result.returncode){var inputId="".concat(this.displayAreaId,"-input-field");(0,_jquery.default)(this.textDisplay).html((0,_jquery.default)(this.textDisplay).html()+''));var inputEl=(0,_jquery.default)(document.getElementById(inputId));inputEl.focus(),inputEl.on("keyup",(e=>{if(13===e.keyCode){const line=inputEl.val();inputEl.remove(),(0,_jquery.default)(this.textDisplay).html((0,_jquery.default)(this.textDisplay).html()+line),this.handleRunButtonClick()}}))}}}}));
+define("qtype_coderunner/outputdisplayarea",["exports","jquery","core/ajax","core/str"],(function(_exports,_jquery,_ajax,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperties(target,props){for(var i=0;i< No output! >'),isNoOutput}},{key:"display",value:function(response){if(!this.displayNoOutput(response))if("json"===this.mode){var json=JSON.parse(response);this.displayJson(json)}else if("html"===this.mode)this.displayHtml(response);else{if("text"!==this.mode)throw Error('Invalid outputMode given: "'.concat(this.mode,'"'));var text=response;this.displayText(text)}}},{key:"handleRunButtonClick",value:(_handleRunButtonClick=_asyncToGenerator(regeneratorRuntime.mark((function _callee2(code,lang,sandboxParams){var _this=this;return regeneratorRuntime.wrap((function(_context2){for(;;)switch(_context2.prev=_context2.next){case 0:this.clearDisplay(),_ajax.default.call([{methodname:"qtype_coderunner_run_in_sandbox",args:{contextid:M.cfg.contextid,sourcecode:code,language:lang,params:JSON.stringify(sandboxParams)},done:function(responseJson){var response=JSON.parse(responseJson);_this.display(response)},fail:function(error){alert(error.message)}}]);case 2:case"end":return _context2.stop()}}),_callee2,this)}))),function(_x4,_x5,_x6){return _handleRunButtonClick.apply(this,arguments)})},{key:"displayJson",value:function(json){var _this2=this,result=json,text=result.stdout;42!==result.returncode&&(text+=result.stderr),13==result.returncode&&(text+="\n*** Timeout error ***\n");var base64,numImages=0;if(result.files)for(var prop in(0,_jquery.default)(this.imageDisplay).empty(),result.files){var image=(base64=result.files[prop],(0,_jquery.default)('')));(0,_jquery.default)(this.imageDisplay).append(image),numImages+=1}if(""===text.trim()&&42!==result.returncode?0==numImages&&(0,_jquery.default)(this.textDisplay).html('< No output! >'):(0,_jquery.default)(this.textDisplay).text(text),42===result.returncode){var inputId="".concat(this.displayAreaId,"-input-field");(0,_jquery.default)(this.textDisplay).html((0,_jquery.default)(this.textDisplay).html()+''));var inputEl=(0,_jquery.default)(document.getElementById(inputId));inputEl.focus(),inputEl.on("keyup",(function(e){if(13===e.keyCode){var line=inputEl.val();inputEl.remove(),(0,_jquery.default)(_this2.textDisplay).html((0,_jquery.default)(_this2.textDisplay).html()+line),_this2.handleRunButtonClick()}}))}}}],protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),OutputDisplayArea}();_exports.OutputDisplayArea=OutputDisplayArea}));
//# sourceMappingURL=outputdisplayarea.min.js.map
\ No newline at end of file
diff --git a/amd/build/outputdisplayarea.min.js.map b/amd/build/outputdisplayarea.min.js.map
new file mode 100644
index 000000000..1642a3868
--- /dev/null
+++ b/amd/build/outputdisplayarea.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"outputdisplayarea.min.js","sources":["../src/outputdisplayarea.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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/**\n * @module coderunner/outputdisplayarea\n * @copyright James Napier, 2023, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\n\nimport ajax from 'core/ajax';\nimport {get_string as getLangString} from 'core/str';\n\n\n/**\n * Get the specified language string using\n * AJAX and plug it into the given textarea\n * @param {string} langStringName The language string name.\n * @param {DOMnode} textarea The textarea into which the error message\n * should be plugged.\n * @param {string} additionalText Extra text to follow the result code.\n */\nconst setLangString = async(langStringName, textarea, additionalText) => {\n const message = await getLangString(langStringName, 'filter_ace_inline');\n textarea.show();\n textarea.html(escapeHtml(\"*** \" + message + \" ***\\n\" + additionalText));\n};\n\nconst diagnoseWebserviceResponse = response => {\n // Table of error conditions.\n // Each row is response.error, response.result, langstring\n // response.result is ignored if response.error is non-zero.\n // Any condition not in the table is deemed an \"Unknown runtime error\".\n const ERROR_RESPONSES = [\n [1, 0, 'Sandbox access denied.'], // Sandbox AUTH_ERROR\n [2, 0, 'error_unknown_language'], // Sandbox WRONG_LANG_ID\n [3, 0, 'Sandbox access denied.'], // Sandbox ACCESS_DENIED\n [4, 0, 'error_submission_limit_reached'], // Sandbox SUBMISSION_LIMIT_EXCEEDED\n [5, 0, 'Sandbox overload. Please wait and try again later'], // Sandbox SERVER_OVERLOAD\n [0, 11, ''], // RESULT_COMPILATION_ERROR\n [0, 12, 'Scratchpad crashed. Out of memory, perhaps?'], // RESULT_RUNTIME_ERROR (supervisor process broke)\n [0, 13, 'Scratchpad time limit error. Please report'], // RESULT TIME_LIMIT (supervisor process broke)\n [0, 15, ''], // RESULT_SUCCESS\n [0, 17, 'Scratchpad memory limit error. Please report'], // RESULT_MEMORY_LIMIT\n [0, 21, 'Sandbox overload. Please wait and try again later'], // RESULT_SERVER_OVERLOAD\n [0, 30, 'Excessive output.'] // RESULT OUTPUT_LIMIT\n ];\n for (let i = 0; i < ERROR_RESPONSES.length; i++) {\n let row = ERROR_RESPONSES[i];\n if (row[0] == response.error && (response.error != 0 || response.result == row[1])) {\n return row[2];\n }\n }\n return 'error_unknown_runtime'; // We're dead, Fred.\n};\n\nconst getImage = base64 => {\n let image = $(``);\n return image;\n};\n\n/**\n *\n */\nclass OutputDisplayArea {\n constructor(displayAreaId, outputMode) {\n this.displayAreaId = displayAreaId;\n this.displayArea = document.getElementById(displayAreaId);\n this.textDisplay = document.getElementById(displayAreaId + '-text');\n this.htmlDisplay = document.getElementById(displayAreaId + '-html');\n this.imageDisplay = document.getElementById(displayAreaId + '-images');\n this.mode = outputMode;\n this.stdIn = [];\n }\n\n clearDisplay() {\n this.textDisplay.innerHTML = \"\";\n this.htmlDisplay.innerHTML = \"\";\n this.imageDisplay.innerHTML = \"\";\n }\n\n displayText(response) {\n const output = response.output;\n const error = response.stderr;\n this.textDisplay.innerText = output + error;\n }\n\n displayHtml(response) {\n const output = response.output;\n const error = response.stderr;\n this.textDisplay.innerHTML = output + error;\n }\n\n displayNoOutput(response) {\n const isNoOutput = response.output === '' && response.stderr === '';\n if (isNoOutput) {\n this.textDisplay.innerHTML = '< No output! >';\n }\n return isNoOutput;\n }\n\n display(response) {\n if (this.displayNoOutput(response)) {\n return;\n }\n if (this.mode === 'json') {\n // TODO: error handling.\n const json = JSON.parse(response);\n this.displayJson(json);\n } else if (this.mode === 'html') {\n this.displayHtml(response);\n } else if (this.mode === 'text') {\n const text = response;\n this.displayText(text);\n } else {\n throw Error(`Invalid outputMode given: \"${this.mode}\"`);\n }\n }\n\n async handleRunButtonClick(code, lang, sandboxParams) {\n this.clearDisplay();\n ajax.call([{\n methodname: 'qtype_coderunner_run_in_sandbox',\n args: {\n contextid: M.cfg.contextid, // Moodle context ID\n sourcecode: code,\n language: lang,\n params: JSON.stringify(sandboxParams) // Sandbox params\n },\n done: (responseJson) => {\n // TODO: error handling.\n const response = JSON.parse(responseJson);\n // Const error = diagnoseWebserviceResponse(response);\n this.display(response);\n },\n fail: (error) => {\n alert(error.message);\n }\n }]);\n }\n\n displayJson(json) {\n var result = json;\n var text = result.stdout;\n\n if (result.returncode !== 42) {\n text += result.stderr;\n }\n if (result.returncode == 13) { // Timeout\n text += \"\\n*** Timeout error ***\\n\";\n }\n\n var numImages = 0;\n if (result.files) {\n $(this.imageDisplay).empty();\n for (var prop in result.files) {\n var image = getImage(result.files[prop]);\n $(this.imageDisplay).append(image);\n numImages += 1;\n }\n }\n\n if (text.trim() === '' && result.returncode !== 42) {\n if (numImages == 0) {\n $(this.textDisplay).html('< No output! >');\n }\n } else {\n $(this.textDisplay).text(text);\n }\n\n if (result.returncode === 42) {\n var inputId = `${this.displayAreaId}-input-field`;\n $(this.textDisplay).html($(this.textDisplay).html() + ``);\n var inputEl = $(document.getElementById(inputId));\n inputEl.focus();\n\n inputEl.on('keyup', (e) => {\n if (e.keyCode === 13) {\n const line = inputEl.val();\n inputEl.remove();\n $(this.textDisplay).html($(this.textDisplay).html() + line);\n this.handleRunButtonClick();\n }\n });\n }\n }\n}\n\nexport {\n OutputDisplayArea\n};\n"],"names":["langStringName","textarea","additionalText","message","show","html","escapeHtml","OutputDisplayArea","displayAreaId","outputMode","displayArea","document","getElementById","textDisplay","htmlDisplay","imageDisplay","mode","stdIn","innerHTML","response","output","error","stderr","innerText","isNoOutput","this","displayNoOutput","json","JSON","parse","displayJson","displayHtml","Error","text","displayText","code","lang","sandboxParams","clearDisplay","call","methodname","args","contextid","M","cfg","sourcecode","language","params","stringify","done","responseJson","_this","display","fail","alert","result","stdout","returncode","base64","numImages","files","prop","empty","image","append","trim","inputId","inputEl","focus","on","e","keyCode","line","val","remove","_this2","handleRunButtonClick"],"mappings":"2uCAkCsB,iBAAMA,eAAgBC,SAAUC,yJAC5B,mBAAcF,eAAgB,4BAA9CG,sBACNF,SAASG,OACTH,SAASI,KAAKC,WAAW,OAASH,QAAU,SAAWD,kFAuCrDK,wDACUC,cAAeC,yKAClBD,cAAgBA,mBAChBE,YAAcC,SAASC,eAAeJ,oBACtCK,YAAcF,SAASC,eAAeJ,cAAgB,cACtDM,YAAcH,SAASC,eAAeJ,cAAgB,cACtDO,aAAeJ,SAASC,eAAeJ,cAAgB,gBACvDQ,KAAOP,gBACPQ,MAAQ,2IAGjB,gBACSJ,YAAYK,UAAY,QACxBJ,YAAYI,UAAY,QACxBH,aAAaG,UAAY,8BAGlC,SAAYC,cACFC,OAASD,SAASC,OAClBC,MAAQF,SAASG,YAClBT,YAAYU,UAAYH,OAASC,iCAG1C,SAAYF,cACFC,OAASD,SAASC,OAClBC,MAAQF,SAASG,YAClBT,YAAYK,UAAYE,OAASC,qCAG1C,SAAgBF,cACNK,WAAiC,KAApBL,SAASC,QAAqC,KAApBD,SAASG,cAClDE,kBACKX,YAAYK,UAAY,uDAE1BM,kCAGX,SAAQL,cACAM,KAAKC,gBAAgBP,aAGP,SAAdM,KAAKT,KAAiB,KAEhBW,KAAOC,KAAKC,MAAMV,eACnBW,YAAYH,WACd,GAAkB,SAAdF,KAAKT,UACPe,YAAYZ,cACd,CAAA,GAAkB,SAAdM,KAAKT,WAINgB,2CAAoCP,KAAKT,eAHzCiB,KAAOd,cACRe,YAAYD,6GAMzB,kBAA2BE,KAAMC,KAAMC,2IAC9BC,6BACAC,KAAK,CAAC,CACPC,WAAY,kCACZC,KAAM,CACFC,UAAWC,EAAEC,IAAIF,UACjBG,WAAYV,KACZW,SAAUV,KACVW,OAAQnB,KAAKoB,UAAUX,gBAE3BY,KAAM,SAACC,kBAEG/B,SAAWS,KAAKC,MAAMqB,cAE5BC,MAAKC,QAAQjC,WAEjBkC,KAAM,SAAChC,OACHiC,MAAMjC,MAAMlB,iLAKxB,SAAYwB,sBACJ4B,OAAS5B,KACTM,KAAOsB,OAAOC,OAEQ,KAAtBD,OAAOE,aACPxB,MAAQsB,OAAOjC,QAEM,IAArBiC,OAAOE,aACPxB,MAAQ,iCA7FHyB,OAgGLC,UAAY,KACZJ,OAAOK,UAEF,IAAIC,2BADPpC,KAAKV,cAAc+C,QACJP,OAAOK,MAAO,KACvBG,OApGHL,OAoGoBH,OAAOK,MAAMC,OAnGlC,6DAAqCH,mCAoGnCjC,KAAKV,cAAciD,OAAOD,OAC5BJ,WAAa,KAID,KAAhB1B,KAAKgC,QAAuC,KAAtBV,OAAOE,WACZ,GAAbE,+BACElC,KAAKZ,aAAaR,KAAK,2EAG3BoB,KAAKZ,aAAaoB,KAAKA,MAGH,KAAtBsB,OAAOE,WAAmB,KACtBS,kBAAazC,KAAKjB,kDACpBiB,KAAKZ,aAAaR,MAAK,mBAAEoB,KAAKZ,aAAaR,wCAAmC6D,mBAC5EC,SAAU,mBAAExD,SAASC,eAAesD,UACxCC,QAAQC,QAERD,QAAQE,GAAG,SAAS,SAACC,MACC,KAAdA,EAAEC,QAAgB,KACZC,KAAOL,QAAQM,MACrBN,QAAQO,6BACNC,OAAK9D,aAAaR,MAAK,mBAAEsE,OAAK9D,aAAaR,OAASmE,MACtDG,OAAKC"}
\ No newline at end of file
diff --git a/amd/build/resetbutton.min.js.map b/amd/build/resetbutton.min.js.map
new file mode 100644
index 000000000..6ca89e48d
--- /dev/null
+++ b/amd/build/resetbutton.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"resetbutton.min.js","sources":["../src/resetbutton.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 * This AMD module provides the functionality for the \"Reset\"\n * button that is shown just below the student answer field if the question is\n * defined to have preloaded text.\n * If clicked, the button reloads the student answer field with the original\n * preloaded text (after a Confirm dialogue, of course).\n *\n * @module qtype_coderunner/resetbutton\n * @copyright Richard Lobb, 2016, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Initialise the Reset button.\n * @param {string} buttonId The ID of the button.\n * @param {string} answerId The ID of the answer box whose contents are to\n * be reset.\n * @param {string} confirmText The language string to display when asking\n * the user to confirm the reset.\n */\n function initResetButton(buttonId, answerId, confirmText) {\n var resetButton = $('[id=\"' + buttonId + '\"]'),\n studentAnswer = $('[id=\"' + answerId + '\"]'),\n uiWrapper;\n\n resetButton.on(\"click\", function() {\n if (window.behattesting || window.confirm(confirmText)) {\n var reloadText = resetButton.attr('data-reload-text');\n uiWrapper = studentAnswer.data('current-ui-wrapper');\n if (uiWrapper && uiWrapper.uiInstance) {\n // If the textarea has a UI wrapper, and it's active.\n uiWrapper.stop();\n studentAnswer.val(reloadText);\n uiWrapper.restart();\n } else {\n studentAnswer.val(reloadText);\n }\n }\n });\n }\n\n return { \"initResetButton\": initResetButton };\n});\n"],"names":["define","$","buttonId","answerId","confirmText","uiWrapper","resetButton","studentAnswer","on","window","behattesting","confirm","reloadText","attr","data","uiInstance","stop","val","restart"],"mappings":";;;;;;;;;;;AA4BAA,sCAAO,CAAC,WAAW,SAASC,SA+BjB,0BArBkBC,SAAUC,SAAUC,iBAGrCC,UAFAC,YAAcL,EAAE,QAAUC,SAAW,MACrCK,cAAgBN,EAAE,QAAUE,SAAW,MAG3CG,YAAYE,GAAG,SAAS,cAChBC,OAAOC,cAAgBD,OAAOE,QAAQP,aAAc,KAChDQ,WAAaN,YAAYO,KAAK,qBAClCR,UAAYE,cAAcO,KAAK,wBACdT,UAAUU,YAEvBV,UAAUW,OACVT,cAAcU,IAAIL,YAClBP,UAAUa,WAEVX,cAAcU,IAAIL"}
\ No newline at end of file
diff --git a/amd/build/showdiff.min.js.map b/amd/build/showdiff.min.js.map
new file mode 100644
index 000000000..a80d3f760
--- /dev/null
+++ b/amd/build/showdiff.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"showdiff.min.js","sources":["../src/showdiff.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 * This AMD module provides the functionality for the \"Show differences\"\n * button that is shown in the student's result page if their answer\n * isn't right and an \"exact-match\" (or near equivalent) grader is being used.\n *\n * @module qtype_coderunner/showdiff\n * @copyright Richard Lobb, 2016, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n var NLCHAR = '\\u21A9'; // Unicode \"leftwards arrow with hook\" to show newlines.\n\n /**\n * Given two lists of items, items1 and item2, return the length matrix\n * M defined as M[i][j] = max subsequence length of the two item lists\n * items1[0:i], items2[0:j]\n * @param {array} items1 The first list of items.\n * @param {array} items2 The second list of items.\n * @return {array} The length matrix.\n */\n function lcsLengths(items1, items2) {\n\n var n1 = items1.length,\n n2 = items2.length,\n lengths, i, j,\n has_fill = typeof [1].fill === 'function';\n\n lengths = [];\n\n for (i = 0; i <= n1; i += 1) {\n lengths[i] = new Array(n2 + 1);\n if (has_fill) {\n lengths[i].fill(0);\n } else {\n // Bloody IE.\n for (j = 0; j < n2 + 1; j++) {\n lengths[i][j] = 0;\n }\n }\n }\n for (i = 0; i < n1; i += 1) {\n for (j = 0; j < n2; j += 1) {\n if (items1[i] == items2[j]) {\n lengths[i + 1][j + 1] = 1 + lengths[i][j];\n } else {\n lengths[i + 1][j + 1] = Math.max(lengths[i][j + 1], lengths[i + 1][j]);\n }\n }\n }\n return lengths;\n }\n\n /**\n * Given two lists of items, items1 and item2, return the longest common\n * subsequence.\n * @param {array} items1 The first list of items.\n * @param {array} items2 The second list of items.\n * @return {array} The longest common subsequence.\n */\n function lcss(items1, items2) {\n\n var M, i, j, n, result, length;\n M = lcsLengths(items1, items2);\n length = M[items1.length][items2.length];\n result = [];\n i = items1.length;\n j = items2.length;\n n = length - 1;\n while (n >= 0) {\n if (items1[i - 1] == items2[j - 1]) {\n result[n] = items1[i - 1];\n n -= 1;\n i -= 1;\n j -= 1;\n } else if (M[i - 1][j] == M[i][j]) {\n i -= 1;\n } else {\n j -= 1;\n }\n }\n return result;\n }\n\n /**\n * Process the given token list and a subsequence of it, concatenating\n * tokens and wrapping all items not in the subsequence with\n * del tags (or whatever strings are specified by startDel, endDel).\n * @param {array} tokens A list of tokens in the original string.\n * @param {array} subSeq A subsequence of the tokens array.\n * @param {string} startDel An optional string that denotes the start of\n * a sequence of tokens to be deleted. Default ''\n * @param {string} endDel An optional string to mark the end of a sequence\n * of deleted tokens. Default ''.\n * @return {string} The concatenated sequence of tokens with start and\n * end delete tokens inserted to mark where the tokens from the first\n * parameter are not present in the second.\n */\n function insertDels(tokens, subSeq, startDel, endDel) {\n var html = \"\",\n deleting = false,\n i,\n ssi = 0;\n if (startDel === undefined) {\n startDel = '';\n }\n if (endDel === undefined) {\n endDel = '';\n }\n for (i = 0; i < tokens.length; i += 1) {\n if (ssi < subSeq.length && tokens[i] == subSeq[ssi]) {\n if (deleting) {\n html += endDel;\n deleting = false;\n }\n ssi += 1;\n } else {\n if (!deleting) {\n html += startDel;\n deleting = true;\n }\n }\n\n html += tokens[i];\n }\n if (deleting) {\n html += endDel;\n }\n return html;\n }\n\n /**\n * @param {string} elem An HTML element\n * @return {string} The HTML element type (i.e. its tag name) in lower case\n */\n function elType(elem) {\n return elem.tagName.toLowerCase();\n }\n\n /**\n * Return the sequence of tokens from the given HTML element.\n * A token is either a single character or an HTML entity (&.*;)\n * Extra 'leftward-arrow-with-hook' characters (\\u21A9) are added\n * at the ends of lines.\n * @param {string} element The HTML element whose contents are to be tokenised.\n * @return {array} The list of tokens extracted from the element.\n */\n function getTokens(element) {\n var isPre = elType(element) === 'pre',\n text = element.innerHTML,\n seq,\n i = 0;\n\n /**\n * Extract and return the next token starting at text[i]. Update i.\n * Precondition: i < text.length.\n */\n function nextToken() {\n var token, match;\n if (text[i] != '&') {\n token = text[i];\n i = i + 1;\n } else {\n match = text.substring(i, text.length).match(/(^&[a-zA-Z]+;)|(^[0-9]+;)|(^[xX][0-9a-fA-F]+;)/);\n if (match === null) {\n token = text[i];\n i = i + 1;\n } else {\n token = match[0];\n i = i + token.length;\n }\n }\n return token;\n }\n\n if (isPre) {\n text = text.replace(/\\n/g, NLCHAR + '\\n');\n }\n text = text.replace(/( )/g, NLCHAR + '$1');\n seq = [];\n i = 0;\n while (i < text.length) {\n seq.push(nextToken());\n }\n\n return seq;\n }\n\n /**\n * Given (references to) two HTML elements, extract the innerHTML\n * of both, find the longest common subsequence of chars and wrap text not\n * in that subsequence in del elements.\n * elements within the innerHTML are preceded by a\n * Unicode \"leftwards arrow with hook\" ('\\u21A9') so that line break changes\n * can be highlighted.\n * @param {string} firstEl The first HTML element to be processed.\n * @param {string} secondEl The second HTML element to be processed.\n */\n function showDifferences(firstEl, secondEl) {\n var openDelTag = '',\n closeDelTag = '',\n seq1,\n seq2,\n css;\n\n seq1 = getTokens(firstEl);\n seq2 = getTokens(secondEl);\n css = lcss(seq1, seq2);\n firstEl.innerHTML = insertDels(seq1, css, openDelTag, closeDelTag);\n secondEl.innerHTML = insertDels(seq2, css, openDelTag, closeDelTag);\n }\n\n /**\n * Given (references to) two DOM elements, delete all and \n * tags from the innerHTML of both. Also remove the \"leftwards arrows with\n * hooks\".\n * @param {string} firstEl The first HTML element to be processed.\n * @param {string} secondEl The second HTML element to be processed.\n */\n function hideDifferences(firstEl, secondEl) {\n var replPat = new RegExp('(?del[^>]*>)|(' + NLCHAR + ')', 'g');\n firstEl.innerHTML = firstEl.innerHTML.replace(replPat, '');\n secondEl.innerHTML = secondEl.innerHTML.replace(replPat, '');\n }\n\n /**\n * Now the API for applying diffs to rows in a CodeRunner\n * results table. Defines a class with methods initDiffButton and\n * processAllRows.\n * @param {array} tableRows The list of rows from the CodeRunner results table.\n * @param {int} gotCol The column number of the 'Got' column in the table.\n * @param {int} expectedCol The column number of the 'Expected' column.\n * @param {function} f The function to apply to the (expected, got) pair.\n */\n function processAllRows(tableRows, gotCol, expectedCol, f) {\n var row,\n cells,\n expected,\n got;\n\n for (var i = 0; i < tableRows.length; i++) {\n row = tableRows[i];\n cells = row.getElementsByTagName('td');\n expected = cells[expectedCol].children[0];\n got = cells[gotCol].children[0];\n f(expected, got);\n }\n }\n\n /**\n * Initialise the Show Differences button.\n * @param {string} buttonId The ID of the Show Differences button.\n * @param {string} showValue The text in the button initially.\n * @param {string} hideValue The text in the button when differences are showing.\n * @param {string} expectedString The column header denoting the 'Expected' column.\n * @param {string} gotString The column header denoting the 'Got' column.\n */\n function initDiffButton(buttonId, showValue, hideValue, expectedString, gotString) {\n var diffButton = $('[id=\"' + buttonId + '\"]'),\n table,\n tableRows,\n thEls,\n columnCount=0,\n gotCol=-1,\n expectedCol=-1;\n\n table = diffButton.closest('div.coderunner-test-results');\n thEls = table.find('thead tr').children();\n tableRows = table.find('tbody tr');\n\n // Find 'Expected' and 'Got' columns.\n thEls.each(function() {\n if ($(this).html() === gotString) {\n gotCol = columnCount;\n } else if ($(this).html() === expectedString) {\n expectedCol = columnCount;\n }\n columnCount += 1;\n });\n\n if (gotCol !== -1 && expectedCol !== -1) {\n diffButton.on(\"click\", function() {\n if (diffButton.prop('value') === showValue) {\n processAllRows(tableRows.toArray(), gotCol, expectedCol, showDifferences);\n diffButton.prop('value', hideValue);\n } else {\n processAllRows(tableRows.toArray(), gotCol, expectedCol, hideDifferences);\n diffButton.prop('value', showValue);\n }\n });\n } else {\n diffButton.enabled = false;\n diffButton.hide();\n }\n }\n\n return { \"initDiffButton\": initDiffButton };\n});\n"],"names":["define","$","lcss","items1","items2","M","i","j","n","result","length","lengths","n1","n2","has_fill","fill","Array","Math","max","lcsLengths","insertDels","tokens","subSeq","startDel","endDel","html","deleting","ssi","undefined","getTokens","element","seq","token","match","isPre","tagName","toLowerCase","text","innerHTML","replace","NLCHAR","push","substring","showDifferences","firstEl","secondEl","seq1","seq2","css","hideDifferences","replPat","RegExp","processAllRows","tableRows","gotCol","expectedCol","f","cells","getElementsByTagName","children","buttonId","showValue","hideValue","expectedString","gotString","table","thEls","diffButton","columnCount","closest","find","each","this","on","prop","toArray","enabled","hide"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/textareas.min.js b/amd/build/textareas.min.js
index f7c2df89a..3826a6463 100644
--- a/amd/build/textareas.min.js
+++ b/amd/build/textareas.min.js
@@ -6,6 +6,6 @@
* @copyright Richard Lobb, 2015, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/textareas",["jquery"],(function($){function initTextArea(){$(this).data("clickInProgress",!1),$(this).data("capturingTab",!0),$(this).on("mousedown",(function(){$(this).data("clickInProgress",!0)})),$(this).on("focusin",(function(){$(this).data("capturingTab",$(this).data("clickInProgress"))})),$(this).on("click",(function(){$(this).data("clickInProgress",!1)})),$(this).on("keydown",(function(e){if(!(window.hasOwnProperty("behattesting")&&window.behattesting||void 0!==e.which&&0===e.which))if(9==e.keyCode&&$(this).data("capturingTab"))(e.shiftKey||insertString(this," "))&&e.preventDefault();else if(13===e.keyCode&&void 0!==this.selectionStart){for(var before=this.value.substring(0,this.selectionStart),eol=before.lastIndexOf("\n"),line=before.substring(eol+1),indent="",i=0;i{div.is(":visible")?(div.hide(),hdr.innerText=hdrText.replace("▾","▸")):(div.show(),div.trigger("mousemove"),hdr.innerText=hdrText.replace("▸","▾"))}))}}}));
+define("qtype_coderunner/textareas",["jquery"],(function($){function initTextArea(){$(this).data("clickInProgress",!1),$(this).data("capturingTab",!0),$(this).on("mousedown",(function(){$(this).data("clickInProgress",!0)})),$(this).on("focusin",(function(){$(this).data("capturingTab",$(this).data("clickInProgress"))})),$(this).on("click",(function(){$(this).data("clickInProgress",!1)})),$(this).on("keydown",(function(e){if(!(window.hasOwnProperty("behattesting")&&window.behattesting||void 0!==e.which&&0===e.which))if(9==e.keyCode&&$(this).data("capturingTab"))(e.shiftKey||insertString(this," "))&&e.preventDefault();else if(13===e.keyCode&&void 0!==this.selectionStart){for(var before=this.value.substring(0,this.selectionStart),eol=before.lastIndexOf("\n"),line=before.substring(eol+1),indent="",i=0;i.\n */\n\n/**\n * JavaScript for handling textareas and form actions in CodeRunner question\n * editing forms and student question answering forms.\n *\n * @module qtype_coderunner/textareas\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Function to initialise all code-input text-areas in a page.\n * Used by the form editor but can't be used for question text areas as\n * renderer.php is called once for each question in a quiz, and there is\n * no communication between the questions.\n */\n function setupAllTAs() {\n $('textarea.edit_code').each(initTextArea);\n }\n\n /**\n * Initialise a particular text area (TA), given its ID (which may contain\n * colons, so can't use jQuery selector).\n * @param {string} taId The ID of the textarea html element.\n */\n function initQuestionTA(taId) {\n $(document.getElementById(taId)).each(initTextArea);\n }\n\n\n /**\n * Set up to expand or collapse the question author's answer when the user clicks\n * on the show/hide answer button.\n * @param {string} linkId The ID of the link that the user clicks.\n * @param {sting} divId The ID of the div to be shown or hidden.\n */\n function setupShowHideAnswer(linkId, divId) {\n let link = $(document.getElementById(linkId)),\n div = $(document.getElementById(divId)),\n hdr = link.children()[0], // The
element.\n hdrText = hdr.innerText; // Its body.\n link.click(() => {\n if (div.is(\":visible\")) {\n div.hide();\n hdr.innerText = hdrText.replace(\"\\u25BE\", \"\\u25B8\");\n } else {\n div.show();\n div.trigger('mousemove'); // So user sees contents.\n hdr.innerText = hdrText.replace(\"\\u25B8\", \"\\u25BE\");\n }\n });\n }\n\n /**\n * Set up the JavaScript to handle a text area 'this'.\n * It just does rudimentary autoindent on return and replaces tabs with\n * 4 spaces always.\n * For info on key handling browser inconsistencies see\n * http://unixpapa.com/js/key.html\n * If the AceEditor is handling a text area, this code is unused as\n * the actual textarea is hidden by Ace.\n */\n function initTextArea() {\n var TAB = 9,\n ENTER = 13,\n ESC = 27,\n KEY_M = 77;\n\n $(this).data('clickInProgress', false);\n $(this).data('capturingTab', true);\n\n $(this).on('mousedown', function() {\n /*\n * Event order seems to be (\\ is where the mouse button is pressed, / released):\n * Chrome: \\ mousedown, mouseup, focusin / click.\n * Firefox/IE: \\ mousedown, focusin / mouseup, click.\n */\n $(this).data('clickInProgress', true);\n });\n\n $(this).on('focusin', function() {\n /*\n * At first, pressing TAB moves focus.\n */\n $(this).data('capturingTab', $(this).data('clickInProgress'));\n });\n\n $(this).on('click', function() {\n $(this).data('clickInProgress', false);\n });\n\n $(this).on('keydown', function(e) {\n /*\n * Don't autoindent when behat testing in progress.\n */\n if (window.hasOwnProperty('behattesting') && window.behattesting) { return; }\n\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode == TAB && $(this).data('capturingTab')) {\n /*\n * Ignore SHIFT/TAB. Insert 4 spaces on TAB.\n */\n if (e.shiftKey || insertString(this, \" \")) {\n e.preventDefault();\n }\n }\n else if (e.keyCode === ENTER && this.selectionStart !== undefined) {\n /*\n * Handle autoindent only on non-IE.\n */\n var before = this.value.substring(0, this.selectionStart);\n var eol = before.lastIndexOf(\"\\n\");\n var line = before.substring(eol + 1); // Take from eol to end.\n var indent = \"\";\n for (var i = 0; i < line.length && line.charAt(i) === ' '; i++) {\n indent = indent + \" \";\n }\n if (insertString(this, \"\\n\" + indent)) {\n e.preventDefault();\n }\n /*\n * Once the user has started typing, TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n else if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n /*\n * CTRL + M toggles TAB capturing mode.\n * This is the short-cut recommended by\n * https:www.w3.org/TR/wai-aria-practices/#richtext.\n */\n $(this).data('capturingTab', !$(this).data('capturingTab'));\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n /*\n * ESC always stops capturing TAB.\n */\n $(this).data('capturingTab', false);\n }\n else if (!(e.ctrlKey || e.altKey)) {\n /*\n * Once the user has started typing (not modifier keys) TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n }\n });\n }\n\n /**\n * Insert into the given textarea ta the given string sToInsert.\n * @param {html_element} ta The textarea to be updated.\n * @param {string} sToInsert The string to be inserted at the current selection\n * point.\n */\n function insertString(ta, sToInsert) {\n if (ta.selectionStart !== undefined) { // Firefox etc.\n var before = ta.value.substring(0, ta.selectionStart);\n var selSave = ta.selectionEnd;\n var after = ta.value.substring(ta.selectionEnd, ta.value.length);\n\n /**\n * Update the text field.\n */\n var tmp = ta.scrollTop; // Inhibit annoying auto-scroll.\n ta.value = before + sToInsert + after;\n var pos = selSave + sToInsert.length;\n ta.selectionStart = pos;\n ta.selectionEnd = pos;\n ta.scrollTop = tmp;\n return true;\n\n }\n else if (document.selection && document.selection.createRange) { // IE.\n /*\n * TODO: check if this still works. OLD CODE.\n */\n var r = document.selection.createRange();\n var dr = r.duplicate();\n dr.moveToElementText(ta);\n dr.setEndPoint(\"EndToEnd\", r);\n r.text = sToInsert;\n return true;\n }\n /*\n * Other browsers we can't handle.\n */\n else {\n return false;\n }\n }\n\n return {\n setupAllTAs: setupAllTAs,\n initQuestionTA: initQuestionTA,\n setupShowHideAnswer: setupShowHideAnswer,\n };\n});\n"],"names":["define","$","initTextArea","this","data","on","e","window","hasOwnProperty","behattesting","undefined","which","keyCode","shiftKey","insertString","preventDefault","selectionStart","before","value","substring","eol","lastIndexOf","line","indent","i","length","charAt","ctrlKey","altKey","ta","sToInsert","selSave","selectionEnd","after","tmp","scrollTop","pos","document","selection","createRange","r","dr","duplicate","moveToElementText","setEndPoint","text","setupAllTAs","each","initQuestionTA","taId","getElementById","setupShowHideAnswer","linkId","divId","link","div","hdr","children","hdrText","innerText","click","is","hide","replace","show","trigger"],"mappings":";;;;;;;;AA2BAA,oCAAO,CAAC,WAAW,SAASC,YAsDfC,eAMLD,EAAEE,MAAMC,KAAK,mBAAmB,GAChCH,EAAEE,MAAMC,KAAK,gBAAgB,GAE7BH,EAAEE,MAAME,GAAG,aAAa,WAMpBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,WAIlBJ,EAAEE,MAAMC,KAAK,eAAgBH,EAAEE,MAAMC,KAAK,uBAG9CH,EAAEE,MAAME,GAAG,SAAS,WAChBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,SAASC,QAIvBC,OAAOC,eAAe,iBAAmBD,OAAOE,mBAEpCC,IAAZJ,EAAEK,OAAmC,IAAZL,EAAEK,UAlCzB,GAmCEL,EAAEM,SAAkBX,EAAEE,MAAMC,KAAK,iBAI7BE,EAAEO,UAAYC,aAAaX,KAAM,UACjCG,EAAES,sBAGL,GA1CD,KA0CKT,EAAEM,cAA6CF,IAAxBP,KAAKa,eAA8B,SAI3DC,OAASd,KAAKe,MAAMC,UAAU,EAAGhB,KAAKa,gBACtCI,IAAMH,OAAOI,YAAY,MACzBC,KAAOL,OAAOE,UAAUC,IAAM,GAC9BG,OAAS,GACJC,EAAI,EAAGA,EAAIF,KAAKG,QAA6B,MAAnBH,KAAKI,OAAOF,GAAYA,IACvDD,QAAkB,IAElBT,aAAaX,KAAM,KAAOoB,SAC1BjB,EAAES,iBAKNd,EAAEE,MAAMC,KAAK,gBAAgB,QAzD7B,KA2DKE,EAAEM,SAAqBN,EAAEqB,UAAYrB,EAAEsB,QAM5C3B,EAAEE,MAAMC,KAAK,gBAAiBH,EAAEE,MAAMC,KAAK,iBAC3CE,EAAES,kBAnEJ,KAqEOT,EAAEM,QAIPX,EAAEE,MAAMC,KAAK,gBAAgB,GAEtBE,EAAEqB,SAAWrB,EAAEsB,QAItB3B,EAAEE,MAAMC,KAAK,gBAAgB,eAYpCU,aAAae,GAAIC,mBACIpB,IAAtBmB,GAAGb,eAA8B,KAC7BC,OAASY,GAAGX,MAAMC,UAAU,EAAGU,GAAGb,gBAClCe,QAAUF,GAAGG,aACbC,MAAQJ,GAAGX,MAAMC,UAAUU,GAAGG,aAAcH,GAAGX,MAAMO,QAKrDS,IAAML,GAAGM,UACbN,GAAGX,MAAQD,OAASa,UAAYG,UAC5BG,IAAML,QAAUD,UAAUL,cAC9BI,GAAGb,eAAiBoB,IACpBP,GAAGG,aAAeI,IAClBP,GAAGM,UAAYD,KACR,EAGN,GAAIG,SAASC,WAAaD,SAASC,UAAUC,YAAa,KAIvDC,EAAIH,SAASC,UAAUC,cACvBE,GAAKD,EAAEE,mBACXD,GAAGE,kBAAkBd,IACrBY,GAAGG,YAAY,WAAYJ,GAC3BA,EAAEK,KAAOf,WACF,SAMA,QAIR,CACHgB,uBAjLA7C,EAAE,sBAAsB8C,KAAK7C,eAkL7B8C,wBA1KoBC,MACpBhD,EAAEoC,SAASa,eAAeD,OAAOF,KAAK7C,eA0KtCiD,6BAhKyBC,OAAQC,WAC7BC,KAAOrD,EAAEoC,SAASa,eAAeE,SACjCG,IAAMtD,EAAEoC,SAASa,eAAeG,QAChCG,IAAMF,KAAKG,WAAW,GACtBC,QAAUF,IAAIG,UAClBL,KAAKM,OAAM,WACHL,IAAIM,GAAG,aACPN,IAAIO,OACJN,IAAIG,UAAYD,QAAQK,QAAQ,IAAU,OAE1CR,IAAIS,OACJT,IAAIU,QAAQ,aACZT,IAAIG,UAAYD,QAAQK,QAAQ,IAAU"}
\ No newline at end of file
diff --git a/amd/build/ui_ace.min.js b/amd/build/ui_ace.min.js
index 05517f12c..52801a983 100644
--- a/amd/build/ui_ace.min.js
+++ b/amd/build/ui_ace.min.js
@@ -14,6 +14,6 @@
* @copyright Richard Lobb, 2015, 2017, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_ace",["jquery"],(function($){function AceWrapper(textareaId,w,h,params){var textarea=$(document.getElementById(textareaId)),wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=textarea[0]===document.activeElement,lang=params.lang,t=this;try{window.ace.require("ace/ext/language_tools"),this.modelist=window.ace.require("ace/ext/modelist"),this.textarea=textarea,this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),textarea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,this.editor.getSession().setValue(this.textarea.val()),this.fixSlowLoad(),params.theme&&this.editor.setTheme("ace/theme/"+params.theme),window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):params.theme?this.editor.setTheme("ace/theme/"+params.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(textarea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.fail=!1}catch(err){this.fail=!0}}return AceWrapper.prototype.failed=function(){return this.fail},AceWrapper.prototype.failMessage=function(){return"ace_ui_notready"},AceWrapper.prototype.sync=function(){},AceWrapper.prototype.syncIntervalSecs=function(){return 0},AceWrapper.prototype.setLanguage=function(language){var session=this.editor.getSession(),mode=this.findMode(language);mode&&session.setMode(mode.mode)},AceWrapper.prototype.getElement=function(){return this.editNode},AceWrapper.prototype.captureTab=function(){this.capturingTab=!0,this.editor.commands.bindKeys({Tab:"indent","Shift-Tab":"outdent"})},AceWrapper.prototype.releaseTab=function(){this.capturingTab=!1,this.editor.commands.bindKeys({Tab:null,"Shift-Tab":null})},AceWrapper.prototype.fixSlowLoad=function(){const observer=new IntersectionObserver((()=>{$(document).trigger("mousemove")})),editNode=this.editNode.get(0);observer.observe(editNode)},AceWrapper.prototype.setEventHandlers=function(){var t=this;this.editor.getSession().on("change",(function(){t.textarea.val(t.editor.getSession().getValue()),t.contents_changed=!0})),this.editor.on("blur",(function(){t.contents_changed&&t.textarea.trigger("change")})),this.editor.on("mousedown",(function(){t.clickInProgress=!0})),this.editor.on("focus",(function(){t.clickInProgress?t.captureTab():t.releaseTab()})),this.editor.on("click",(function(){t.clickInProgress=!1})),this.editor.container.addEventListener("keydown",(function(e){void 0!==e.which&&0===e.which||(77===e.keyCode&&e.ctrlKey&&!e.altKey?(t.capturingTab?t.releaseTab():t.captureTab(),e.preventDefault()):27===e.keyCode?t.releaseTab():e.shiftKey||e.ctrlKey||e.altKey||9==e.keyCode||t.captureTab())}),!0)},AceWrapper.prototype.destroy=function(){var focused;this.fail||(focused=this.editor.isFocused(),this.textarea.val(this.editor.getSession().getValue()),this.editor.destroy(),$(this.editNode).remove(),focused&&(this.textarea.focus(),this.textarea[0].selectionStart=this.textarea[0].value.length))},AceWrapper.prototype.hasFocus=function(){return this.editor.isFocused()},AceWrapper.prototype.findMode=function(language){var candidate,filename,result,candidates,nameMap={octave:"matlab",nodejs:"javascript","c#":"cs"};if("string"==typeof language){language.toLowerCase()in nameMap&&(language=nameMap[language.toLowerCase()]),candidates=[language,language.replace(/\d+$/,"")];for(var i=0;i
"),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),textarea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({enableBasicAutocompletion:!0,enableLiveAutocompletion:params.live_autocompletion,fontSize:params.font_size?params.font_size:"14px",newLineMode:"unix"}),this.editor.$blockScrolling=1/0,session=this.editor.getSession(),code=this.textarea.val(),(void 0===params.import_from_scratchpad||params.import_from_scratchpad)&&(code=this.extract_from_json_maybe(code)),session.setValue(code);var userTheme=window.localStorage.getItem("qtype_coderunner.ace.theme"),consider_prefers=(void 0===params.auto_switch_light_dark||params.auto_switch_light_dark)&&window.matchMedia;null!==userTheme?this.editor.setTheme(userTheme):consider_prefers&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):consider_prefers&&window.matchMedia("(prefers-color-scheme: light)").matches?this.editor.setTheme("ace/theme/textmate"):params.theme?this.editor.setTheme("ace/theme/"+params.theme):this.editor.setTheme("ace/theme/textmate"),this.fixSlowLoad(),this.setLanguage(lang),this.setEventHandlers(textarea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.fail=!1}catch(err){this.fail=!0}}return AceWrapper.prototype.extract_from_json_maybe=function(code){try{code=JSON.parse(code).answer_code[0]}catch(err){}return code},AceWrapper.prototype.failed=function(){return this.fail},AceWrapper.prototype.failMessage=function(){return"ace_ui_notready"},AceWrapper.prototype.sync=function(){var thisThemeNow=this.editor.getOption("theme"),thisEditorKey="qtype_coderunner.".concat(this.textareaId,".theme"),lastTheme=window.sessionStorage.getItem(thisEditorKey),globalTheme=window.localStorage.getItem("qtype_coderunner.ace.theme");null!==lastTheme&&lastTheme!=thisThemeNow?window.localStorage.setItem("qtype_coderunner.ace.theme",thisThemeNow):globalTheme&&globalTheme!==thisThemeNow&&(this.editor.setOption("theme",globalTheme),function(name){throw new TypeError('"'+name+'" is read-only')}("thisThemeNow")),window.sessionStorage.setItem(thisEditorKey,thisThemeNow)},AceWrapper.prototype.syncIntervalSecs=function(){return 2},AceWrapper.prototype.setLanguage=function(language){var session=this.editor.getSession(),mode=this.findMode(language);mode&&session.setMode(mode.mode)},AceWrapper.prototype.getElement=function(){return this.editNode},AceWrapper.prototype.captureTab=function(){this.capturingTab=!0,this.editor.commands.bindKeys({Tab:"indent","Shift-Tab":"outdent"})},AceWrapper.prototype.releaseTab=function(){this.capturingTab=!1,this.editor.commands.bindKeys({Tab:null,"Shift-Tab":null})},AceWrapper.prototype.fixSlowLoad=function(){var observer=new IntersectionObserver((function(){$(document).trigger("mousemove")})),editNode=this.editNode.get(0);observer.observe(editNode)},AceWrapper.prototype.setEventHandlers=function(){var t=this;this.editor.getSession().on("change",(function(){t.textarea.val(t.editor.getSession().getValue()),t.contents_changed=!0})),this.editor.on("blur",(function(){t.contents_changed&&t.textarea.trigger("change")})),this.editor.on("mousedown",(function(){t.clickInProgress=!0})),this.editor.on("focus",(function(){t.clickInProgress?t.captureTab():t.releaseTab()})),this.editor.on("click",(function(){t.clickInProgress=!1})),this.editor.container.addEventListener("keydown",(function(e){void 0!==e.which&&0===e.which||(77===e.keyCode&&e.ctrlKey&&!e.altKey?(t.capturingTab?t.releaseTab():t.captureTab(),e.preventDefault()):27===e.keyCode?t.releaseTab():e.shiftKey||e.ctrlKey||e.altKey||9==e.keyCode||t.captureTab())}),!0)},AceWrapper.prototype.destroy=function(){var focused;this.fail||(focused=this.editor.isFocused(),this.textarea.val(this.editor.getSession().getValue()),this.editor.destroy(),$(this.editNode).remove(),focused&&(this.textarea.focus(),this.textarea[0].selectionStart=this.textarea[0].value.length))},AceWrapper.prototype.hasFocus=function(){return this.editor.isFocused()},AceWrapper.prototype.findMode=function(language){var candidate,filename,result,candidates,nameMap={octave:"matlab",nodejs:"javascript","c#":"cs"};if("string"==typeof language){language.toLowerCase()in nameMap&&(language=nameMap[language.toLowerCase()]),candidates=[language,language.replace(/\d+$/,"")];for(var i=0;i.\n\n/**\n * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n code,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n this.textareaId = textareaId;\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n enableLiveAutocompletion: params.live_autocompletion,\n fontSize: params.font_size ? params.font_size : \"14px\",\n newLineMode: \"unix\",\n });\n\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n code = this.textarea.val();\n if (params.import_from_scratchpad === undefined || params.import_from_scratchpad) {\n code = this.extract_from_json_maybe(code);\n }\n session.setValue(code);\n\n // If there's a user-defined theme in local storage, use that.\n // Otherwise use the 'prefers-color-scheme' option if given or\n // the question/system defaults if not.\n const userTheme = window.localStorage.getItem('qtype_coderunner.ace.theme');\n const consider_prefers = (params.auto_switch_light_dark === undefined || params.auto_switch_light_dark)\n && window.matchMedia;\n if (userTheme !== null) {\n this.editor.setTheme(userTheme);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: light)').matches) {\n this.editor.setTheme(ACE_LIGHT_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.fixSlowLoad();\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n AceWrapper.prototype.extract_from_json_maybe = function(code) {\n // If the given code looks like JSON from the Scratchpad UI,\n // extract and return the answer_code attribute.\n try {\n const jsonObj = JSON.parse(code);\n code = jsonObj.answer_code[0];\n } catch(err) {}\n\n return code;\n };\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // The data is always sync'd to the text area. But here we use sync to\n // poll the value of the current theme and record in browser local\n // storage if the value for this particular Ace instance has changed\n // (as recorded in session storage). The theme to use for all Ace windows\n // from now on in this browser is recorded in browser local storage.\n const thisThemeNow = this.editor.getOption('theme');\n const thisEditorKey = `qtype_coderunner.${this.textareaId}.theme`;\n const lastTheme = window.sessionStorage.getItem(thisEditorKey);\n const globalKey = 'qtype_coderunner.ace.theme';\n const globalTheme = window.localStorage.getItem(globalKey);\n\n if (lastTheme !== null && lastTheme != thisThemeNow) {\n // The theme in this Ace window has been changed by the user.\n window.localStorage.setItem(globalKey, thisThemeNow);\n } else if (globalTheme && globalTheme !== thisThemeNow) {\n // A theme has been defined by another Ace window since we loaded.\n // Switch to that theme.\n this.editor.setOption('theme', globalTheme);\n thisThemeNow = globalTheme;\n }\n window.sessionStorage.setItem(thisEditorKey, thisThemeNow);\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 2;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n // Sometimes Ace editors do not load until the mouse is moved. To fix this,\n // 'move' the mouse using JQuery when the editor div enters the viewport.\n AceWrapper.prototype.fixSlowLoad = function () {\n const observer = new IntersectionObserver( () => {\n $(document).trigger('mousemove');\n });\n const editNode = this.editNode.get(0); // Non-JQuerry node.\n observer.observe(editNode);\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","session","code","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","enableLiveAutocompletion","live_autocompletion","fontSize","font_size","newLineMode","$blockScrolling","Infinity","getSession","val","undefined","import_from_scratchpad","extract_from_json_maybe","setValue","userTheme","localStorage","getItem","consider_prefers","auto_switch_light_dark","matchMedia","setTheme","matches","theme","fixSlowLoad","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","JSON","parse","answer_code","failed","failMessage","sync","thisThemeNow","getOption","thisEditorKey","lastTheme","sessionStorage","globalTheme","setItem","setOption","syncIntervalSecs","language","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","observer","IntersectionObserver","trigger","observe","getValue","container","addEventListener","e","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_ace_gapfiller.min.js b/amd/build/ui_ace_gapfiller.min.js
index 3cf7ed422..fa7b24b07 100644
--- a/amd/build/ui_ace_gapfiller.min.js
+++ b/amd/build/ui_ace_gapfiller.min.js
@@ -38,6 +38,6 @@
* @copyright Matthew Toohey, 2021, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_ace_gapfiller",["jquery"],(function($){var Range;const validChars=/[ !"#$%&'()*+,`\-./0-9:;<=>?@A-Z\[\]\\^_a-z{}|~]/;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this;let code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){let cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){let nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,selectionRange.start,char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){let maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+1=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(let i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(let i=start;i?@A-Z\[\]\\^_a-z{}|~]/;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this,code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){var cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){var nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){var char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){var _char=e.args;validChars.test(_char)&&gap.insertChar(t.gaps,selectionRange.start,_char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){var maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+1=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(var i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(var i=start;i.\n\n/**\n * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","matchMedia","matches","setTheme","theme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_gapfiller.min.js.map b/amd/build/ui_gapfiller.min.js.map
new file mode 100644
index 000000000..5dcfd71fe
--- /dev/null
+++ b/amd/build/ui_gapfiller.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_gapfiller.min.js","sources":["../src/ui_gapfiller.js"],"sourcesContent":["/**\n * 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 util.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/**\n * Implementation of the gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer box with a div\n * consisting of pre-formatted text supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra). HTML\n * entry or textarea elements are then inserted at\n * specified points. It is intended primarily for use with coding questions\n * where the answerbox presents the students with code that has smallish bits\n * missing.\n *\n * The locations within the globalextra text at which the input elements are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * for an HTML input element\n *\n * or\n *\n * {[ rows, columns ]}\n *\n * for a textarea element\n *\n * where size, rows and column are integer literals. These respectively\n * inject an HTML input element or a textarea element of the\n * specified size.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the input element insertion tags are by default '{[' and\n * ']}', but can be changed by an optional ui parameter gap_filler_delimiters,\n * which must be a 2-element array of strings. For example:\n *\n * {\"gap_filler_delimiters\": [\"{{\", \"}}\"]}\n *\n * Note that the double-brace delimiters in that example are the same as those\n * used by Twig, so using them instead of the default would prevent you from\n * ever adding Twig expansion (e.g. for randomisation) to the question.\n *\n * @module qtype_coderunner/ui_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n /**\n * Constructor for UI. Source html comes from data-globalextra by default,\n * else from whatever source is specified by the uiParams parameter.\n * @param {string} textareaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\n function GapfillerUi(textareaId, width, height, uiParams) {\n var html;\n this.textArea = $(document.getElementById(textareaId));\n this.readOnly = this.textArea.prop('readonly');\n this.uiParams = uiParams;\n this.fail = false;\n this.htmlDiv = null;\n this.source = uiParams.ui_source || 'globalextra';\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for HTML in ui_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n html = this.textArea.attr('data-globalextra');\n } else {\n html = this.textArea.attr('data-test0');\n }\n this.html = html.replace('<', '<');\n this.reload();\n }\n\n GapfillerUi.prototype.failed = function() {\n return this.fail; // Currently always true. See reload function.\n };\n\n /**\n * Copy the serialised version of the HTML UI area to the TextArea.\n */\n GapfillerUi.prototype.sync = function() {\n var\n serialisation = [], // A list of field values.\n empty = true;\n\n this.getFields().each(function() {\n var name, value;\n name = $(this).attr('name');\n if (name !== 'cr_gapfiller_field') {\n alert('Unexpected UI element found in answer box');\n } else {\n value = $(this).val();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n });\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n GapfillerUi.prototype.getElement = function() {\n return this.htmlDiv;\n };\n\n GapfillerUi.prototype.getFields = function() {\n return $(this.htmlDiv).find('.coderunner-ui-element');\n };\n\n /**\n * Set the value of the jQuery field to the given value.\n * If the field is a radio button or a checkbox,\n * the checked attribute is set. Otherwise the field's\n * val() function is called to set the value.\n * @param {object} field The JQuery field elemetn whose value is to be set.\n * @param {string} value The value to be used.\n */\n GapfillerUi.prototype.setField = function(field, value) {\n if (field.attr('type') === 'checkbox' || field.attr('type') === 'radio') {\n field.prop('checked', field.val() === value);\n } else {\n field.val(value);\n }\n };\n\n /**\n * Process the supplied HTML, HTML-escaping existing HTML\n * and inserting the input and textarea elements\n * at the marked locations.\n */\n GapfillerUi.prototype.markedUpHtml = function() {\n\n /**\n * Prefix any regular expression special chars in s with a backslash.\n * @param {string} s The string whose special values are to be escaped.\n * @return {string} The escaped string.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n var sepLeft = reEscape('{['),\n sepRight = reEscape(']}'),\n splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+, *\\\\d+)) *' + sepRight),\n bits = this.html.split(splitter),\n result = '
' + bits[0],\n i;\n\n for (i = 1; i < bits.length; i += 2) {\n result += this.markUp(bits[i]);\n if (i + 1 < bits.length) {\n result += bits[i + 1];\n }\n }\n\n result = result + '
';\n return result;\n };\n\n\n /**\n * Return the HTML element to insert given the tag contents, which\n * should be either a single integer (size of input element) or\n * two integers separated by a comma (rows and cols of textarea).\n * @param {string} tagContents The text between the delimiters of a gap specifier.\n * @return {string} The HTML for an input or textarea element build\n * according to the given tagContents.\n */\n GapfillerUi.prototype.markUp = function(tagContents) {\n var numbers, result='';\n\n /**\n * The function to handle an 'input' tag.\n * @param {int} size The size of the input element to return.\n * @return {string} The html for a text area to the given specs.\n */\n function input(size) {\n return '';\n }\n\n /**\n * The function to handle a 'textarea' tag.\n * @param {int} rows The number of rows of text required.\n * @param {int} cols The number of columns of text required.\n * @return The HTML for a textarea to the given specs.\n */\n function textarea(rows, cols) {\n return '';\n }\n\n numbers = tagContents.split(',');\n if (numbers.length == 1) {\n result = input(parseInt(numbers[0]));\n } else {\n result = textarea(parseInt(numbers[0]), parseInt(numbers[1]));\n }\n\n return result;\n };\n\n /**\n * Reload the HTML fields from the given serialisation.\n * Unlike other plugins, we don't actually fail the load if, for example\n * the number of fields doesn't match the number of values in the\n * serialisation. We simply set any excess fields for which data\n * in unavailable to '???' or discard extra values. This ensures\n * that at least the unfilled content is presented to the question author\n * when the number of fields is altered during editing.\n */\n GapfillerUi.prototype.reload = function() {\n var\n content = $(this.textArea).val(), // JSON-encoded HTML element settings.\n value,\n values,\n i,\n fields,\n outerDiv = \"
\");\n if (content) {\n try {\n values = JSON.parse(content);\n fields = this.getFields();\n for (i = 0; i < fields.length; i++) {\n value = i < values.length ? values[i] : '???';\n this.setField($(fields[i]), value);\n }\n } catch(e) {\n /**\n * Just ignore errors\n */\n }\n }\n };\n\n GapfillerUi.prototype.resize = function() {}; // Nothing to see here. Move along please.\n\n GapfillerUi.prototype.hasFocus = function() {\n var focused = false;\n this.getFields().each(function() {\n if (this === document.activeElement) {\n focused = true;\n }\n });\n return focused;\n };\n\n /**\n * Destroy the GapFiller UI and serialise the result into the original text area.\n */\n GapfillerUi.prototype.destroy = function() {\n this.sync();\n $(this.htmlDiv).remove();\n this.htmlDiv = null;\n };\n\n return {\n Constructor: GapfillerUi\n };\n});\n"],"names":["define","$","GapfillerUi","textareaId","width","height","uiParams","html","textArea","document","getElementById","readOnly","this","prop","fail","htmlDiv","source","ui_source","alert","attr","replace","reload","prototype","failed","sync","serialisation","empty","getFields","each","value","val","push","JSON","stringify","getElement","find","setField","field","markedUpHtml","reEscape","s","c","result","i","length","j","sepLeft","sepRight","splitter","RegExp","bits","split","markUp","tagContents","numbers","rows","cols","parseInt","values","fields","content","parse","e","resize","hasFocus","focused","activeElement","destroy","remove","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_graph.min.js.map b/amd/build/ui_graph.min.js.map
new file mode 100644
index 000000000..707291cfd
--- /dev/null
+++ b/amd/build/ui_graph.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_graph.min.js","sources":["../src/ui_graph.js"],"sourcesContent":["/**\n * This file is part of Moodle - http:moodle.org/\n *\n * Much of this code is from Finite State Machine Designer:\n */\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n/**\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 util.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/**\n * JavaScript to interface to the Graph editor, which is used both in\n * the author editing page and by the student question submission page.\n *\n * @module qtype_coderunner/ui_graph\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/graphutil', 'qtype_coderunner/graphelements'], function($, util, elements) {\n\n /**\n * Constructor for a GraphCanvas object, which is a wrapper for a Graph's HTML canvas\n * object.\n * @param {object} parent The Graph that owns this object.\n * @param {string} canvasId The ID of the HTML canvas to be wrapped by this object.\n * @param {int} w The required width of the wrapper.\n * @param {int} h The required height of the wrapper.\n */\n function GraphCanvas(parent, canvasId, w, h) {\n this.HANDLE_SIZE = 10;\n\n this.parent = parent;\n this.canvas = $(document.createElement(\"canvas\"));\n this.canvas.attr({\n id: canvasId,\n class: \"coderunner_graphcanvas\",\n tabindex: 1 // So canvas can get focus.\n });\n this.canvas.css({'background-color': 'white'});\n\n this.canvas.on('mousedown', function(e) {\n return parent.mousedown(e);\n });\n\n this.canvas.on('mouseup', function(e) {\n return parent.mouseup(e);\n });\n\n this.canvas.on('dblclick', function(e) {\n return parent.dblclick(e);\n });\n\n this.canvas.on('keydown', function(e) {\n return parent.keydown(e);\n });\n\n this.canvas.on('mousemove', function(e) {\n return parent.mousemove(e);\n });\n\n this.canvas.on('keypress', function(e) {\n return parent.keypress(e);\n });\n\n /**\n * Resize this object to then given dimensions.\n * @param {int} w Required width.\n * @param {int} h Required height.\n */\n this.resize = function(w, h) {\n this.canvas.attr(\"width\", w);\n this.canvas.attr(\"height\", h);\n };\n\n this.resize(w, h);\n }\n\n /**\n * Constructor for the Graph object.\n * This is the ui component for a graph-drawing coderunner question.\n *\n * Relevant ui parameters:\n *\n * isfsm. True if the graph is of a Finite State Machine.\n * If true, the graph can contain an incoming edge from nowhere\n * (the start edge). Default: true.\n * isdirected. True if edges are directed. Default: true.\n * noderadius. The radius of a node, in pixels. Default: 26.\n * fontsize. The font size used for node and edge labels. Default: 20 points.\n * textoffset. An offset in pixels used to determine how far from the link\n * a label is initially positioned. Default 5. Largely defunct\n * now that link text can be dragged.\n * helpmenutext. A string to be used in lieu of the default Help info, if supplied.\n * No default.\n * locknodepositions. True to prevent the user from moving nodes. Useful when the\n * answer box is preloaded with a graph that the student has to\n * annotate by changing node or edge labels or by\n * adding/removing edges. Note, though that nodes can still be\n * added and deleted. See locknodeset.\n * locknodeset. True to prevent the user from adding or deleting nodes, or\n * toggling node types to/from acceptors.\n * locknodelabels: True to prevent the user from editing node labels. This\n * will also prevent any new nodes having non-empty labels.\n * lockedgepositions. True to prevent the user from dragging edges to change\n * their curvature. Possibly useful if the answer box is\n * preloaded with a graph that the student has to annotate by\n * changing node or edge labels or by adding/removing edges.\n * Also ensures that edges added by a student are straight, e.g.\n * to draw a polygon on a set of given points. Note, though that\n * edges can still be added and deleted. See lockedgeset.\n * lockedgeset. True to prevent the user from adding or deleting edges.\n * lockedgelabels: True to prevent the user from editing edge labels. This\n * also prevents any new edges from having labels.\n * @param {string} textareaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\n function Graph(textareaId, width, height, uiParams) {\n /**\n * Constructor.\n */\n var save_this = this;\n\n this.SNAP_TO_PADDING = 6;\n this.DUPLICATE_LINK_OFFSET = 16; // Pixels offset for a duplicate link\n this.HIT_TARGET_PADDING = 6; // Pixels.\n this.DEFAULT_NODE_RADIUS = 26; // Pixels. UI parameter noderadius can override this.\n this.DEFAULT_FONT_SIZE = 20; // px. UI parameter fontsize can override this.\n this.DEFAULT_TEXT_OFFSET = 5; // Link label tweak. UI params can override.\n this.DEFAULT_LINK_LABEL_REL_DIST = 0.5; // Relative distance along link to place labels\n this.MAX_VERSIONS = 30; // Maximum number of versions saved for undo/redo\n\n this.canvasId = 'graphcanvas_' + textareaId;\n this.textArea = $(document.getElementById(textareaId));\n this.helpText = ''; // Obtained by JSON - see below.\n this.readOnly = this.textArea.prop('readonly');\n this.uiParams = uiParams;\n this.graphCanvas = new GraphCanvas(this, this.canvasId, width, height);\n this.caretVisible = true;\n this.caretTimer = 0; // Need global so we can kill a running timer.\n this.originalClick = null;\n this.nodes = [];\n this.links = [];\n this.selectedObject = null; // Either a elements.Link or a elements.Node or a elements.Button.\n this.currentLink = null;\n this.movingObject = false;\n this.fail = false; // Will be set true if reload fails (can't deserialise).\n this.failString = null; // Language string key for fail error message.\n this.versions = [];\n this.versionIndex = -1; //Index of current state of graph in versions list\n\n this.helpBox = new elements.HelpBox(this, 0, 0); // Button that opens a help text box\n this.clearButton = new elements.Button(this, 60, 0, \"Clear\"); // Button that clears the canvas\n this.clearButton.onClick = function() {\n if (confirm(\"Are you sure you want to clear the diagram?\")) {\n this.parent.clear();\n }\n };\n this.buttons = [this.helpBox, this.clearButton];\n\n /**\n * Legacy support for locknodes and lockedges.\n */\n if ('locknodes' in uiParams) {\n uiParams.locknodepositions = uiParams.locknodes;\n }\n if ('lockedges' in uiParams) {\n uiParams.lockedgepositions = uiParams.lockedges;\n }\n\n if ('helpmenutext' in uiParams) {\n this.helpText = uiParams.helpmenutext;\n } else {\n require(['core/str'], function(str) {\n /**\n * Get help text via AJAX.\n */\n var helpPresent = str.get_string('graphhelp', 'qtype_coderunner');\n $.when(helpPresent).done(function(graphhelp) {\n save_this.helpText = graphhelp;\n });\n });\n }\n this.reload();\n if (!this.fail) {\n this.draw();\n }\n }\n\n Graph.prototype.failed = function() {\n return this.fail;\n };\n\n Graph.prototype.failMessage = function() {\n return this.failString;\n };\n\n Graph.prototype.getElement = function() {\n return this.getCanvas();\n };\n\n Graph.prototype.hasFocus = function() {\n return document.activeElement == this.getCanvas();\n };\n\n Graph.prototype.getCanvas = function() {\n var canvas = this.graphCanvas.canvas[0];\n return canvas;\n };\n\n Graph.prototype.nodeRadius = function() {\n return this.uiParams.noderadius ? this.uiParams.noderadius : this.DEFAULT_NODE_RADIUS;\n };\n\n Graph.prototype.fontSize = function() {\n return this.uiParams.fontsize ? this.uiParams.fontsize : this.DEFAULT_FONT_SIZE;\n };\n\n Graph.prototype.isFsm = function() {\n return this.uiParams.isfsm !== undefined ? this.uiParams.isfsm : true;\n };\n\n\n Graph.prototype.textOffset = function() {\n return this.uiParams.textoffset ? this.uiParams.textoffset : this.DEFAULT_TEXT_OFFSET;\n };\n\n /**\n * Draw an arrow head if this is a directed graph. Otherwise do nothing.\n * @param {object} c The graphic context.\n * @param {int} x The x location of the arrow head.\n * @param {int} y The y location of the arrow head.\n * @param {float} angle The angle of the arrow.\n */\n Graph.prototype.arrowIfReqd = function(c, x, y, angle) {\n if (this.uiParams.isdirected === undefined || this.uiParams.isdirected) {\n util.drawArrow(c, x, y, angle);\n }\n };\n\n /**\n * Copy the serialised version of the graph to the TextArea.\n */\n Graph.prototype.sync = function() {\n /**\n * Nothing to do ... always sync'd.\n */\n };\n\n /**\n * Disable autosync, too.\n */\n Graph.prototype.syncIntervalSecs = function() {\n return 0;\n };\n\n Graph.prototype.keypress = function(e) {\n var key = util.crossBrowserKey(e);\n\n if (this.readOnly) {\n return;\n }\n\n if(key >= 0x20 &&\n key <= 0x7E &&\n !e.metaKey &&\n !e.altKey &&\n !e.ctrlKey &&\n key !== 37 && //Don't register arrow keys\n key !== 39 &&\n this.selectedObject !== null &&\n this.canEditText()) {\n if (this.selectedObject.justMoved) {\n this.saveVersion();\n }\n this.selectedObject.justMoved = false;\n this.selectedObject.textBox.insertChar(String.fromCharCode(key));\n this.resetCaret();\n this.draw();\n\n /**\n * Don't let keys do their actions (like space scrolls down the page).\n */\n return false;\n } else if(key === 8 || key === 0x20 || key === 9) {\n /**\n * Disable scrolling on backspace, tab and space.\n */\n return false;\n }\n };\n\n Graph.prototype.mousedown = function(e) {\n var mouse = util.crossBrowserRelativeMousePos(e);\n\n if (this.readOnly) {\n return;\n }\n\n this.selectedObject = this.selectObject(mouse.x, mouse.y);\n this.movingObject = false;\n this.movingGraph = false;\n this.movingText = false;\n this.originalClick = mouse;\n\n this.saveVersion();\n\n if (this.selectedObject !== this.helpBox){\n this.helpBox.helpOpen = false;\n }\n\n if(this.selectedObject !== null) {\n if(this.selectedObject instanceof elements.Button){\n this.selectedObject.onClick();\n } else if(e.shiftKey && this.selectedObject instanceof elements.Node) {\n if (!this.uiParams.lockedgeset) {\n this.currentLink = new elements.SelfLink(this, this.selectedObject, mouse);\n }\n } else if (e.altKey && this.selectedObject instanceof elements.Node) {\n /**\n * Moving an entire connected graph component.\n */\n if (!this.uiParams.locknodepositions) {\n this.movingGraph = true;\n this.movingNodes = this.selectedObject.traverseGraph(this.links, []);\n for (var i = 0; i < this.movingNodes.length; i++) {\n this.movingNodes[i].setMouseStart(mouse.x, mouse.y);\n }\n }\n } else if (this.selectedObject instanceof elements.TextBox){\n if (!this.uiParams.lockedgelabels) {\n this.movingText = true;\n this.selectedObject.setMouseStart(mouse.x, mouse.y);\n this.selectedObject = this.selectedObject.parent;\n }\n } else if (!(this.uiParams.locknodepositions && this.selectedObject instanceof elements.Node) &&\n !(this.uiParams.lockedgepositions && this.selectedObject instanceof elements.Link)){\n this.movingObject = true;\n if(this.selectedObject.setMouseStart) {\n this.selectedObject.setMouseStart(mouse.x, mouse.y);\n }\n }\n this.selectedObject.justMoved = true;\n this.resetCaret();\n } else if(e.shiftKey && this.isFsm()) {\n this.currentLink = new elements.TemporaryLink(this, mouse, mouse);\n }\n\n this.draw();\n\n if(this.hasFocus()) {\n /**\n * Disable drag-and-drop only if the canvas is already focused.\n */\n return false;\n } else {\n /**\n * Otherwise, let the browser switch the focus away from wherever it was.\n */\n this.resetCaret();\n return true;\n }\n };\n\n /**\n * Return true if currently selected object has text that we are allowed\n * to edit.\n */\n Graph.prototype.canEditText = function() {\n var isNode = this.selectedObject instanceof elements.Node,\n isLink = (this.selectedObject instanceof elements.Link ||\n this.selectedObject instanceof elements.SelfLink);\n return 'textBox' in this.selectedObject &&\n ((isNode && !this.uiParams.locknodelabels) ||\n (isLink && !this.uiParams.lockedgelabels));\n };\n\n Graph.prototype.keydown = function(e) {\n var key = util.crossBrowserKey(e), i, nodeDeleted=false;\n\n if (this.readOnly) {\n return;\n }\n\n if(key === 8) { // Backspace key.\n if(this.selectedObject !== null && this.canEditText()) {\n this.selectedObject.textBox.deleteChar();\n this.resetCaret();\n this.draw();\n }\n /**\n * Backspace is a shortcut for the back button, but do NOT want to change pages.\n */\n return false;\n } else if(key === 46 && this.selectedObject !== null) { // Delete key\n this.saveVersion();\n for (i = 0; i < this.nodes.length; i++) {\n if (this.nodes[i] === this.selectedObject && !this.uiParams.locknodeset) {\n this.nodes.splice(i--, 1);\n nodeDeleted = true;\n }\n }\n for (i = 0; i < this.links.length; i++) {\n if((this.links[i] === this.selectedObject && !this.uiParams.lockedgeset) ||\n nodeDeleted && (\n this.links[i].node === this.selectedObject ||\n this.links[i].nodeA === this.selectedObject ||\n this.links[i].nodeB === this.selectedObject)) {\n this.links.splice(i--, 1);\n }\n }\n this.selectedObject = null;\n this.draw();\n } else if(key === 13) { // Enter key.\n if(this.selectedObject !== null) {\n /**\n * Deselect the object.\n */\n this.selectedObject = null;\n this.draw();\n }\n } else if(key === 37) { // Left arrow key\n if(this.selectedObject !== null && this.canEditText()) {\n this.selectedObject.textBox.caretLeft();\n this.resetCaret();\n this.draw();\n }\n } else if(key === 39) { // Right arrow key\n if(this.selectedObject !== null && this.canEditText()) {\n this.selectedObject.textBox.caretRight();\n this.resetCaret();\n this.draw();\n }\n } else if ((e.keyCode == 90 && e.ctrlKey && e.shiftKey) || (e.keyCode == 89 && e.ctrlKey)) { //CTRL+SHIFT+z or CTRL+y\n this.redo();\n } else if (e.keyCode == 90 && e.ctrlKey) { //CTRL+z\n this.undo();\n }\n };\n\n Graph.prototype.dblclick = function(e) {\n var mouse = util.crossBrowserRelativeMousePos(e);\n\n if (this.readOnly || this.uiParams.locknodeset) {\n return;\n }\n\n this.selectedObject = this.selectObject(mouse.x, mouse.y);\n\n this.saveVersion();\n\n if(this.selectedObject === null) {\n this.selectedObject = new elements.Node(this, mouse.x, mouse.y);\n this.nodes.push(this.selectedObject);\n this.selectedObject.justMoved = true;\n this.resetCaret();\n this.draw();\n } else {\n if(this.selectedObject instanceof elements.Node && this.isFsm()) {\n this.selectedObject.isAcceptState = !this.selectedObject.isAcceptState;\n this.draw();\n }\n }\n };\n\n Graph.prototype.resize = function(w, h) {\n this.graphCanvas.resize(w, h);\n this.draw();\n };\n\n Graph.prototype.mousemove = function(e) {\n var mouse = util.crossBrowserRelativeMousePos(e),\n closestPoint;\n\n if (this.readOnly) {\n return;\n }\n\n for (i = 0; i < this.buttons.length; i++){\n if (this.buttons[i].containsPoint(mouse.x, mouse.y)){\n this.buttons[i].highLighted = true;\n }else{\n this.buttons[i].highLighted = false;\n }\n this.draw();\n }\n\n if(this.currentLink !== null) {\n var targetNode = this.selectObject(mouse.x, mouse.y);\n if(!(targetNode instanceof elements.Node)) {\n targetNode = null;\n }\n\n if(this.selectedObject === null) {\n if(targetNode !== null) {\n this.currentLink = new elements.StartLink(this, targetNode, this.originalClick);\n } else {\n this.currentLink = new elements.TemporaryLink(this, this.originalClick, mouse);\n }\n } else {\n if(targetNode === this.selectedObject) {\n this.currentLink = new elements.SelfLink(this, this.selectedObject, mouse);\n } else if(targetNode !== null) {\n this.currentLink = new elements.Link(this, this.selectedObject, targetNode);\n } else {\n closestPoint = this.selectedObject.closestPointOnCircle(mouse.x, mouse.y);\n this.currentLink = new elements.TemporaryLink(this, closestPoint, mouse);\n }\n }\n this.draw();\n }\n if (this.movingGraph) {\n var nodes = this.movingNodes;\n for (var i = 0; i < nodes.length; i++) {\n nodes[i].trackMouse(mouse.x, mouse.y);\n this.snapNode(nodes[i]);\n }\n this.draw();\n } else if(this.movingText){\n this.selectedObject.textBox.setAnchorPoint(mouse.x, mouse.y);\n this.draw();\n } else if(this.movingObject) {\n this.selectedObject.setAnchorPoint(mouse.x, mouse.y);\n if(this.selectedObject instanceof elements.Node) {\n this.snapNode(this.selectedObject);\n }\n this.draw();\n }\n };\n\n Graph.prototype.mouseup = function() {\n\n if (this.readOnly) {\n return;\n }\n\n this.movingObject = false;\n this.movingGraph = false;\n this.movingText = false;\n\n if(this.currentLink !== null) {\n if(!(this.currentLink instanceof elements.TemporaryLink)) {\n this.selectedObject = this.currentLink;\n this.addLink(this.currentLink);\n this.resetCaret();\n }\n this.currentLink = null;\n this.draw();\n }\n };\n\n Graph.prototype.selectObject = function(x, y) {\n for (i = 0; i < this.buttons.length; i++){\n if (this.buttons[i].containsPoint(x, y)){\n return this.buttons[i];\n }\n }\n var i;\n for(i = 0; i < this.nodes.length; i++) {\n if(this.nodes[i].containsPoint(x, y)) {\n return this.nodes[i];\n }\n }\n for(i = 0; i < this.links.length; i++) {\n if(this.links[i].containsPoint(x, y)) {\n return this.links[i];\n }else if ('textBox' in this.links[i] && this.links[i].textBox.containsPoint(x, y)){\n return this.links[i].textBox;\n }\n }\n return null;\n };\n\n Graph.prototype.snapNode = function(node) {\n for(var i = 0; i < this.nodes.length; i++) {\n if(this.nodes[i] === node){\n continue;\n }\n\n if(Math.abs(node.x - this.nodes[i].x) < this.SNAP_TO_PADDING) {\n node.x = this.nodes[i].x;\n }\n\n if(Math.abs(node.y - this.nodes[i].y) < this.SNAP_TO_PADDING) {\n node.y = this.nodes[i].y;\n }\n }\n };\n\n /**\n * Add a new link (always 'this.currentLink') to the set of links.\n * If the link connects two nodes already linked, the angle of the new link\n * is tweaked so it is distinguishable from the existing links.\n * @param {object} newLink The link to be added.\n */\n Graph.prototype.addLink = function(newLink) {\n var maxPerpRHS = null;\n for (var i = 0; i < this.links.length; i++) {\n var link = this.links[i];\n if (link.nodeA === newLink.nodeA && link.nodeB === newLink.nodeB) {\n if (maxPerpRHS === null || link.perpendicularPart > maxPerpRHS) {\n maxPerpRHS = link.perpendicularPart;\n }\n }\n if (link.nodeA === newLink.nodeB && link.nodeB === newLink.nodeA) {\n if (maxPerpRHS === null || -link.perpendicularPart > maxPerpRHS ) {\n maxPerpRHS = -link.perpendicularPart;\n }\n }\n }\n if (maxPerpRHS !== null) {\n newLink.perpendicularPart = maxPerpRHS + this.DUPLICATE_LINK_OFFSET;\n }\n this.links.push(newLink);\n };\n\n Graph.prototype.reload = function() {\n var content = $(this.textArea).val();\n if (content) {\n try {\n /**\n * Load up the student's previous answer if non-empty.\n */\n var backup = JSON.parse(content), i;\n\n for(i = 0; i < backup.nodes.length; i++) {\n var backupNode = backup.nodes[i];\n var backupNodeLayout = backup.nodeGeometry[i];\n var node = new elements.Node(this, backupNodeLayout[0], backupNodeLayout[1]);\n node.isAcceptState = backupNode[1];\n node.textBox = new elements.TextBox(backupNode[0].toString(), node);\n this.nodes.push(node);\n }\n\n for(i = 0; i < backup.edges.length; i++) {\n var backupLink = backup.edges[i];\n var backupLinkLayout = backup.edgeGeometry[i];\n var link = null;\n if(backupLink[0] === backupLink[1]) {\n /**\n * Self link has two identical nodes.\n */\n link = new elements.SelfLink(this, this.nodes[backupLink[0]]);\n link.anchorAngle = backupLinkLayout.anchorAngle;\n link.textBox = new elements.TextBox(backupLink[2].toString(), link);\n if (backupLink.length > 3) {\n link.textBox.setAnchorPoint(backupLink[3].x, backupLink[3].y);\n }\n } else if(backupLink[0] === -1) {\n link = new elements.StartLink(this, this.nodes[backupLink[1]]);\n link.deltaX = backupLinkLayout.deltaX;\n link.deltaY = backupLinkLayout.deltaY;\n } else {\n link = new elements.Link(this, this.nodes[backupLink[0]], this.nodes[backupLink[1]]);\n link.parallelPart = backupLinkLayout.parallelPart;\n link.perpendicularPart = backupLinkLayout.perpendicularPart;\n link.lineAngleAdjust = backupLinkLayout.lineAngleAdjust;\n link.textBox = new elements.TextBox(backupLink[2].toString(), link);\n if (backupLink.length > 3) {\n link.textBox.setAnchorPoint(backupLink[3].x, backupLink[3].y);\n }\n }\n if(link !== null) {\n this.links.push(link);\n }\n }\n } catch(e) {\n this.fail = true;\n this.failString = 'graph_ui_invalidserialisation';\n }\n }\n };\n\n Graph.prototype.save = function() {\n\n var backup = {\n 'edgeGeometry': [],\n 'nodeGeometry': [],\n 'nodes': [],\n 'edges': [],\n };\n var i;\n\n if(!JSON || (this.textArea.val().trim() === '' && this.nodes.length === 0)) {\n return; // Don't save if we have an empty textbox and no graphic content.\n }\n\n for(i = 0; i < this.nodes.length; i++) {\n var node = this.nodes[i];\n\n var nodeData = [node.textBox.text, node.isAcceptState];\n var nodeLayout = [node.x, node.y];\n\n backup.nodeGeometry.push(nodeLayout);\n backup.nodes.push(nodeData);\n }\n\n for(i = 0; i < this.links.length; i++) {\n var link = this.links[i],\n linkData = null,\n linkLayout = null;\n\n if(link instanceof elements.SelfLink) {\n linkLayout = {\n 'anchorAngle': link.anchorAngle,\n };\n linkData = [this.nodes.indexOf(link.node), this.nodes.indexOf(link.node), link.textBox.text];\n if (link.textBox.dragged) {\n linkData.push(link.textBox.position);\n }\n } else if(link instanceof elements.StartLink) {\n linkLayout = {\n 'deltaX': link.deltaX,\n 'deltaY': link.deltaY\n };\n linkData = [-1, this.nodes.indexOf(link.node), \"\"];\n } else if(link instanceof elements.Link) {\n linkLayout = {\n 'lineAngleAdjust': link.lineAngleAdjust,\n 'parallelPart': link.parallelPart,\n 'perpendicularPart': link.perpendicularPart,\n };\n linkData = [this.nodes.indexOf(link.nodeA), this.nodes.indexOf(link.nodeB), link.textBox.text];\n if (link.textBox.dragged) {\n linkData.push(link.textBox.position);\n }\n }\n if (linkData !== null && linkLayout !== null) {\n backup.edges.push(linkData);\n backup.edgeGeometry.push(linkLayout);\n }\n }\n this.textArea.val(JSON.stringify(backup));\n };\n\n Graph.prototype.saveVersion = function () {\n var curState = this.textArea.val();\n if (this.versions.length == 0 || curState.localeCompare(this.versions[this.versionIndex]) != 0){\n this.versionIndex++;\n while (this.versionIndex < this.versions.length){ //Clear newer versions that have been overwritten by this save\n this.versions.pop();\n }\n this.versions.push(curState);\n if (this.versions.length > this.MAX_VERSIONS){ //Limit the size of this.versions\n this.versions.shift();\n this.versionIndex--;\n }\n }\n };\n\n Graph.prototype.undo = function () {\n this.saveVersion();\n if (this.versionIndex > 0){\n this.versionIndex--;\n this.textArea.val(this.versions[this.versionIndex]);\n /**\n * Clear graph nodes and links\n */\n this.nodes = [];\n this.links = [];\n /**\n * Reload graph from serialisation\n */\n this.reload();\n this.draw();\n }\n };\n\n Graph.prototype.redo = function() {\n if (this.versionIndex < this.versions.length - 1){\n this.versionIndex++;\n this.textArea.val(this.versions[this.versionIndex]);\n /**\n * Clear graph nodes and links\n */\n this.nodes = [];\n this.links = [];\n /**\n * Reload graph from serialisation\n */\n this.reload();\n this.draw();\n }\n };\n\n Graph.prototype.clear = function () {\n this.saveVersion();\n this.nodes = [];\n this.links = [];\n this.save();\n this.draw();\n };\n\n Graph.prototype.destroy = function () {\n clearInterval(this.caretTimer); // Stop the caret timer.\n this.graphCanvas.canvas.off(); // Stop all events.\n this.graphCanvas.canvas.remove();\n };\n\n Graph.prototype.resetCaret = function () {\n var t = this; // For embedded function to access this.\n\n clearInterval(this.caretTimer);\n this.caretTimer = setInterval(function() {\n t.caretVisible = !t.caretVisible;\n t.draw();\n }, 500);\n this.caretVisible = true;\n };\n\n Graph.prototype.draw = function () {\n var canvas = this.getCanvas(),\n c = canvas.getContext('2d'),\n i;\n\n c.clearRect(0, 0, this.getCanvas().width, this.getCanvas().height);\n c.save();\n c.translate(0.5, 0.5);\n\n for (i = 0; i < this.buttons.length; i++){\n this.buttons[i].draw(c);\n }\n\n if (!this.helpBox.helpOpen) { // Only proceed if help info not showing.\n\n for(i = 0; i < this.nodes.length; i++) {\n c.lineWidth = 1;\n c.fillStyle = c.strokeStyle = (this.nodes[i] === this.selectedObject) ? 'blue' : 'black';\n this.nodes[i].draw(c);\n }\n for(i = 0; i < this.links.length; i++) {\n c.lineWidth = 1;\n c.fillStyle = c.strokeStyle = (this.links[i] === this.selectedObject\n || this.links[i].textBox === this.selectedObject) ? 'blue' : 'black';\n this.links[i].draw(c);\n }\n if(this.currentLink !== null) {\n c.lineWidth = 1;\n c.fillStyle = c.strokeStyle = 'black';\n this.currentLink.draw(c);\n }\n }\n\n c.restore();\n this.save();\n };\n\n return {\n Constructor: Graph\n };\n});\n"],"names":["define","$","util","elements","GraphCanvas","parent","canvasId","w","h","HANDLE_SIZE","canvas","document","createElement","attr","id","class","tabindex","css","on","e","mousedown","mouseup","dblclick","keydown","mousemove","keypress","resize","Graph","textareaId","width","height","uiParams","save_this","this","SNAP_TO_PADDING","DUPLICATE_LINK_OFFSET","HIT_TARGET_PADDING","DEFAULT_NODE_RADIUS","DEFAULT_FONT_SIZE","DEFAULT_TEXT_OFFSET","DEFAULT_LINK_LABEL_REL_DIST","MAX_VERSIONS","textArea","getElementById","helpText","readOnly","prop","graphCanvas","caretVisible","caretTimer","originalClick","nodes","links","selectedObject","currentLink","movingObject","fail","failString","versions","versionIndex","helpBox","HelpBox","clearButton","Button","onClick","confirm","clear","buttons","locknodepositions","locknodes","lockedgepositions","lockedges","helpmenutext","require","str","helpPresent","get_string","when","done","graphhelp","reload","draw","prototype","failed","failMessage","getElement","getCanvas","hasFocus","activeElement","nodeRadius","noderadius","fontSize","fontsize","isFsm","undefined","isfsm","textOffset","textoffset","arrowIfReqd","c","x","y","angle","isdirected","drawArrow","sync","syncIntervalSecs","key","crossBrowserKey","metaKey","altKey","ctrlKey","canEditText","justMoved","saveVersion","textBox","insertChar","String","fromCharCode","resetCaret","mouse","crossBrowserRelativeMousePos","selectObject","movingGraph","movingText","helpOpen","shiftKey","Node","lockedgeset","SelfLink","movingNodes","traverseGraph","i","length","setMouseStart","TextBox","lockedgelabels","Link","TemporaryLink","isNode","isLink","locknodelabels","nodeDeleted","deleteChar","locknodeset","splice","node","nodeA","nodeB","caretLeft","caretRight","keyCode","redo","undo","push","isAcceptState","closestPoint","containsPoint","highLighted","targetNode","StartLink","closestPointOnCircle","trackMouse","snapNode","setAnchorPoint","addLink","Math","abs","newLink","maxPerpRHS","link","perpendicularPart","content","val","backup","JSON","parse","backupNode","backupNodeLayout","nodeGeometry","toString","edges","backupLink","backupLinkLayout","edgeGeometry","anchorAngle","deltaX","deltaY","parallelPart","lineAngleAdjust","save","trim","nodeData","text","nodeLayout","linkData","linkLayout","indexOf","dragged","position","stringify","curState","localeCompare","pop","shift","destroy","clearInterval","off","remove","t","setInterval","getContext","clearRect","translate","lineWidth","fillStyle","strokeStyle","restore","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_html.min.js.map b/amd/build/ui_html.min.js.map
new file mode 100644
index 000000000..9d3bb59cf
--- /dev/null
+++ b/amd/build/ui_html.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_html.min.js","sources":["../src/ui_html.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 util.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 * Implementation of the html_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a div\n * containing the author-supplied HTML. The serialisation of that HTML,\n * which is what is essentially copied back into the textarea for submissions\n * as the answer, is a JSON object. The fields of that object are the names\n * of all author-supplied HTML elements with a class 'coderunner-ui-element';\n * all such objects are expected to have a 'name' attribute as well. The\n * associated field values are lists. Each list contains all the values, in\n * document order, of the results of calling the jquery val() method in turn\n * on each of the UI elements with that name.\n * This means that at least input, select and textarea\n * elements are supported. The author is responsible for checking the\n * compatibility of other elements with jquery's val() method.\n *\n * The HTML to use in the answer area must be provided as the contents of\n * either the globalextra field or the prototypeextra field in the question\n * authoring form. The choice of which is set by the html_src UI parameter, which\n * must be either 'globalextra' or 'prototypeextra'.\n *\n * If any fields of the answer html are to be preloaded, these should be specified\n * in the answer preload with json of the form '{\"\": \"\",...}'\n * where fieldValueList is a list of all the values to be assigned to the fields\n * with the given name, in document order.\n *\n * To accommodate the possibility of dynamic HTML, any leftover preload values,\n * that is, values that cannot be positioned within the HTML either because\n * there is no field of the required name or because, in the case of a list,\n * there are insufficient elements, are assigned to the data['leftovers']\n * attribute of the outer html div, as a sub-object of the original object.\n * This outer div can be located as the 'closest' (in a jQuery sense)\n * div.qtype-coderunner-html-outer-div. The author-supplied HTML must include\n * JavaScript to make use of the 'leftovers'.\n *\n * As a special case of the serialisation, if all values in the serialisation\n * are either empty strings or a list of empty strings, the serialisation is\n * itself the empty string.\n *\n * @module coderunner/ui_html\n * @copyright Richard Lobb, 2018, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the HtmlUi object.\n * @param {string} textareaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\n function HtmlUi(textareaId, width, height, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n this.textareaId = textareaId;\n var srcField = uiParams.html_src || 'globalextra';\n this.html = this.textArea.attr('data-' + srcField);\n this.html = this.html.replace(/___textareaId___/gm, textareaId);\n this.readOnly = this.textArea.prop('readonly');\n this.uiParams = uiParams;\n this.fail = false;\n this.htmlDiv = null;\n this.reload();\n }\n\n HtmlUi.prototype.failed = function() {\n return this.fail;\n };\n\n\n HtmlUi.prototype.failMessage = function() {\n return 'htmluiloadfail';\n };\n\n\n // Copy the serialised version of the HTML UI area to the TextArea.\n HtmlUi.prototype.sync = function() {\n var\n serialisation = {},\n name,\n empty = true;\n\n this.getFields().each(function() {\n var value, type;\n type = $(this).attr('type');\n name = $(this).attr('name');\n if ((type === 'checkbox' || type === 'radio') && !($(this).is(':checked'))) {\n value = '';\n } else {\n value = $(this).val();\n }\n if (serialisation.hasOwnProperty(name)) {\n serialisation[name].push(value);\n } else {\n serialisation[name] = [value];\n }\n if (value !== '') {\n empty = false;\n }\n });\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n\n HtmlUi.prototype.getElement = function() {\n return this.htmlDiv;\n };\n\n HtmlUi.prototype.getFields = function() {\n return $(this.htmlDiv).find('.coderunner-ui-element');\n };\n\n // Set the value of the jQuery field to the given value.\n // If the field is a radio button or a checkbox and its name matches\n // the given value, the checked attribute is set. Otherwise the field's\n // val() function is called to set the value.\n HtmlUi.prototype.setField = function(field, value) {\n if (field.attr('type') === 'checkbox' || field.attr('type') === 'radio') {\n field.prop('checked', field.val() === value);\n } else {\n field.val(value);\n }\n };\n\n HtmlUi.prototype.reload = function() {\n var\n content = $(this.textArea).val(), // JSON-encoded HTML element settings.\n valuesToLoad,\n values,\n i,\n fields,\n leftOvers,\n outerDivId = 'qtype-coderunner-outer-div-' + this.textareaId.toString(),\n outerDiv = \"
\");\n this.htmlDiv.data('uiparams', this.uiParams); // For use by scripts embedded in html.\n this.htmlDiv.data('templateparams', this.uiParams); // Legacy support only. DEPRECATED.\n if (content) {\n try {\n valuesToLoad = JSON.parse(content);\n leftOvers = {};\n for (var name in valuesToLoad) {\n values = valuesToLoad[name];\n fields = this.getFields().filter(\"[name='\" + name + \"']\");\n leftOvers[name] = [];\n for (i = 0; i < values.length; i++) {\n if (i < fields.length) {\n this.setField($(fields[i]), values[i]);\n } else {\n leftOvers[name].push(values[i]);\n }\n }\n if (leftOvers[name].length === 0) {\n delete leftOvers[name];\n }\n }\n\n if (!$.isEmptyObject(leftOvers)) {\n this.htmlDiv.data('leftovers', leftOvers);\n }\n\n } catch(e) {\n this.fail = true;\n }\n }\n };\n\n HtmlUi.prototype.resize = function() {}; // Nothing to see here. Move along please.\n\n HtmlUi.prototype.hasFocus = function() {\n var focused = false;\n this.getFields().each(function() {\n if (this === document.activeElement) {\n focused = true;\n }\n });\n return focused;\n };\n\n // Destroy the HTML UI and serialise the result into the original text area.\n HtmlUi.prototype.destroy = function() {\n this.sync();\n $(this.htmlDiv).remove();\n this.htmlDiv = null;\n };\n\n return {\n Constructor: HtmlUi\n };\n});\n"],"names":["define","$","HtmlUi","textareaId","width","height","uiParams","textArea","document","getElementById","srcField","html_src","html","this","attr","replace","readOnly","prop","fail","htmlDiv","reload","prototype","failed","failMessage","sync","name","serialisation","empty","getFields","each","value","type","is","val","hasOwnProperty","push","JSON","stringify","getElement","find","setField","field","valuesToLoad","values","i","fields","leftOvers","content","outerDiv","toString","data","parse","filter","length","isEmptyObject","e","resize","hasFocus","focused","activeElement","destroy","remove","Constructor"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DAA,kCAAO,CAAC,WAAW,SAASC,YAQfC,OAAOC,WAAYC,MAAOC,OAAQC,eAClCC,SAAWN,EAAEO,SAASC,eAAeN,kBACrCA,WAAaA,eACdO,SAAWJ,SAASK,UAAY,mBAC/BC,KAAOC,KAAKN,SAASO,KAAK,QAAUJ,eACpCE,KAAOC,KAAKD,KAAKG,QAAQ,qBAAsBZ,iBAC/Ca,SAAWH,KAAKN,SAASU,KAAK,iBAC9BX,SAAWA,cACXY,MAAO,OACPC,QAAU,UACVC,gBAGTlB,OAAOmB,UAAUC,OAAS,kBACfT,KAAKK,MAIhBhB,OAAOmB,UAAUE,YAAc,iBACpB,kBAKXrB,OAAOmB,UAAUG,KAAO,eAGhBC,KADAC,cAAgB,GAEhBC,OAAQ,OAEPC,YAAYC,MAAK,eACdC,MAAOC,KACXA,KAAO9B,EAAEY,MAAMC,KAAK,QACpBW,KAAOxB,EAAEY,MAAMC,KAAK,QAIhBgB,MAHU,aAATC,MAAgC,UAATA,MAAuB9B,EAAEY,MAAMmB,GAAG,YAGlD/B,EAAEY,MAAMoB,MAFR,GAIRP,cAAcQ,eAAeT,MAC7BC,cAAcD,MAAMU,KAAKL,OAEzBJ,cAAcD,MAAQ,CAACK,OAEb,KAAVA,QACAH,OAAQ,MAGZA,WACKpB,SAAS0B,IAAI,SAEb1B,SAAS0B,IAAIG,KAAKC,UAAUX,iBAKzCxB,OAAOmB,UAAUiB,WAAa,kBACnBzB,KAAKM,SAGhBjB,OAAOmB,UAAUO,UAAY,kBAClB3B,EAAEY,KAAKM,SAASoB,KAAK,2BAOhCrC,OAAOmB,UAAUmB,SAAW,SAASC,MAAOX,OACb,aAAvBW,MAAM3B,KAAK,SAAiD,UAAvB2B,MAAM3B,KAAK,QAChD2B,MAAMxB,KAAK,UAAWwB,MAAMR,QAAUH,OAEtCW,MAAMR,IAAIH,QAIlB5B,OAAOmB,UAAUD,OAAS,eAGlBsB,aACAC,OACAC,EACAC,OACAC,UALAC,QAAU9C,EAAEY,KAAKN,UAAU0B,MAO3Be,SAAW,gFADE,8BAAgCnC,KAAKV,WAAW8C,YAC4C,aAExG9B,QAAUlB,EAAE+C,SAAWnC,KAAKD,KAAO,eACnCO,QAAQ+B,KAAK,WAAYrC,KAAKP,eAC9Ba,QAAQ+B,KAAK,iBAAkBrC,KAAKP,UACrCyC,gBAIS,IAAItB,QAFTiB,aAAeN,KAAKe,MAAMJ,SAC1BD,UAAY,GACKJ,aAAc,KAC3BC,OAASD,aAAajB,MACtBoB,OAAShC,KAAKe,YAAYwB,OAAO,UAAY3B,KAAO,MACpDqB,UAAUrB,MAAQ,GACbmB,EAAI,EAAGA,EAAID,OAAOU,OAAQT,IACvBA,EAAIC,OAAOQ,YACNb,SAASvC,EAAE4C,OAAOD,IAAKD,OAAOC,IAEnCE,UAAUrB,MAAMU,KAAKQ,OAAOC,IAGL,IAA3BE,UAAUrB,MAAM4B,eACTP,UAAUrB,MAIpBxB,EAAEqD,cAAcR,iBACZ3B,QAAQ+B,KAAK,YAAaJ,WAGrC,MAAMS,QACCrC,MAAO,IAKxBhB,OAAOmB,UAAUmC,OAAS,aAE1BtD,OAAOmB,UAAUoC,SAAW,eACnBC,SAAU,cACV9B,YAAYC,MAAK,WACdhB,OAASL,SAASmD,gBAClBD,SAAU,MAGXA,SAIXxD,OAAOmB,UAAUuC,QAAU,gBAClBpC,OACLvB,EAAEY,KAAKM,SAAS0C,cACX1C,QAAU,MAGZ,CACH2C,YAAa5D"}
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js b/amd/build/ui_scratchpad.min.js
index ce4cbf97a..a2d09c459 100644
--- a/amd/build/ui_scratchpad.min.js
+++ b/amd/build/ui_scratchpad.min.js
@@ -1,54 +1,3 @@
-define("qtype_coderunner/ui_scratchpad",["exports","core/ajax","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_ajax,_templates,_userinterfacewrapper,_outputdisplayarea){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
-/**
- * Implementation of the scratchpad_ui user interface plugin. For overall details
- * of the UI plugin architecture, see userinterfacewrapper.js.
- *
- * This plugin replaces the usual textarea answer element with a UI is designed to
- * allow the execution of code in the CodeRunner question in a manner similar to an IDE.
- * It contains two editor boxes, one on top of another, allowing users to enter and
- * edit code in both.
- * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,
- * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,
- * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that
- * provides information about how to use the Scratchpad.
- * It's possible to run code 'in-browser' by clicking the Run Button,
- * without making a submission via the Check Button:
- * If Prefix with Answer is not checked, only the code in the Scratchpad is run --
- * allowing for a rough working spot to quickly check the result of code.
- * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is
- * appended to the code in the first editor before being run.
- * The Run Button has some limitations when using its default configuration:
- * Does not support programs that use STDIN (by default);
- * Only supports textual STDOUT (by default).
- * Note: These features can be supported, see the README section on wrappers...
- * The serialisation of this UI is a JSON object with the fields
- * with fields:
- * answer_code: [""] A list containing a string with answer code from the first editor;
- * test_code: [""] A list containing a string with containing answer code from the second editor;
- * show_hide: ["1"] when scratchpad is visible, otherwise [""];
- * prefix_ans: ["1"] when Prefix with Answer is checked, otherwise [""].
- * . The fields of that object are the names
- *
- * UI Parameters:
- * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.
- * - button_name: run button text.
- * - prefix_name: prefix with answer check-box label text.
- * - help_text: help text to show.
- * - run_lang: language used to run code when the run button is clicked,
- * this should be the language your wrapper is written in (if applicable).
- * - wrapper_src: location of wrapper code to be used by the run button, if applicable:
- * setting to globalextra will use text in global extra field,
- * - prototypeextra will use the prototype extra field.
- * - html_output: when true, the output from run will be displayed as raw HTML instead of text.
- * - disable_scratchpad: disable the scratchpad, effectively returning back to Ace UI
- * from student perspective.
- * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.
- * This can be used to swap the default state.
- *
- * @module coderunner/ui_scratchpad
- * @copyright Richard Lobb, 2022, The University of Canterbury
- * @copyright James Napier, 2022, The University of Canterbury
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Constructor=void 0,_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates);const invertSerial=current=>"1"==current[0]?[""]:["1"],overwriteValues=(defaults,prescribed)=>{let overwritten={...defaults};if(prescribed)for(const[key,value]of Object.entries(defaults))overwritten[key]=prescribed[key]||value;return overwritten};_exports.Constructor=class{constructor(textAreaId,width,height,uiParams){const DEF_UI_PARAMS={scratchpad_name:"",button_name:"",prefix_name:"",help_text:"",run_lang:uiParams.lang,html_output:!1,disable_scratchpad:!1,wrapper_src:null};this.textArea=document.getElementById(textAreaId),this.textAreaId=textAreaId,this.height=height,this.readOnly=this.textArea.readonly,this.fail=!1,this.outerDiv=null,this.outputDisplay=null,this.invertPreload=uiParams.invert_prefix,this.lang=uiParams.lang,this.numRows=this.textArea.rows,this.uiParams=overwriteValues(DEF_UI_PARAMS,uiParams),this.runWrapper=this.getRunWrapper(),this.reload()}getRunWrapper(){const wrapperSrc=this.uiParams.wrapper_src;let runWrapper=null;return wrapperSrc&&("globalextra"===wrapperSrc||"prototypeextra"===wrapperSrc?runWrapper=this.textArea.dataset[wrapperSrc]:(this.fail=!0,this.failString="scratchpad_ui_badrunwrappersrc")),runWrapper}failed(){return this.fail}failMessage(){return this.failString}sync(){if(!this.context)return;const serialisation=this.getSerialisation();this.setSerialisation(serialisation)}getSerialisation(){const prefixAns=document.getElementById(this.context.prefix_ans.id),showHide=document.getElementById(this.context.show_hide.id);let serialisation={answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:[""]};return this.answerTextarea&&(serialisation.answer_code=[this.answerTextarea.value]),this.testTextarea&&(serialisation.test_code=[this.testTextarea.value]),showHide&&!(el=>{if(!el.classList.contains("collapse"))throw Error("Element does not have collapse class");return!el.classList.contains("show")})(showHide)&&(serialisation.show_hide=["1"]),(null!=prefixAns&&prefixAns.checked||this.context.disable_scratchpad)&&(serialisation.prefix_ans=["1"]),this.invertPreload&&(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans)),serialisation}setSerialisation(serialisation){serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),Object.values(serialisation).some((val=>1===val.length&&val[0].length>0))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}getElement(){return this.outerDiv}handleRunButtonClick(){this.sync();const preloadString=this.textArea.value,serial=this.readJson(preloadString),sandboxParams=this.uiParams.params,language=this.lang,code=(answerCode=serial.answer_code,testCode=serial.test_code,prefixAns=serial.prefix_ans[0],(template=this.runWrapper)||(template="{{ ANSWER_CODE }}\n{{ SCRATCHPAD_CODE }}"),prefixAns||(answerCode=""),(template=template.replaceAll("{{ ANSWER_CODE }}",answerCode)).replaceAll("{{ SCRATCHPAD_CODE }}",testCode));var answerCode,testCode,prefixAns,template;this.outputDisplay.handleRunButtonClick(code,language,sandboxParams)}updateContext(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}readJson(preloadString){let serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch{serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}async reload(){const preloadString=this.textArea.value;let preload;try{preload=this.readJson(preloadString)}catch(error){return this.fail=!0,void(this.failString="scratchpad_ui_invalidserialisation")}this.updateContext(preload);try{const{html:html}=await _templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context),outputMode=this.uiParams.html_output?"html":"text";this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,outputMode),this.addEventListeners()}catch(e){return this.fail=!0,void(this.failString="scratchpad_ui_templateloadfail")}}drawUi(html){const wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}addAceUis(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}addEventListeners(){const runButton=document.getElementById(this.textAreaId+"_run-btn"),outputDisplayarea=document.getElementById(this.context.output_display.id);runButton&&runButton.addEventListener("click",(()=>this.handleRunButtonClick(_ajax.default,outputDisplayarea)))}resize(){}hasFocus(){var _this$answerCodeUi,_this$testCodeUi;let focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}destroy(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}}));
+define("qtype_coderunner/ui_scratchpad",["exports","core/ajax","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_ajax,_templates,_userinterfacewrapper,_outputdisplayarea){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}function _defineProperties(target,props){for(var i=0;iarr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0}))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}},{key:"getElement",value:function(){return this.outerDiv}},{key:"handleRunButtonClick",value:function(){this.sync();var answerCode,testCode,prefixAns,template,preloadString=this.textArea.value,serial=this.readJson(preloadString),sandboxParams=this.uiParams.params,language=this.lang,code=(answerCode=serial.answer_code,testCode=serial.test_code,prefixAns=serial.prefix_ans[0],(template=this.runWrapper)||(template="{{ ANSWER_CODE }}\n{{ SCRATCHPAD_CODE }}"),prefixAns||(answerCode=""),(template=template.replaceAll("{{ ANSWER_CODE }}",answerCode)).replaceAll("{{ SCRATCHPAD_CODE }}",testCode));this.outputDisplay.handleRunButtonClick(code,language,sandboxParams)}},{key:"updateContext",value:function(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}},{key:"readJson",value:function(preloadString){var serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch(_unused){serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}},{key:"reload",value:(fn=regeneratorRuntime.mark((function _callee(){var preloadString,preload,_yield$Templates$rend,html,outputMode;return regeneratorRuntime.wrap((function(_context){for(;;)switch(_context.prev=_context.next){case 0:preloadString=this.textArea.value,_context.prev=1,preload=this.readJson(preloadString),_context.next=10;break;case 5:return _context.prev=5,_context.t0=_context.catch(1),this.fail=!0,this.failString="scratchpad_ui_invalidserialisation",_context.abrupt("return");case 10:return this.updateContext(preload),_context.prev=11,_context.next=14,_templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);case 14:_yield$Templates$rend=_context.sent,html=_yield$Templates$rend.html,outputMode=this.uiParams.html_output?"html":"text",this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,outputMode),this.addEventListeners(),_context.next=28;break;case 23:return _context.prev=23,_context.t1=_context.catch(11),this.fail=!0,this.failString="scratchpad_ui_templateloadfail",_context.abrupt("return");case 28:case"end":return _context.stop()}}),_callee,this,[[1,5],[11,23]])})),_reload=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(){return _reload.apply(this,arguments)})},{key:"drawUi",value:function(html){var wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}},{key:"addAceUis",value:function(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}},{key:"addEventListeners",value:function(){var _this=this,runButton=document.getElementById(this.textAreaId+"_run-btn"),outputDisplayarea=document.getElementById(this.context.output_display.id);runButton&&runButton.addEventListener("click",(function(){return _this.handleRunButtonClick(_ajax.default,outputDisplayarea)}))}},{key:"resize",value:function(){}},{key:"hasFocus",value:function(){var _this$answerCodeUi,_this$testCodeUi,focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}},{key:"destroy",value:function(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}],protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),ScratchpadUi}();_exports.Constructor=ScratchpadUi}));
//# sourceMappingURL=ui_scratchpad.min.js.map
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js.map b/amd/build/ui_scratchpad.min.js.map
new file mode 100644
index 000000000..668f63c1f
--- /dev/null
+++ b/amd/build/ui_scratchpad.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n * . The fields of that object are the names\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - html_output: when true, the output from run will be displayed as raw HTML instead of text.\n * - disable_scratchpad: disable the scratchpad, effectively returning back to Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport ajax from 'core/ajax';\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = current => current[0] == '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text.\n * @param {string} testCode text.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template) => {\n if (!template) {\n template = '{{ ANSWER_CODE }}\\n' +\n '{{ SCRATCHPAD_CODE }}';\n }\n if (!prefixAns) {\n answerCode = '';\n }\n template = template.replaceAll('{{ ANSWER_CODE }}', answerCode);\n template = template.replaceAll('{{ SCRATCHPAD_CODE }}', testCode);\n return template;\n};\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the defualt value.\n * Does not add keys/values to the result if that key is not in defualts.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with defualt values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!el.classList.contains('collapse')) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n html_output: false,\n disable_scratchpad: false,\n wrapper_src: null\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang; // Todo: this vs this.ui params\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n let serialisation = {\n answer_code: [''],\n test_code: [''],\n show_hide: [''],\n prefix_ans: ['']\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const sandboxParams = this.uiParams.params;\n const language = this.lang;\n const code = fillWrapper(\n serial.answer_code,\n serial.test_code,\n serial.prefix_ans[0],\n this.runWrapper\n );\n // TODO: handle case where no output display area exists...\n this.outputDisplay.handleRunButtonClick(code, language, sandboxParams);\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuerry friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n const outputMode = this.uiParams.html_output ? 'html' : 'text';\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(this.context.output_display.id, outputMode); // TODO: change!\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n\n return;\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n const outputDisplayarea = document.getElementById(this.context.output_display.id);\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick(ajax, outputDisplayarea));\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","overwriteValues","defaults","prescribed","overwritten","Object","entries","key","value","ScratchpadUi","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","run_lang","lang","html_output","disable_scratchpad","wrapper_src","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","reload","wrapperSrc","dataset","failString","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","test_code","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","checked","values","some","val","length","JSON","stringify","sync","answerCode","testCode","template","preloadString","serial","readJson","sandboxParams","params","language","code","replaceAll","handleRunButtonClick","preload","text","render","CSS","escape","parse","hasOwnProperty","TypeError","updateContext","Templates","renderForPromise","html","outputMode","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","outputDisplayarea","addEventListener","_this","ajax","focused","_this$answerCodeUi","uiInstance","hasFocus","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_table.min.js.map b/amd/build/ui_table.min.js.map
new file mode 100644
index 000000000..0c2069ba6
--- /dev/null
+++ b/amd/build/ui_table.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui_table.min.js","sources":["../src/ui_table.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 util.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 * Implementation of the table_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a div\n * containing an HTML table. The number of columns, and\n * the initial number of rows are specified by required UI parameters\n * num_columns and num_rows respectively.\n * Optional additional UI parameters are:\n * 1. column_headers: a list of strings that can be used to provide a\n * fixed header row at the top.\n * 2. row_labels: a list of strings that can be used to provide a\n * fixed row label column at the left.\n * 3. dynamic_rows, which, if true, allows the user to add rows.\n * 4. locked_cells: a list of [row, column] pairs, being the coordinates\n * of table cells that cannot be changed by the user. row and column numbers\n * are zero origin and do not include the header row or the row labels.\n * 5. width_percents: a list of the percentages of the width occupied\n * by each column. This list must include a value for the row labels, if present.\n *\n * The serialisation of the table, which is what is essentially copied back\n * into the textarea for submissions as the answer, is a JSON array. Each\n * element in the array is itself an array containing the values of one row\n * of the table. Empty cells are empty strings. The table header row and row\n * label columns are not provided in the serialisation.\n *\n * To preload the table with data, simply set the answer_preload of the question\n * to a json array of row values (each itself an array). If the number of rows\n * in the preload exceeds the number set by num_rows, extra rows are\n * added. If the number is less than num_rows, or if there is no\n * answer preload, undefined rows are simply left blank.\n *\n * As a special case of the serialisation, if all cells in the serialisation\n * are empty strings, the serialisation is itself the empty string.\n *\n * @module qtype_coderunner/ui_table\n * @copyright Richard Lobb, 2018, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the TableUI object.\n * @param {string} textareaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\n function TableUi(textareaId, width, height, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n this.readOnly = this.textArea.prop('readonly');\n this.tableDiv = null;\n this.uiParams = uiParams;\n if (!uiParams.num_columns ||\n !uiParams.num_rows) {\n this.fail = true;\n this.failString = 'table_ui_missingparams';\n return; // We're dead, fred.\n }\n\n this.fail = false;\n this.lockedCells = uiParams.locked_cells || [];\n this.hasHeader = uiParams.column_headers && uiParams.column_headers.length > 0 ? true : false;\n this.hasRowLabels = uiParams.row_labels && uiParams.row_labels.length > 0 ? true : false;\n this.numDataColumns = uiParams.num_columns;\n this.rowsPerCell = uiParams.lines_per_cell || 2;\n this.totNumColumns = this.numDataColumns + (this.hasRowLabels ? 1 : 0);\n this.columnWidths = this.computeColumnWidths();\n this.reload();\n }\n\n // Return an array of the percentage widths required for each of the\n // totNumColumns columns.\n TableUi.prototype.computeColumnWidths = function() {\n var defaultWidth = Math.trunc(100 / this.totNumColumns),\n columnWidths = [];\n if (this.uiParams.column_width_percents && this.uiParams.column_width_percents.length > 0) {\n return this.uiParams.column_width_percents;\n } else if (Array.prototype.fill) { // Anything except bloody IE.\n return new Array(this.totNumColumns).fill(defaultWidth);\n } else { // IE. What else?\n for (var i = 0; i < this.totNumColumns; i++) {\n columnWidths.push(defaultWidth);\n }\n return columnWidths;\n }\n };\n\n // Return True if the cell at the given row and column is locked.\n // The given row and column numbers exclude column headers and row labels.\n TableUi.prototype.isLockedCell = function(row, col) {\n for (var i = 0; i < this.lockedCells.length; i++) {\n if (this.lockedCells[i][0] == row && this.lockedCells[i][1] == col) {\n return true;\n }\n }\n return false;\n };\n\n TableUi.prototype.getElement = function() {\n return this.tableDiv;\n };\n\n TableUi.prototype.failed = function() {\n return this.fail;\n };\n\n TableUi.prototype.failMessage = function() {\n return this.failString;\n };\n\n // Copy the serialised version of the Table UI area to the TextArea.\n TableUi.prototype.sync = function() {\n var\n serialisation = [],\n empty = true,\n tableRows = $(this.tableDiv).find('table tbody tr');\n\n tableRows.each(function () {\n var rowValues = [];\n $(this).find('textarea').each(function () {\n var cellVal = $(this).val();\n rowValues.push(cellVal);\n if (cellVal) {\n empty = false;\n }\n });\n serialisation.push(rowValues);\n });\n\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Return the HTML for row number iRow.\n TableUi.prototype.tableRow = function(iRow, preload) {\n var html = '
', widthIndex = 0, width;\n\n // Insert the row label if required.\n if (this.hasRowLabels) {\n width = this.columnWidths[0];\n widthIndex = 1;\n html += \"
\";\n if (iRow < this.uiParams.row_labels.length) {\n html += this.uiParams.row_labels[iRow];\n }\n html += \"
\";\n }\n\n for (var iCol = 0; iCol < this.numDataColumns; iCol++) {\n width = this.columnWidths[widthIndex++];\n html += \"
\";\n html += '';\n html += \"
\";\n }\n html += '
';\n return html;\n };\n\n // Return the HTML for the table's head section.\n TableUi.prototype.tableHeadSection = function() {\n var html = \"\\n\",\n colIndex = 0; // Column index including row label if present.\n\n if (this.hasHeader) {\n html += \"
\";\n if (iCol < this.uiParams.column_headers.length) {\n html += this.uiParams.column_headers[iCol];\n }\n colIndex++;\n html += \"
\";\n }\n html += \"
\\n\";\n }\n html += \"\\n\";\n return html;\n };\n\n // Build the HTML table, filling it with the data from the serialisation\n // currently in the textarea (if there is any).\n TableUi.prototype.reload = function() {\n var\n preloadJson = $(this.textArea).val(), // JSON-encoded table values.\n preload = [],\n divHtml = \"
\\n\" +\n \"
\\n\";\n\n if (preloadJson) {\n try {\n preload = JSON.parse(preloadJson);\n } catch(error) {\n this.fail = true;\n this.failString = 'table_ui_invalidjson';\n return;\n }\n }\n\n try {\n // Build the table head section.\n divHtml += this.tableHeadSection();\n\n // Build the table body. Each table cell has a textarea inside it,\n // except for row labels (if present).\n divHtml += \"\\n\";\n var num_rows_required = Math.max(this.uiParams.num_rows, preload.length);\n for (var iRow = 0; iRow < num_rows_required; iRow++) {\n divHtml += this.tableRow(iRow, preload);\n }\n\n divHtml += '\\n
\\n
';\n this.tableDiv = $(divHtml);\n if (this.uiParams.dynamic_rows) {\n this.addButtons();\n }\n } catch (error) {\n this.fail = true;\n this.failString = 'table_ui_invalidserialisation';\n }\n };\n\n // Add 'Add row' and 'Delete row' buttons at the end of the table.\n TableUi.prototype.addButtons = function() {\n var deleteButtonHtml = '',\n deleteButton = $(deleteButtonHtml),\n t = this;\n this.tableDiv.append(deleteButton);\n deleteButton.click(function() {\n var numRows = t.tableDiv.find('table tbody tr').length,\n lastRow = t.tableDiv.find('tr:last');\n if (numRows > t.uiParams.num_rows) {\n lastRow.remove();\n }\n lastRow = t.tableDiv.find('tr:last'); // New last row.\n if (numRows == t.uiParams.num_rows + 1) {\n $(this).prop('disabled', true);\n }\n });\n\n var addButtonHtml = '',\n addButton = $(addButtonHtml);\n t.tableDiv.append(addButton);\n addButton.click(function() {\n var lastRow, newRow;\n lastRow = t.tableDiv.find('table tbody tr:last');\n newRow = lastRow.clone(); // Copy the last row of the table.\n newRow.find('textarea').each(function() { // Clear all td elements in it.\n $(this).val('');\n });\n lastRow.after(newRow);\n $(this).prev().prop('disabled', false);\n });\n };\n\n TableUi.prototype.resize = function() {}; // Nothing to see here. Move along please.\n\n TableUi.prototype.hasFocus = function() {\n var focused = false;\n $(this.tableDiv).find('textarea').each(function() {\n if (this === document.activeElement) {\n focused = true;\n }\n });\n return focused;\n };\n\n // Destroy the HTML UI and serialise the result into the original text area.\n TableUi.prototype.destroy = function() {\n this.sync();\n $(this.tableDiv).remove();\n this.tableDiv = null;\n };\n\n return {\n Constructor: TableUi\n };\n});\n"],"names":["define","$","TableUi","textareaId","width","height","uiParams","textArea","document","getElementById","readOnly","this","prop","tableDiv","num_columns","num_rows","fail","failString","lockedCells","locked_cells","hasHeader","column_headers","length","hasRowLabels","row_labels","numDataColumns","rowsPerCell","lines_per_cell","totNumColumns","columnWidths","computeColumnWidths","reload","prototype","defaultWidth","Math","trunc","column_width_percents","Array","fill","i","push","isLockedCell","row","col","getElement","failed","failMessage","sync","serialisation","empty","find","each","rowValues","cellVal","val","JSON","stringify","tableRow","iRow","preload","html","widthIndex","iCol","tableHeadSection","colIndex","preloadJson","divHtml","parse","error","num_rows_required","max","dynamic_rows","addButtons","deleteButton","t","append","click","numRows","lastRow","remove","addButton","newRow","clone","after","prev","resize","hasFocus","focused","activeElement","destroy","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/userinterfacewrapper.min.js b/amd/build/userinterfacewrapper.min.js
index 0825142de..09ae46ac3 100644
--- a/amd/build/userinterfacewrapper.min.js
+++ b/amd/build/userinterfacewrapper.min.js
@@ -96,6 +96,6 @@
* 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)
*
*****************************************************************************/
-define("qtype_coderunner/userinterfacewrapper",["jquery"],(function($){function InterfaceWrapper(uiname,textareaId){let t=this;this.GUTTER=14,this.DEFAULT_SYNC_INTERVAL_SECS=5;this.taId=textareaId,this.loadFailId=textareaId+"_loadfailerr";const ta=document.getElementById(textareaId);this.textArea=$(ta);const params=this.textArea.attr("data-params");this.uiParams=params?JSON.parse(params):{},this.uiParams.lang=this.textArea.attr("data-lang"),this.readOnly=this.textArea.prop("readonly"),this.isLoading=!1,this.loadFailed=!1,this.retries=0;let h=parseInt(this.textArea.css("height")),content_lines=this.textArea.val().split("\n").length,rows=ta.rows;content_lines>rows&&(rows=Math.min(content_lines,50)),h=Math.max(h,19*rows,50),this.wrapperNode=$(""),this.textArea.after(this.wrapperNode),this.wrapperNode.hide(),this.wrapperNode.css({resize:"vertical",overflow:"hidden",minHeight:h,width:"100%",border:"1px solid darkgrey"}),this.textArea.data("current-ui-wrapper",this),this.uiInstance=null,this.loadUi(uiname,this.uiParams),$(document).mousemove((function(){t.checkForResize()})),$(window).resize((function(){t.checkForResize()})),this.textArea.closest("form").submit((function(){null!==t.uiInstance&&t.uiInstance.sync()})),$(document.body).on("keydown",(function(e){77===e.keyCode&&e.ctrlKey&&e.altKey&&(null!==t.uiInstance||t.loadFailed?t.stop():t.restart())}))}return InterfaceWrapper.prototype.loadUi=function(uiname,params){const t=this;function syncIntervalSecsBase(){return params.hasOwnProperty("sync_interval_secs")?parseInt(params.sync_interval_secs):t.DEFAULT_SYNC_INTERVAL_SECS}if(this.isLoading)return this.retries+=1,void(this.retries>20?(alert("Failed to load "+uiname+" UI component. If this error persists, please report it to the forum on coderunner.org.nz"),this.retries=0,this.loading=0):setTimeout((function(){t.loadUi(uiname,params)}),200));this.retries=0,this.params=params,this.stop(),this.uiname=uiname,""===this.uiname||"none"===this.uiname||sessionStorage.getItem("disableUis")?this.uiInstance=null:(this.isLoading=!0,require(["qtype_coderunner/ui_"+this.uiname],(function(ui){const h=t.wrapperNode.innerHeight()-t.GUTTER,w=t.wrapperNode.innerWidth(),uiInstance=new ui.Constructor(t.taId,w,h,params);if(uiInstance.failed()){t.loadFailed=!0,t.wrapperNode.hide(),uiInstance.destroy(),t.uiInstance=null,t.textArea.addClass("uiloadfailed");const loadFailDiv='';let jqLoadFailDiv=$(loadFailDiv);jqLoadFailDiv.insertBefore(t.textArea),langString=uiInstance.failMessage(),errorDiv=jqLoadFailDiv,require(["core/str"],(function(str){const s=str.get_string(langString,"qtype_coderunner"),fallback=str.get_string("ui_fallback","qtype_coderunner");$.when(s,fallback).done((function(s,fallback){errorDiv.html(s+" "+fallback)}))}))}else{t.hLast=0,t.wLast=0,t.textArea.hide(),t.wrapperNode.show(),t.wrapperNode.append(uiInstance.getElement()),t.uiInstance=uiInstance,t.loadFailed=!1,t.checkForResize();let uiInstancePrototype=Object.getPrototypeOf(uiInstance);uiInstancePrototype.syncIntervalSecs=uiInstancePrototype.syncIntervalSecs||syncIntervalSecsBase,t.startSyncTimer(uiInstance)}var langString,errorDiv;t.isLoading=!1})))},InterfaceWrapper.prototype.startSyncTimer=function(uiInstance){const timeout=uiInstance.syncIntervalSecs();this.uiInstance.timer=timeout?setInterval((function(){uiInstance.sync()}),1e3*timeout):null},InterfaceWrapper.prototype.stopSyncTimer=function(uiInstance){uiInstance.timer&&clearTimeout(uiInstance.timer)},InterfaceWrapper.prototype.stop=function(){null!==this.uiInstance&&(this.stopSyncTimer(this.uiInstance),this.textArea.show(),this.uiInstance.hasFocus()&&(this.textArea.focus(),this.textArea[0].selectionStart=this.textArea[0].value.length),this.uiInstance.destroy(),this.uiInstance=null,this.wrapperNode.hide()),this.loadFailed=!1,this.textArea.removeClass("uiloadfailed"),$(document.getElementById(this.loadFailId)).remove()},InterfaceWrapper.prototype.restart=function(){null===this.uiInstance&&this.loadUi(this.uiname,this.params)},InterfaceWrapper.prototype.checkForResize=function(){if(this.uiInstance){const h=this.wrapperNode.innerHeight(),w=this.wrapperNode.innerWidth();if(h!=this.hLast||w!=this.wLast){const xLeft=this.wrapperNode.offset().left,maxWidth=$(window).innerWidth()-xLeft-25,hAdjusted=h-this.GUTTER,wAdjusted=Math.min(maxWidth,w);this.uiInstance.resize(wAdjusted,hAdjusted),this.hLast=this.wrapperNode.innerHeight(),this.wLast=this.wrapperNode.innerWidth()}}},{newUiWrapper:function(uiname,textareaId){return uiname?new InterfaceWrapper(uiname,textareaId):null},InterfaceWrapper:InterfaceWrapper}}));
+define("qtype_coderunner/userinterfacewrapper",["jquery"],(function($){function InterfaceWrapper(uiname,textareaId){var t=this;this.GUTTER=14,this.DEFAULT_SYNC_INTERVAL_SECS=5;this.taId=textareaId,this.loadFailId=textareaId+"_loadfailerr";var ta=document.getElementById(textareaId);this.textArea=$(ta);var params=this.textArea.attr("data-params");this.uiParams=params?JSON.parse(params):{},this.uiParams.lang=this.textArea.attr("data-lang"),this.readOnly=this.textArea.prop("readonly"),this.isLoading=!1,this.loadFailed=!1,this.retries=0;var h=parseInt(this.textArea.css("height")),content_lines=this.textArea.val().split("\n").length,rows=ta.rows;content_lines>rows&&(rows=Math.min(content_lines,50)),h=Math.max(h,19*rows,50),this.wrapperNode=$(""),this.textArea.after(this.wrapperNode),this.wrapperNode.hide(),this.wrapperNode.css({resize:"vertical",overflow:"hidden",minHeight:h,width:"100%",border:"1px solid darkgrey"}),this.textArea.data("current-ui-wrapper",this),this.uiInstance=null,this.loadUi(uiname,this.uiParams),$(document).mousemove((function(){t.checkForResize()})),$(window).resize((function(){t.checkForResize()})),this.textArea.closest("form").submit((function(){null!==t.uiInstance&&t.uiInstance.sync()})),$(document.body).on("keydown",(function(e){77===e.keyCode&&e.ctrlKey&&e.altKey&&(null!==t.uiInstance||t.loadFailed?t.stop():t.restart())}))}return InterfaceWrapper.prototype.loadUi=function(uiname,params){var t=this;function syncIntervalSecsBase(){return params.hasOwnProperty("sync_interval_secs")?parseInt(params.sync_interval_secs):t.DEFAULT_SYNC_INTERVAL_SECS}if(this.isLoading)return this.retries+=1,void(this.retries>20?(alert("Failed to load "+uiname+" UI component. If this error persists, please report it to the forum on coderunner.org.nz"),this.retries=0,this.loading=0):setTimeout((function(){t.loadUi(uiname,params)}),200));this.retries=0,this.params=params,this.stop(),this.uiname=uiname,""===this.uiname||"none"===this.uiname||sessionStorage.getItem("disableUis")?this.uiInstance=null:(this.isLoading=!0,require(["qtype_coderunner/ui_"+this.uiname],(function(ui){var langString,errorDiv,h=t.wrapperNode.innerHeight()-t.GUTTER,w=t.wrapperNode.innerWidth(),uiInstance=new ui.Constructor(t.taId,w,h,params);if(uiInstance.failed()){t.loadFailed=!0,t.wrapperNode.hide(),uiInstance.destroy(),t.uiInstance=null,t.textArea.addClass("uiloadfailed");var loadFailDiv='',jqLoadFailDiv=$(loadFailDiv);jqLoadFailDiv.insertBefore(t.textArea),langString=uiInstance.failMessage(),errorDiv=jqLoadFailDiv,require(["core/str"],(function(str){var s=str.get_string(langString,"qtype_coderunner"),fallback=str.get_string("ui_fallback","qtype_coderunner");$.when(s,fallback).done((function(s,fallback){errorDiv.html(s+" "+fallback)}))}))}else{t.hLast=0,t.wLast=0,t.textArea.hide(),t.wrapperNode.show(),t.wrapperNode.append(uiInstance.getElement()),t.uiInstance=uiInstance,t.loadFailed=!1,t.checkForResize();var uiInstancePrototype=Object.getPrototypeOf(uiInstance);uiInstancePrototype.syncIntervalSecs=uiInstancePrototype.syncIntervalSecs||syncIntervalSecsBase,t.startSyncTimer(uiInstance)}t.isLoading=!1})))},InterfaceWrapper.prototype.startSyncTimer=function(uiInstance){var timeout=uiInstance.syncIntervalSecs();this.uiInstance.timer=timeout?setInterval((function(){uiInstance.sync()}),1e3*timeout):null},InterfaceWrapper.prototype.stopSyncTimer=function(uiInstance){uiInstance.timer&&clearTimeout(uiInstance.timer)},InterfaceWrapper.prototype.stop=function(){null!==this.uiInstance&&(this.stopSyncTimer(this.uiInstance),this.textArea.show(),this.uiInstance.hasFocus()&&(this.textArea.focus(),this.textArea[0].selectionStart=this.textArea[0].value.length),this.uiInstance.destroy(),this.uiInstance=null,this.wrapperNode.hide()),this.loadFailed=!1,this.textArea.removeClass("uiloadfailed"),$(document.getElementById(this.loadFailId)).remove()},InterfaceWrapper.prototype.restart=function(){null===this.uiInstance&&this.loadUi(this.uiname,this.params)},InterfaceWrapper.prototype.checkForResize=function(){if(this.uiInstance){var h=this.wrapperNode.innerHeight(),w=this.wrapperNode.innerWidth();if(h!=this.hLast||w!=this.wLast){var xLeft=this.wrapperNode.offset().left,maxWidth=$(window).innerWidth()-xLeft-25,hAdjusted=h-this.GUTTER,wAdjusted=Math.min(maxWidth,w);this.uiInstance.resize(wAdjusted,hAdjusted),this.hLast=this.wrapperNode.innerHeight(),this.wLast=this.wrapperNode.innerWidth()}}},{newUiWrapper:function(uiname,textareaId){return uiname?new InterfaceWrapper(uiname,textareaId):null},InterfaceWrapper:InterfaceWrapper}}));
//# sourceMappingURL=userinterfacewrapper.min.js.map
\ No newline at end of file
diff --git a/amd/build/userinterfacewrapper.min.js.map b/amd/build/userinterfacewrapper.min.js.map
new file mode 100644
index 000000000..631c835e8
--- /dev/null
+++ b/amd/build/userinterfacewrapper.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"userinterfacewrapper.min.js","sources":["../src/userinterfacewrapper.js"],"sourcesContent":["/******************************************************************************\n *\n * This module provides a wrapper for user-interface modules, handling hiding\n * of the textArea that is being replaced by the UI element,\n * resizing of the UI component, and support of such usability functions as\n * ctrl-alt-M to disable/re-enable the entire user interface, including the\n * wrapper.\n *\n * @module coderunner/userinterfacewrapper\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * The InterfaceWrapper class is constructed either by Moodle PHP calls of\n * the form\n *\n * $PAGE->requires->js_call_amd($modulename, $functionname, $params)\n *\n * (e.g. from within render.php) or by JavaScript require calls, e.g. from\n * authorform.js when the question author changes UI type.\n *\n * The InterfaceWrapper provides:\n *\n * 1. A constructor InterfaceWrapper(uiname, textareaId) which\n * hides the given text area, replaces it with a wrapper div (resizable in\n * height by the user but with width resizing managed by changes in window\n * width), created an instance of nameInstance as defined in the file\n * ui_name.js (see below).\n * params is a record containing the decoded value of\n *\n * 2. A stop() method that destroys the embedded UI and hides the wrapper.\n *\n * 3. A restart() method that shows the wrapper again and re-creates the prior\n * embedded UI component within it.\n *\n * 4. A loadUi(uiname, params) method that kills any currently running UI element\n * (if there is one) and (re)loads the specified one. The params parameter\n * is a record that allows additional parameters to be passed in, such as\n * those from the question's uiParams field and, in the case of the\n * Ace UI, the 'lang' (language) that the editor is editing. This data\n * is supplied by the PHP via the data-params attribute of the answer's\n * base textarea.\n *\n * 5. Regular checking for any resizing of the wrapper, which are passed on to\n * the embedded UI element's resize() method.\n *\n * 6. Monitoring of alt-ctrl-M key presses which toggle the visibility of the\n * wrapper plus UI element and the syncronised textArea by calls to stop()\n * and restart\n *\n * =========================================================================\n *\n * The embedded user-interface module must be defined in a JavaScript file\n * of the form ui_name.js which must define a class nameInstance with\n * the following functionality:\n *\n * 1. A constructor SomeUiName(textareaId, width, height, params) that\n * builds an HTML component of the given width and height. textareaId is the\n * ID of the textArea from which the UI element should obtain its initial\n * serialisation and to which it should write the serialisation when its save\n * or destroy methods are called. params is a JavaScript object,\n * decoded from the JSON uiParams defined by the question plus any\n * additional data required, such as the 'lang' in the case of Ace.\n *\n * 2. A getElement() method that returns the HTML element that the\n * InterfaceWrapper is to insert into the document tree.\n *\n * 3. A method failed() that should return true unless the constructor\n * failed (e.g. because it was not able to de-serialise the text area's\n * contents). The wrapper will call destroy() on the object if failed()\n * returns true and abort the use of the UI element. The text area will\n * have the uiloadfailed class added, which CSS will display in some\n * error mode (e.g. a red border).\n *\n * 4. A method failMessage() that will be called only when failed() returns\n * True. It should be a defined CodeRunner language string key.\n *\n * 5. A sync() method that copies the serialised represention of the UI plugin's\n * data to the related TextArea. This is used when submit is clicked.\n *\n * 6. A destroy() method that should sync the contents to the text area then\n * destroy any HTML elements or other created content. This method is called\n * when CTRL-ALT-M is typed by the user to turn off all UI plugins\n *\n * 7. A resize(width, height) method that should resize the entire UI element\n * to the given dimensions.\n *\n * 8. A hasFocus() method that returns true if the UI element has focus.\n *\n * 9. A syncIntervalSecs() method that returns the time interval between\n * calls to the sync() method. 0 for no sync calls. The userinterfacewrapper\n * provides all instances with a generic (base-class) version that returns\n * the value of a UI parameter sync_interval_secs if given else uses the\n * UI interface wrapper default (currently 10).\n *\n * The return value from the module define is a record with a single field\n * 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)\n *\n *****************************************************************************/\n\n/**\n * 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 util.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\ndefine(['jquery'], function($) {\n /**\n * Constructor for a new user interface.\n * @param {string} uiname The name of the interface element (e.g. ace, graph, etc)\n * which should be in file ui_ace.js, ui_graph.js etc.\n * @param {string} textareaId The id of the text area that the UI is to manage.\n * The text area should have an attribute data-params, which is a\n * JSON encoded record containing whatever additional parameters might\n * be needed by the User interface. As a minimum it should contain all\n * the parameters from the uiparameters field of\n * the question so that question authors can pass in additional data\n * such as whether graph edges are bidirectional or not in the case of\n * the graph UI. Additionally the Ace editor requires a 'lang' field\n * to specify what language the editor is editing.\n * When the wrapper has been set up on a text area, the text area's\n * data attribute contains an entry for 'current-ui-wrapper' that is\n * a reference to the wrapper ('this').\n */\n function InterfaceWrapper(uiname, textareaId) {\n let t = this; // For use by embedded functions.\n\n this.GUTTER = 14; // Size of gutter at base of wrapper Node (pixels)\n this.DEFAULT_SYNC_INTERVAL_SECS = 5;\n\n const PIXELS_PER_ROW = 19; // For estimating height of textareas.\n const MAX_GROWN_ROWS = 50; // Upper limit to artifically grown textarea rows.\n const MIN_WRAPPER_HEIGHT = 50;\n\n this.taId = textareaId;\n this.loadFailId = textareaId + '_loadfailerr';\n const ta = document.getElementById(textareaId);\n this.textArea = $(ta);\n const params = this.textArea.attr('data-params');\n if (params) {\n this.uiParams = JSON.parse(params);\n } else {\n this.uiParams = {};\n }\n this.uiParams.lang = this.textArea.attr('data-lang');\n this.readOnly = this.textArea.prop('readonly');\n this.isLoading = false; // True if we're busy loading a UI element.\n this.loadFailed = false; // True if UI failed to initialise properly.\n this.retries = 0; // Number of failed attempts to load a UI component.\n\n let h = parseInt(this.textArea.css(\"height\"));\n let content_lines = this.textArea.val().split('\\n').length;\n let rows = ta.rows;\n if (content_lines > rows) {\n // Allow reloaded text areas with lots of text to grow bigger, within limits.\n rows = Math.min(content_lines, MAX_GROWN_ROWS);\n }\n h = Math.max(h, rows * PIXELS_PER_ROW, MIN_WRAPPER_HEIGHT);\n\n /**\n * Construct an empty hidden wrapper div, inserted directly after the\n * textArea, ready to contain the actual UI.\n */\n this.wrapperNode = $(\"\");\n this.textArea.after(this.wrapperNode);\n this.wrapperNode.hide();\n this.wrapperNode.css({\n resize: 'vertical',\n overflow: 'hidden',\n minHeight: h,\n width: \"100%\",\n border: \"1px solid darkgrey\"\n });\n\n /**\n * Record a reference to this wrapper in the text area's data attribute\n * for use by external javascript that needs to interact with the\n * wrapper, e.g. the multilanguage.js module.\n */\n this.textArea.data('current-ui-wrapper', this);\n\n /**\n * Load the UI into the wrapper (aysnchronous).\n */\n this.uiInstance = null; // Defined by loadUi asynchronously\n this.loadUi(uiname, this.uiParams); // Load the required UI element\n\n /**\n * Add event handlers\n */\n $(document).mousemove(function() {\n t.checkForResize();\n });\n $(window).resize(function() {\n t.checkForResize();\n });\n this.textArea.closest('form').submit(function() {\n if (t.uiInstance !== null) {\n t.uiInstance.sync();\n }\n });\n $(document.body).on('keydown', function(e) {\n const KEY_M = 77;\n if (e.keyCode === KEY_M && e.ctrlKey && e.altKey) {\n if (t.uiInstance !== null || t.loadFailed) {\n t.stop();\n } else {\n t.restart(); // Reactivate\n }\n }\n });\n }\n\n /**\n * Load the specified UI element (which in the case of Ace will need\n * to know the language, lang, as well - this must be supplied as\n * a 'lang' attribute of the record params.\n * When ui is up and running, this.uiInstance will reference it.\n * To avoid a potential race problem, if this method is already busy\n * with a load, try again in 200 msecs.\n * @param {string} uiname The name of the User Interface to be used.\n * @param {object} params The UI parameters object that passes parameters\n * to the actual UI object.\n */\n InterfaceWrapper.prototype.loadUi = function(uiname, params) {\n const t = this,\n errPart1 = 'Failed to load ',\n errPart2 = ' UI component. If this error persists, please report it to the forum on coderunner.org.nz';\n\n /**\n * Get the given language string and plug it into the given jQuery\n * div element as its html, plus a 'fallback' message on a separate line.\n * @param {string} langString The language string specifier for the error message,\n * to be loaded by AJAX.\n * @param {object} errorDiv The div object into which the error message\n * is to be inserted.\n */\n function setLoadFailMessage(langString, errorDiv) {\n require(['core/str'], function(str) {\n /**\n * Get langString text via AJAX\n */\n const\n s = str.get_string(langString, 'qtype_coderunner'),\n fallback = str.get_string('ui_fallback', 'qtype_coderunner');\n $.when(s, fallback).done(function(s, fallback) {\n errorDiv.html(s + ' ' + fallback);\n });\n });\n }\n\n /**\n * The default method for a UIs sync_interval_secs method.\n * Returns the sync_interval_secs parameter if given, else\n * DEFAULT_SYNC_INTERVAL_SECS.\n */\n function syncIntervalSecsBase() {\n if (params.hasOwnProperty('sync_interval_secs')) {\n return parseInt(params.sync_interval_secs);\n } else {\n return t.DEFAULT_SYNC_INTERVAL_SECS;\n }\n }\n\n if (this.isLoading) { // Oops, we're loading a UI element already\n this.retries += 1;\n if (this.retries > 20) {\n alert(errPart1 + uiname + errPart2);\n this.retries = 0;\n this.loading = 0;\n } else {\n setTimeout(function() {\n t.loadUi(uiname, params);\n }, 200); // Try again in 200 msecs\n }\n return;\n }\n this.retries = 0;\n this.params = params; // Save in case need to restart\n\n this.stop(); // Kill any active UI first\n this.uiname = uiname;\n\n if (this.uiname === '' || this.uiname === 'none' || sessionStorage.getItem('disableUis')) {\n this.uiInstance = null;\n } else {\n this.isLoading = true;\n require(['qtype_coderunner/ui_' + this.uiname],\n function(ui) {\n const h = t.wrapperNode.innerHeight() - t.GUTTER;\n const w = t.wrapperNode.innerWidth();\n const uiInstance = new ui.Constructor(t.taId, w, h, params);\n if (uiInstance.failed()) {\n /*\n * Constructor failed to load serialisation.\n * Set uiloadfailed class on text area.\n */\n t.loadFailed = true;\n t.wrapperNode.hide();\n uiInstance.destroy();\n t.uiInstance = null;\n t.textArea.addClass('uiloadfailed');\n const loadFailDiv = '';\n let jqLoadFailDiv = $(loadFailDiv);\n jqLoadFailDiv.insertBefore(t.textArea);\n setLoadFailMessage(uiInstance.failMessage(), jqLoadFailDiv); // Insert error by AJAX\n } else {\n t.hLast = 0; // Force resize (and hence redraw)\n t.wLast = 0; // ... on first call to checkForResize\n t.textArea.hide();\n t.wrapperNode.show();\n t.wrapperNode.append(uiInstance.getElement());\n t.uiInstance = uiInstance;\n t.loadFailed = false;\n t.checkForResize();\n\n /*\n * Set a default syncIntervalSecs method if uiInstance lacks one.\n */\n let uiInstancePrototype = Object.getPrototypeOf(uiInstance);\n uiInstancePrototype.syncIntervalSecs = uiInstancePrototype.syncIntervalSecs || syncIntervalSecsBase;\n t.startSyncTimer(uiInstance);\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Start a sync timer on the given uiInstance, unless its time interval is 0.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.startSyncTimer = function(uiInstance) {\n const timeout = uiInstance.syncIntervalSecs();\n if (timeout) {\n this.uiInstance.timer = setInterval(function () {\n uiInstance.sync();\n }, timeout * 1000);\n } else {\n this.uiInstance.timer = null;\n }\n };\n\n\n /**\n * Stop the sync timer on the given uiInstance, if running.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.stopSyncTimer = function(uiInstance) {\n if (uiInstance.timer) {\n clearTimeout(uiInstance.timer);\n }\n };\n\n\n InterfaceWrapper.prototype.stop = function() {\n /*\n * Disable (shutdown) the embedded ui component.\n * The wrapper remains active for ctrl-alt-M events, but is hidden.\n */\n if (this.uiInstance !== null) {\n this.stopSyncTimer(this.uiInstance);\n this.textArea.show();\n if (this.uiInstance.hasFocus()) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n this.uiInstance.destroy();\n this.uiInstance = null;\n this.wrapperNode.hide();\n }\n this.loadFailed = false;\n this.textArea.removeClass('uiloadfailed'); // Just in case it failed before\n $(document.getElementById(this.loadFailId)).remove();\n };\n\n /*\n * Re-enable the ui element (e.g. after alt-cntrl-M). This is\n * a full re-initialisation of the ui element.\n */\n InterfaceWrapper.prototype.restart = function() {\n if (this.uiInstance === null) {\n /**\n * Restart the UI component in the textarea\n */\n this.loadUi(this.uiname, this.params);\n }\n };\n\n\n /**\n * Check for wrapper resize - propagate to ui element.\n */\n InterfaceWrapper.prototype.checkForResize = function() {\n const SIZE_HACK = 25; // Horrible but best I can do. TODO: FIXME\n\n if (this.uiInstance) {\n const h = this.wrapperNode.innerHeight();\n const w = this.wrapperNode.innerWidth();\n if (h != this.hLast || w != this.wLast) {\n const xLeft = this.wrapperNode.offset().left;\n const maxWidth = $(window).innerWidth() - xLeft - SIZE_HACK;\n const hAdjusted = h - this.GUTTER;\n const wAdjusted = Math.min(maxWidth, w);\n this.uiInstance.resize(wAdjusted, hAdjusted);\n this.hLast = this.wrapperNode.innerHeight();\n this.wLast = this.wrapperNode.innerWidth();\n }\n }\n };\n\n /**\n * The external entry point from the PHP.\n * @param {string} uiname The name of the User Interface to use e.g. 'ace'\n * @param {string} textareaId The ID of the textarea to be wrapped.\n */\n function newUiWrapper(uiname, textareaId) {\n if (uiname) {\n return new InterfaceWrapper(uiname, textareaId);\n } else {\n return null;\n }\n }\n\n\n return {\n newUiWrapper: newUiWrapper,\n InterfaceWrapper: InterfaceWrapper\n };\n});\n"],"names":["define","$","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","taId","loadFailId","ta","document","getElementById","textArea","params","attr","uiParams","JSON","parse","lang","readOnly","prop","isLoading","loadFailed","retries","h","parseInt","css","content_lines","val","split","length","rows","Math","min","max","wrapperNode","after","hide","resize","overflow","minHeight","width","border","data","uiInstance","loadUi","mousemove","checkForResize","window","closest","submit","sync","body","on","e","keyCode","ctrlKey","altKey","stop","restart","prototype","syncIntervalSecsBase","hasOwnProperty","sync_interval_secs","alert","loading","setTimeout","sessionStorage","getItem","require","ui","langString","errorDiv","innerHeight","w","innerWidth","Constructor","failed","destroy","addClass","loadFailDiv","jqLoadFailDiv","insertBefore","failMessage","str","s","get_string","fallback","when","done","html","hLast","wLast","show","append","getElement","uiInstancePrototype","Object","getPrototypeOf","syncIntervalSecs","startSyncTimer","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","remove","xLeft","offset","left","maxWidth","hAdjusted","wAdjusted","newUiWrapper"],"mappings":"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"}
\ No newline at end of file
From 99771bd046c00f4f8601e6da432dc2b842fa5267 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Thu, 2 Feb 2023 12:57:36 +1300
Subject: [PATCH 053/188] Version date update.
---
version.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/version.php b/version.php
index 814f62037..7f0329923 100644
--- a/version.php
+++ b/version.php
@@ -22,7 +22,7 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2022111600;
+$plugin->version = 2023013002;
$plugin->requires = 2022041900;
$plugin->cron = 0;
$plugin->component = 'qtype_coderunner';
From 2f95ba8af711bb3ff22f7704d16065837b10df26 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Thu, 2 Feb 2023 12:58:48 +1300
Subject: [PATCH 054/188] Add UI parameter to control automatic extraction of
code from a JSON record such as produced by Scratchpad UI. Also allow
user-changes to the Ace theme via the Ace menu to stick. And add a few UI
parameters to give question authors more control on the Ace UI
appearance/behaviour.
---
amd/src/ui_ace.js | 54 +++++++++++++++++++++++++++++++--------------
amd/src/ui_ace.json | 2 +-
2 files changed, 38 insertions(+), 18 deletions(-)
diff --git a/amd/src/ui_ace.js b/amd/src/ui_ace.js
index 40d029cdb..a287ff643 100644
--- a/amd/src/ui_ace.js
+++ b/amd/src/ui_ace.js
@@ -59,7 +59,7 @@ define(['jquery'], function($) {
try {
window.ace.require("ace/ext/language_tools");
this.modelist = window.ace.require('ace/ext/modelist');
-
+ this.textareaId = textareaId;
this.textarea = textarea;
this.enabled = false;
this.contents_changed = false;
@@ -93,24 +93,25 @@ define(['jquery'], function($) {
code = this.extract_from_json_maybe(code);
}
session.setValue(code);
- if (params.theme) {
+
+ // If there's a user-defined theme in local storage, use that.
+ // Otherwise use the 'prefers-color-scheme' option if given or
+ // the question/system defaults if not.
+ const userTheme = window.localStorage.getItem('qtype_coderunner.ace.theme');
+ const consider_prefers = (params.auto_switch_light_dark === undefined || params.auto_switch_light_dark)
+ && window.matchMedia;
+ if (userTheme !== null) {
+ this.editor.setTheme(userTheme);
+ } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ this.editor.setTheme(ACE_DARK_THEME);
+ } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: light)').matches) {
+ this.editor.setTheme(ACE_LIGHT_THEME);
+ } else if (params.theme) {
this.editor.setTheme("ace/theme/" + params.theme);
} else {
this.editor.setTheme(ACE_LIGHT_THEME);
}
- // If auto dark switching allowed, and user-prefers-color-scheme
- // is defined, set to the dark theme accordingly.
- // Logically this should work in both directions, switching from a
- // dark theme to a light theme if requested. But Chrome under Linux
- // always matches 'light' so this breaks. As a workaround, only
- // one way switching (light to dark) is supported.
- if (params.auto_switch_dark === undefined || params.auto_switch_dark) {
- if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
- this.editor.setTheme(ACE_DARK_THEME);
- }
- }
-
this.fixSlowLoad();
this.setLanguage(lang);
@@ -167,14 +168,33 @@ define(['jquery'], function($) {
return 'ace_ui_notready';
};
-
// Sync to TextArea
AceWrapper.prototype.sync = function() {
- // Nothing to do ... always sync'd
+ // The data is always sync'd to the text area. But here we use sync to
+ // poll the value of the current theme and record in browser local
+ // storage if the value for this particular Ace instance has changed
+ // (as recorded in session storage). The theme to use for all Ace windows
+ // from now on in this browser is recorded in browser local storage.
+ const thisThemeNow = this.editor.getOption('theme');
+ const thisEditorKey = `qtype_coderunner.${this.textareaId}.theme`;
+ const lastTheme = window.sessionStorage.getItem(thisEditorKey);
+ const globalKey = 'qtype_coderunner.ace.theme';
+ const globalTheme = window.localStorage.getItem(globalKey);
+
+ if (lastTheme !== null && lastTheme != thisThemeNow) {
+ // The theme in this Ace window has been changed by the user.
+ window.localStorage.setItem(globalKey, thisThemeNow);
+ } else if (globalTheme && globalTheme !== thisThemeNow) {
+ // A theme has been defined by another Ace window since we loaded.
+ // Switch to that theme.
+ this.editor.setOption('theme', globalTheme);
+ thisThemeNow = globalTheme;
+ }
+ window.sessionStorage.setItem(thisEditorKey, thisThemeNow);
};
AceWrapper.prototype.syncIntervalSecs = function() {
- return 0;
+ return 2;
};
AceWrapper.prototype.setLanguage = function(language) {
diff --git a/amd/src/ui_ace.json b/amd/src/ui_ace.json
index 23ec6d67f..2b687c47f 100644
--- a/amd/src/ui_ace.json
+++ b/amd/src/ui_ace.json
@@ -1,7 +1,7 @@
{
"name": "ui_ace",
"parameters": {
- "auto_switch_dark": {
+ "auto_switch_light_dark": {
"type": "boolean",
"default": true
},
From 87cdb1363fa31a7db8640ab7759637e6e141914a Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Thu, 2 Feb 2023 13:02:55 +1300
Subject: [PATCH 055/188] Add functionality for extracting answer code from
JSON produced by the Scratchpad UI (and similar).
---
classes/bulk_tester.php | 3 +-
classes/constants.php | 2 +
classes/jobrunner.php | 11 +
db/install.xml | 1 +
db/upgrade.php | 17 +
edit_coderunner_form.php | 3 +
lang/en/qtype_coderunner.php | 7 +-
questiontestrun.php | 2 +-
questiontype.php | 5 +-
samples/genericpythonoutputonly.xml | 307 +++++++++---------
samples/graphuidemoquestion.xml | 187 +++++------
.../programtestingprototypeandexamples.xml | 4 +
samples/randomisationexamples.xml | 34 +-
samples/trivialnodejsquestion.xml | 1 +
samples/uoctkinterprototype.xml | 1 +
.../sqrexportwithsampleattachment.xml | 1 +
tests/helper.php | 5 +
tests/prototype_test.php | 1 +
tests/walkthrough_randomisation_test.php | 1 +
19 files changed, 329 insertions(+), 264 deletions(-)
diff --git a/classes/bulk_tester.php b/classes/bulk_tester.php
index 774c547ba..3c9116832 100644
--- a/classes/bulk_tester.php
+++ b/classes/bulk_tester.php
@@ -289,8 +289,7 @@ private function test_question($question) {
core_php_time_limit::raise(60); // Prevent PHP timeouts.
gc_collect_cycles(); // Because PHP's default memory management is rubbish.
$question->start_attempt(null);
- $answer = $question->answer;
- $response = array('answer' => $answer);
+ $response = $question->get_correct_answer();
// Check if it's a multilanguage question; if so need to determine
// what language (either specified by answer_language template param, or
// the AceLang default or the first).
diff --git a/classes/constants.php b/classes/constants.php
index ffffc8bc3..858b57afb 100644
--- a/classes/constants.php
+++ b/classes/constants.php
@@ -53,4 +53,6 @@ class constants {
const JOBE_HOST_DEFAULT_API_KEY = '2AAA7A5415B4A9B394B54BF1D2E9D';
const DEFAULT_NUM_ROWS = 18; // Default answerbox size.
+
+ const ANSWER_CODE_KEY = 'answer_code'; // The key to the code in a Scratchpad UI question .
}
diff --git a/classes/jobrunner.php b/classes/jobrunner.php
index ce14ed582..fe51514b8 100644
--- a/classes/jobrunner.php
+++ b/classes/jobrunner.php
@@ -23,6 +23,8 @@
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/coderunner/questiontype.php');
+use qtype_coderunner\constants;
+
// The qtype_coderunner_jobrunner class contains all code concerned with running a question
// in the sandbox and grading the result.
class qtype_coderunner_jobrunner {
@@ -51,6 +53,15 @@ public function run_tests($question, $code, $attachments, $testcases, $isprechec
$outcome->set_status(qtype_coderunner_testing_outcome::STATUS_MISSING_PROTOTYPE, $message);
return $outcome;
}
+
+ // Extra the code from JSON if this is a Scratchpad UI or similar.
+ if ($question->extractcodefromjson) {
+ $json = json_decode($code, true);
+ if ($json !== null and isset($json[constants::ANSWER_CODE_KEY])) {
+ $code = $json[constants::ANSWER_CODE_KEY][0];
+ }
+ }
+
$this->question = $question;
$this->code = $code;
$this->testcases = array_values($testcases);
diff --git a/db/install.xml b/db/install.xml
index 374e24971..9eebfbb66 100644
--- a/db/install.xml
+++ b/db/install.xml
@@ -35,6 +35,7 @@
+
diff --git a/db/upgrade.php b/db/upgrade.php
index 1ba811914..31246f2c9 100644
--- a/db/upgrade.php
+++ b/db/upgrade.php
@@ -432,6 +432,23 @@ function xmldb_qtype_coderunner_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2022012703, 'qtype', 'coderunner');
}
+ if ($oldversion < 2023013002) {
+
+ // Define field extractcodefromjson to be added to question_coderunner_options.
+ $table = new xmldb_table('question_coderunner_options');
+ $field = new xmldb_field('extractcodefromjson', XMLDB_TYPE_INTEGER, '1', null, null, null, '1', 'hoisttemplateparams');
+
+ // Conditionally launch add field extractcodefromjson.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ } else {
+ $dbman->change_field_type($table, $field); // Change from NOTNULL to NULL if already there.
+ }
+
+ // Coderunner savepoint reached.
+ upgrade_plugin_savepoint(true, 2023013002, 'qtype', 'coderunner');
+ }
+
require_once(__DIR__ . '/upgradelib.php');
update_question_types();
diff --git a/edit_coderunner_form.php b/edit_coderunner_form.php
index 5d2bbf28d..0a72a3b95 100644
--- a/edit_coderunner_form.php
+++ b/edit_coderunner_form.php
@@ -618,6 +618,8 @@ private function make_questiontype_panel($mform) {
$twigelements = array();
$twigelements[] = $mform->createElement('advcheckbox', 'hoisttemplateparams', null,
get_string('hoisttemplateparams', 'qtype_coderunner'));
+ $twigelements[] = $mform->createElement('advcheckbox', 'extractcodefromjson', null,
+ get_string('extractcodefromjson', 'qtype_coderunner'));
$twigelements[] = $mform->createElement('advcheckbox', 'twigall', null,
get_string('twigall', 'qtype_coderunner'));
$templateparamlangs = array(
@@ -643,6 +645,7 @@ private function make_questiontype_panel($mform) {
$mform->$hidemethod('templateparamsevalpertry', 'templateparamslang', 'eq', 'None');
$mform->$hidemethod('templateparamsevalpertry', 'templateparamslang', 'eq', 'twig');
$mform->setDefault('hoisttemplateparams', true);
+ $mform->setDefault('extractcodefromjson', true);
$mform->addHelpButton('twigcontrols', 'twigcontrols', 'qtype_coderunner');
// UI parameters.
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index 6176dfff7..00a57523a 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -25,7 +25,7 @@
$string['aborted'] = 'Testing was aborted due to error.';
$string['ace_gapfillerui_ui_source_descr'] = '"globalextra" to take the code to display from the globalextra field or "test0" to take it from the testcode field of the first test';
$string['ace_ui_notready'] = 'Ace editor not ready. Perhaps reload page?';
-$string['aceui_auto_switch_dark_descr'] = 'Allow a user, browser or OS preference for dark themes to override a preset Ace light theme.';
+$string['aceui_auto_switch_light_dark_descr'] = 'Allow a user, browser or OS preference for dark themes to override a preset Ace light theme.';
$string['aceui_live_autocompletion_descr'] = 'Enable the Ace editor\'s live autocompletion mode.';
$string['aceui_font_size_descr'] = 'Ace editor font size.';
$string['aceui_import_from_scratchpad_descr'] = 'True to allow the Ace editor to receive the JSON-format answer used by the scratchpad UI and extract the answer code from it. Facilitates switching UIs. Leave true unless you want Ace to edit JSON objects with an "answer_code" key.';
@@ -180,6 +180,7 @@
$string['exportthisquestion_help'] = 'This will create a Moodle XML export file containing just this one question. One example of when this is useful if you think this question demonstrates a bug in CodeRunner that you would like to report to the developers.';
$string['extra'] = 'Extra template data';
$string['extra_help'] = 'A sometimes-useful extra text field for use by the template, accessed as {{TEST.extra}}';
+$string['extractcodefromjson'] = "Ace/Scratchpad compliant";
$string['fail'] = 'Fail';
$string['fails'] = 'failures';
@@ -1146,6 +1147,10 @@ function should be applied, e.g. {{STUDENT_ANSWER | e(\'py\')}} is
checkbox is checked, the parameters are hoisted into the Twig global name space
and can be referenced simply as {{someparam}}.
+Ace/Scratchpad compliance allows seamless switching between the Ace and
+Scratchpad UIs. Leave checked unless you wish Ace to be able to edit a JSON string
+with an "answer_code" key, which would be taken to be a Scratchpad serialisation.
+
If Twig All is checked, Twig macro expansion is applied to the
question text, sample answer, answer preload, UI parameters and all test case fields
using the template parameters as an environment. You will usually
diff --git a/questiontestrun.php b/questiontestrun.php
index dc2a0b3ab..4d1dbe31e 100644
--- a/questiontestrun.php
+++ b/questiontestrun.php
@@ -24,7 +24,7 @@
*
* The script takes one parameter id which is a questionid as a parameter.
* Only the latest version of the given question is tested.
- *
+ *
* @package qtype_coderunner
* @copyright 2012 the Open University, 2016 Richard Lobb, The University of Canterbury.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
diff --git a/questiontype.php b/questiontype.php
index 6fef0c82b..9a72b47ad 100644
--- a/questiontype.php
+++ b/questiontype.php
@@ -109,6 +109,7 @@ public function extra_question_fields() {
'sandboxparams',
'templateparams',
'hoisttemplateparams',
+ 'extractcodefromjson',
'templateparamslang',
'templateparamsevalpertry',
'templateparamsevald',
@@ -148,6 +149,7 @@ public static function noninherited_fields() {
'validateonsave',
'templateparams',
'hoisttemplateparams',
+ 'extractcodefromjson',
'templateparamslang',
'templateparamsevalpertry',
'templateparamsevald',
@@ -456,7 +458,7 @@ public function set_inherited_fields($target, $prototype) {
* Only the most recent version of each prototype question is returned.
* @param int $courseid the ID of the course whose prototypes are required.
* @return stdClass[] prototype rows from question_coderunner_options,
- * including count number of occurrences.
+ * including count number of occurrences.
*/
public static function get_all_prototypes($courseid) {
global $DB;
@@ -726,6 +728,7 @@ public function
'uiparameters' => null,
'hidecheck' => 0,
'attachments' => 0,
+ 'extractcodefromjson' => 1,
'giveupallowed' => 0,
);
diff --git a/samples/genericpythonoutputonly.xml b/samples/genericpythonoutputonly.xml
index 1b3bb336e..35567686f 100644
--- a/samples/genericpythonoutputonly.xml
+++ b/samples/genericpythonoutputonly.xml
@@ -23,166 +23,166 @@
011100
- 1
- Text output:\n
{output}
\n\n"
-else:
- html += f"
No text output
\n"
-
-files = sorted(os.listdir())
-for filename in files:
- if filename.endswith('.png'):
- data_uri = make_data_uri(filename)
- html += f"""
{filename}
-
-"""
-
-# Lastly print the JSON-encoded result required of a combinator grader
-print(json.dumps({'epiloguehtml': html,
- 'showoutputonly': True
-}))
-
+ Text output:\n
{output}
\n\n"
+else:
+ html += f"
No text output
\n"
+
+files = sorted(os.listdir())
+for filename in files:
+ if filename.endswith('.png'):
+ data_uri = make_data_uri(filename)
+ html += f"""
{filename}
+
+"""
+
+# Lastly print the JSON-encoded result required of a combinator grader
+print(json.dumps({'epiloguehtml': html,
+ 'showoutputonly': True
+}))
+
]]>
10
- 1#\n|ms]]>
@@ -195,6 +195,7 @@ g.render('graph', format='png', view=False)]]>
1
+ 10ace2
diff --git a/samples/graphuidemoquestion.xml b/samples/graphuidemoquestion.xml
index f9bcfe070..4fa0ae459 100644
--- a/samples/graphuidemoquestion.xml
+++ b/samples/graphuidemoquestion.xml
@@ -27,96 +27,96 @@
- #"
-INF = float('inf')
-
-error_count = 0
-def error(s):
- global error_count
- print(s)
- error_count += 1
-
-try:
- graph_rep = json.loads(student_answer)
- node_id_to_name_map = {}
- for i, node in enumerate(graph_rep['nodes']):
- node_id_to_name_map[i] = node[0] if node[0] != '' else ('#' + str(i))
- edges = graph_rep['edges']
- graph = {}
- for node_id, node_name in sorted(node_id_to_name_map.items()):
- edges = []
- for id0, id1, edge_label in graph_rep['edges']:
- if id0 == node_id:
- edges.append((node_id_to_name_map[id1], edge_label))
- edges.sort()
- graph[node_name] = edges
-
-except json.JSONDecodeError as e:
- raise Exception("Oops. Illegal graph received (exception {}). Please report (unless you did something silly yourself)".format(e))
-
-# Now we add Floyd-Warshall
-
-def allPairsShortestPath(g):
- """Return distance dictionary (a map from (u, v) to distance) computed
- by Floyd-Warshall.
- """
- dist = {}
- for u in g:
- for v in g:
- dist[u, v] = INF
- dist[u, u] = 0
-
- for v, edge_label in g[u]:
- try:
- d = int(edge_label)
- except ValueError:
- d = 0
- dist[u, v] = d
-
- for mid in g:
- for u in g:
- for v in g:
- newlen = dist[u, mid] + dist[mid, v]
- if newlen < dist[u, v]:
- dist[u, v] = newlen
-
- return dist
-
-distances = allPairsShortestPath(graph)
-
-{% for TEST in TESTCASES %}
-{{ TEST.testcode }}
-{{ TEST.extra }}
-{% if not loop.last %}
-print(SEPARATOR)
-{% endif %}
+ #"
+INF = float('inf')
+
+error_count = 0
+def error(s):
+ global error_count
+ print(s)
+ error_count += 1
+
+try:
+ graph_rep = json.loads(student_answer)
+ node_id_to_name_map = {}
+ for i, node in enumerate(graph_rep['nodes']):
+ node_id_to_name_map[i] = node[0] if node[0] != '' else ('#' + str(i))
+ edges = graph_rep['edges']
+ graph = {}
+ for node_id, node_name in sorted(node_id_to_name_map.items()):
+ edges = []
+ for id0, id1, edge_label in graph_rep['edges']:
+ if id0 == node_id:
+ edges.append((node_id_to_name_map[id1], edge_label))
+ edges.sort()
+ graph[node_name] = edges
+
+except json.JSONDecodeError as e:
+ raise Exception("Oops. Illegal graph received (exception {}). Please report (unless you did something silly yourself)".format(e))
+
+# Now we add Floyd-Warshall
+
+def allPairsShortestPath(g):
+ """Return distance dictionary (a map from (u, v) to distance) computed
+ by Floyd-Warshall.
+ """
+ dist = {}
+ for u in g:
+ for v in g:
+ dist[u, v] = INF
+ dist[u, u] = 0
+
+ for v, edge_label in g[u]:
+ try:
+ d = int(edge_label)
+ except ValueError:
+ d = 0
+ dist[u, v] = d
+
+ for mid in g:
+ for u in g:
+ for v in g:
+ newlen = dist[u, mid] + dist[mid, v]
+ if newlen < dist[u, v]:
+ dist[u, v] = newlen
+
+ return dist
+
+distances = allPairsShortestPath(graph)
+
+{% for TEST in TESTCASES %}
+{{ TEST.testcode }}
+{{ TEST.extra }}
+{% if not loop.last %}
+print(SEPARATOR)
+{% endif %}
{% endfor %}]]>
@@ -130,11 +130,12 @@ print(SEPARATOR)
- 1
+ 100
diff --git a/samples/programtestingprototypeandexamples.xml b/samples/programtestingprototypeandexamples.xml
index be2e16234..33a167798 100644
--- a/samples/programtestingprototypeandexamples.xml
+++ b/samples/programtestingprototypeandexamples.xml
@@ -48,6 +48,7 @@
1
+ 10
@@ -222,6 +223,7 @@
1
+ 10
@@ -450,6 +452,7 @@
1
+ 10
@@ -1104,6 +1107,7 @@ print(SEPARATOR)
"table_dynamic_rows": true
}]]>
1
+ 10table
diff --git a/samples/randomisationexamples.xml b/samples/randomisationexamples.xml
index 4627c0aaa..cbac79bd2 100644
--- a/samples/randomisationexamples.xml
+++ b/samples/randomisationexamples.xml
@@ -24,7 +24,7 @@
100
@@ -76,6 +76,7 @@
]]>
1
+ 11
@@ -140,6 +141,7 @@
1
+ 11
@@ -238,12 +240,13 @@ print(new_age)
- 1
+ 11
@@ -305,7 +308,7 @@ print(new_age)
-
"sound": "{{ obj.sound }}"
}]]>1
+ 11
@@ -404,6 +408,7 @@ print(SEPARATOR)
"blah": 0
}]]>
1
+ 11ace
@@ -540,6 +545,7 @@ print(SEPARATOR)
"age": {{ 1 + random(30)}}
}]]>
1
+ 11
@@ -596,16 +602,16 @@ print(SEPARATOR)
"""A class representing a {{ animal }}"""
def __init__(self, attribute):
self.attribute = attribute
-
+
def __str__(self):
return "I am a {{ animal }}. My {{ attribute}} is {}".format(self.attribute)
-
+
def get_{{ attribute }}(self):
return self.attribute
-
+
def set_{{ attribute }}(self, value):
self.attribute = value
-
+
def speak(self, times=1):
print(times * "{{ sound }} ")]]>
1
@@ -623,7 +629,7 @@ print(SEPARATOR)
{'name': 'Horse', 'sound': 'Neigh'},
{'name': 'Dog', 'sound': 'Woof'},
{'name': 'Pig', 'sound': 'Oink'},
- {'name': 'Cat', 'sound': 'Miaow'}]) %}
+ {'name': 'Cat', 'sound': 'Miaow'}]) %}
"animal": "{{ obj.name }}",
"sound": "{{ obj.sound }}",
"attribute": "{{ random(["weight", "height", "age"]) }}",
@@ -631,6 +637,7 @@ print(SEPARATOR)
"value2": {{ 50 + random(10)}}
}]]>
1
+ 11
@@ -695,16 +702,16 @@ Now my {{ attribute }} is {{ value2}}
"""A class representing a {{ animal }}"""
def __init__(self, attribute):
self.attribute = attribute
-
+
def __str__(self):
return "I am a {{ animal }}. My {{ attribute}} is {}".format(self.attribute)
-
+
def get_{{ attribute }}(self):
return self.attribute
-
+
def set_{{ attribute }}(self, value):
self.attribute = value
-
+
def speak(self, times=1):
print(times * "{{ sound }} ")]]>
1
@@ -725,6 +732,7 @@ Now my {{ attribute }} is {{ value2}}
"value2": {{ 50 + random(10)}}
}]]>
1
+ 11
diff --git a/samples/trivialnodejsquestion.xml b/samples/trivialnodejsquestion.xml
index 32acd6ba7..4032dc3e2 100644
--- a/samples/trivialnodejsquestion.xml
+++ b/samples/trivialnodejsquestion.xml
@@ -50,6 +50,7 @@
1
+ 10
diff --git a/samples/uoctkinterprototype.xml b/samples/uoctkinterprototype.xml
index 08b3c246d..a6b27d149 100644
--- a/samples/uoctkinterprototype.xml
+++ b/samples/uoctkinterprototype.xml
@@ -553,6 +553,7 @@ from tkinter import *
0
+ 10
diff --git a/tests/fixtures/sqrexportwithsampleattachment.xml b/tests/fixtures/sqrexportwithsampleattachment.xml
index 4ac17db41..e47dd0790 100644
--- a/tests/fixtures/sqrexportwithsampleattachment.xml
+++ b/tests/fixtures/sqrexportwithsampleattachment.xml
@@ -42,6 +42,7 @@
twig1
+ 101
diff --git a/tests/helper.php b/tests/helper.php
index fe801a513..e61a660d4 100644
--- a/tests/helper.php
+++ b/tests/helper.php
@@ -108,6 +108,7 @@ public function get_coderunner_question_data_sqr() {
$qdata->options->answerboxlines = 5;
$qdata->options->displayfeedback = 1;
$qdata->options->giveupallowed = 0;
+ $qdata->options->extractcodefromjson = 1;
// Exclude precheck as it defaults to null.
$qdata->options->useace = 0;
$qdata->options->penaltyregime = '10, 20, ...';
@@ -146,6 +147,7 @@ public function get_coderunner_question_form_data_sqr() {
$form->penaltyregime = "10, 20, ...";
$form->templateparams = "";
$form->hoisttemplateparams = 1;
+ $form->extractcodefromjson = 1;
$form->templateparamslang = 'twig';
$form->templateparamsevalpertry = 0;
$form->templateparamsevald = null;
@@ -211,6 +213,7 @@ public function get_coderunner_question_form_data_printans() {
$form->penaltyregime = "10, 20, ...";
$form->templateparams = "";
$form->hoisttemplateparams = 1;
+ $form->extractcodefromjson = 1;
$form->templateparamslang = 'twig';
$form->templateparamsevalpertry = 0;
$form->templateparamsevald = null;
@@ -578,6 +581,7 @@ public function get_coderunner_question_form_data_demows() {
$form->penaltyregime = "10, 20, ...";
$form->templateparams = "";
$form->hoisttemplateparams = 1;
+ $form->extractcodefromjson = 1;
$form->templateparamslang = 'twig';
$form->templateparamsevalpertry = 0;
$form->templateparamsevald = null;
@@ -1288,6 +1292,7 @@ private function make_coderunner_question($type, $name, $questiontext,
$coderunner->templateparamsevald = null;
$coderunner->uiparameters = null;
$coderunner->hoisttemplateparams = 0;
+ $form->extractcodefromjson = 1;
$coderunner->twigall = 0;
$coderunner->prototypetype = 0;
$coderunner->name = $name;
diff --git a/tests/prototype_test.php b/tests/prototype_test.php
index 3da0d74c2..9c9db572d 100644
--- a/tests/prototype_test.php
+++ b/tests/prototype_test.php
@@ -148,6 +148,7 @@ public function test_export() {
0
+ 1twig0
diff --git a/tests/walkthrough_randomisation_test.php b/tests/walkthrough_randomisation_test.php
index d9a20f040..0e92091df 100644
--- a/tests/walkthrough_randomisation_test.php
+++ b/tests/walkthrough_randomisation_test.php
@@ -131,6 +131,7 @@ private function add_fields($q, $seed=false) {
$q->templateparamsevald = null;
$q->uiparameters = null;
$q->hoisttemplateparams = 1;
+ $q->extractcodefromjson = 1;
$q->twigall = 1;
$q->questiontext = 'Write a function {{ func }}';
$q->template = "{{ STUDENT_ANSWER }}\n{{ TEST.testcode }}\n{{ TEST.extra }}\n";
From d4e0b3cbef661d9efe512d456e5bdf960e9373c3 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Thu, 2 Feb 2023 17:08:39 +1300
Subject: [PATCH 056/188] More merge issues
---
amd/build/authorform.min.js | 10 ++++++++++
amd/build/authorform.min.js.map | 1 +
2 files changed, 11 insertions(+)
create mode 100644 amd/build/authorform.min.js
create mode 100644 amd/build/authorform.min.js.map
diff --git a/amd/build/authorform.min.js b/amd/build/authorform.min.js
new file mode 100644
index 000000000..1d7c6c937
--- /dev/null
+++ b/amd/build/authorform.min.js
@@ -0,0 +1,10 @@
+/**
+ * JavaScript for handling UI actions in the question authoring form.
+ *
+ * @module qtype_coderunner/authorform
+ * @copyright Richard Lobb, 2015, The University of Canterbury
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define("qtype_coderunner/authorform",["jquery","qtype_coderunner/userinterfacewrapper","core/str"],(function($,ui,str){var currentQtype="",JSON_TO_FORM_MAP={template:["#id_template","value",""],iscombinatortemplate:["#id_iscombinatortemplate","checked","",function(value){return"1"===value}],cputimelimitsecs:["#id_cputimelimitsecs","value",""],memlimitmb:["#id_memlimitmb","value",""],sandbox:["#id_sandbox","value","DEFAULT"],sandboxparams:["#id_sandboxparams","value",""],testsplitterre:["#id_testsplitterre","value","",function(splitter){return splitter.replace("\n","\\n")}],allowmultiplestdins:["#id_allowmultiplestdins","checked","",function(value){return"1"===value}],grader:["#id_grader","value","EqualityGrader"],resultcolumns:["#id_resultcolumns","value",""],language:["#id_language","value",""],acelang:["#id_acelang","value",""],uiplugin:["#id_uiplugin","value","ace"]};return{initEditForm:function(){var messagePara,typeCombo=$("#id_coderunnertype"),prototypeDisplay=$("#id_isprototype"),template=$("#id_template"),evaluatePerStudent=$("#id_templateparamsevalpertry"),globalextra=$("#id_globalextra"),prototypeextra=$("#id_prototypeextra"),useace=$("#id_useace"),language=$("#id_language"),acelang=$("#id_acelang"),customise=$("#id_customise"),isCombinator=$("#id_iscombinatortemplate"),testSplitterRe=$("#id_testsplitterre"),allowMultipleStdins=$("#id_allowmultiplestdins"),customisationFieldSet=$("#id_customisationheader"),advancedCustomisation=$("#id_advancedcustomisationheader"),isCustomised=customise.prop("checked"),prototypeType=$("#id_prototypetype"),preloadHdr=$("#id_answerpreloadhdr"),courseId=$('input[name="courseid"]').prop("value"),questiontypeHelpDiv=$("#qtype-help"),precheck=$("select#id_precheck"),testtypedivs=$("div.testtype"),brokenQuestion=$("#id_broken_question"),badQuestionLoad=$("#id_bad_question_load"),uiplugin=$("#id_uiplugin"),uiparameters=$("#id_uiparameters");function setUi(taId,uiname){var lang,uiWrapper,ta=$(document.getElementById(taId)),currentLang=ta.attr("data-lang"),paramsJson=ta.attr("data-params"),params={};ta.attr("data-prototypeextra",prototypeextra.val()),ta.attr("data-globalextra",globalextra.val()),ta.attr("data-test0",$("#id_testcode_0").val());try{params=JSON.parse(paramsJson)}catch(err){}"none"===(uiname=uiname.toLowerCase())&&(uiname=""),"id_templateparams"==taId||"id_uiparameters"==taId?lang="":(lang=language.prop("value"),"id_template"!==taId&&acelang.prop("value")&&(lang=function(acelang){var langs,i;if(acelang.indexOf(",")<0)return acelang;for(langs=acelang.split(","),i=0;i0?langs[0]:""}(acelang.prop("value")))),(uiWrapper=ta.data("current-ui-wrapper"))&&uiWrapper.uiname===uiname&¤tLang==lang||(ta.attr("data-lang",lang),uiWrapper?(params.lang=lang,uiWrapper.loadUi(uiname,params)):uiWrapper=new ui.InterfaceWrapper(uiname,taId))}function setUis(){var uiname=uiplugin.val(),answer=$("#id_answer"),enableUi=!0;if("html"===uiname&&""!==answer.attr("data-params"))try{!1===JSON.parse(answer.attr("data-params")).enable_in_editor&&(enableUi=!1)}catch(error){alert("Invalid UI parameters.")}enableUi&&(setUi("id_answer",uiname),setUi("id_answerpreload",uiname))}function setCustomisationVisibility(isVisible){var display=isVisible?"block":"none";customisationFieldSet.css("display",display),advancedCustomisation.css("display",display),isVisible&&useace.prop("checked")&&setUi("id_template","ace")}function copyFieldsFromQuestionType(newType,response){var formspecifier,attrval,isCombinatorEnabled;for(var key in function(stateOn){var uiWrapper,taIds=["id_template","id_uiparameters"];if(useace.prop("checked"))for(var i=0;i3&&(attrval=(0,formspecifier[3])(attrval)),$(formspecifier[0]).prop(formspecifier[1],attrval);customise.prop("checked",!1),str.get_string("coderunner_question_type","qtype_coderunner").then((function(s){var title,coderunner_descr,html,resultHtml;questiontypeHelpDiv.html((title=newType,coderunner_descr=s,html=response.questiontext,resultHtml='
")}))):loadCustomisationFields(),set_testtype_visibilities(),useace.prop("checked")&&(setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")),loadUiParametersDescription(),customise.on("change",(function(){customise.prop("checked")?setCustomisationVisibility(!0):str.get_string("confirm_proceed","qtype_coderunner").then((function(s){window.confirm(s)?setCustomisationVisibility(!1):customise.prop("checked",!0)}))})),acelang.on("change",check_ace_lang),language.on("change",(function(){useace.prop("checked")&&setUi("id_template","ace"),check_ace_lang()})),typeCombo.on("change",(function(){customise.prop("checked")?str.get_string("question_type_changed","qtype_coderunner").then((function(s){window.confirm(s)&&loadCustomisationFields()})):loadCustomisationFields()})),useace.on("change",(function(){useace.prop("checked")?(setUi("id_template","ace"),setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")):(setUi("id_template",""),setUi("id_templateparams",""),setUi("id_uiparameters",""))})),evaluatePerStudent.on("change",(function(){evaluatePerStudent.is(":checked")&&langStringAlert("templateparamsusingsandbox")})),uiplugin.on("change",(function(){setUis(),loadUiParametersDescription()})),precheck.on("change",set_testtype_visibilities),prototypeType.on("change",(function(){"0"==prototypeType.prop("value")?prototypeDisplay.attr("hidden","1"):prototypeDisplay.removeAttr("hidden")})),new MutationObserver((function(){setUis()})).observe(preloadHdr.get(0),{attributes:!0}),$("button.replaceexpectedwithgot").click((function(){var gotPre=$(this).prev('pre[id^="id_got_"]'),testCaseId=gotPre.attr("id").replace("id_got_","");$("#id_expected_"+testCaseId).val(gotPre.text()),$("#id_fail_expected_"+testCaseId).html(gotPre.text()),$(".failrow_"+testCaseId).addClass("fixed"),$(this).prop("disabled",!0)})),$(".btn-primary").click((function(){typeCombo.prop("disabled",!1)}))}}}));
+
+//# sourceMappingURL=authorform.min.js.map
\ No newline at end of file
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
new file mode 100644
index 000000000..adb230c7d
--- /dev/null
+++ b/amd/build/authorform.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * The sample answer and answer preload have the data-params attribute which contains\n * the UI params in a JSON from the current question merged with the prototype.\n * Both of them are identical and are changed simultaneously; only checking\n * answer as state is identical.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n *\n */\n function setUis() {\n let uiname = uiplugin.val();\n let answer = $('#id_answer');\n let enableUi = true;\n if (uiname === 'html' && answer.attr('data-params') !== '') {\n try {\n let answerparams = JSON.parse(answer.attr('data-params'));\n if (answerparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","messagePara","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","answer","enableUi","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","trim","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":";;;;;;;AAuBAA,qCAAO,CAAC,SAAU,wCAAyC,aAAa,SAASC,EAAGC,GAAIC,SAGhFC,aAAe,GASfC,iBAAmB,CACnBC,SAAqB,CAAC,eAAgB,QAAS,IAC/CC,qBAAqB,CAAC,2BAA4B,UAAW,GACrC,SAAUC,aACW,MAAVA,QAEnCC,iBAAqB,CAAC,uBAAwB,QAAS,IACvDC,WAAqB,CAAC,iBAAkB,QAAS,IACjDC,QAAqB,CAAC,cAAe,QAAS,WAC9CC,cAAqB,CAAC,oBAAqB,QAAS,IACpDC,eAAqB,CAAC,qBAAsB,QAAS,GAC7B,SAAUC,iBACCA,SAASC,QAAQ,KAAM,SAE1DC,oBAAqB,CAAC,0BAA2B,UAAW,GACpC,SAAUR,aACW,MAAVA,QAEnCS,OAAqB,CAAC,aAAc,QAAS,kBAC7CC,cAAqB,CAAC,oBAAqB,QAAS,IACpDC,SAAqB,CAAC,eAAgB,QAAS,IAC/CC,QAAqB,CAAC,cAAe,QAAS,IAC9CC,SAAqB,CAAC,eAAgB,QAAS,cAomB5C,CAACC,4BAnKIC,YAvbJC,UAAYvB,EAAE,sBACdwB,iBAAmBxB,EAAE,mBACrBK,SAAWL,EAAE,gBACbyB,mBAAqBzB,EAAE,gCACvB0B,YAAc1B,EAAE,mBAChB2B,eAAiB3B,EAAE,sBACnB4B,OAAS5B,EAAE,cACXkB,SAAWlB,EAAE,gBACbmB,QAAUnB,EAAE,eACZ6B,UAAY7B,EAAE,iBACd8B,aAAe9B,EAAE,4BACjB+B,eAAiB/B,EAAE,sBACnBgC,oBAAsBhC,EAAE,2BACxBiC,sBAAwBjC,EAAE,2BAC1BkC,sBAAwBlC,EAAE,mCAC1BmC,aAAeN,UAAUO,KAAK,WAC9BC,cAAgBrC,EAAE,qBAClBsC,WAAatC,EAAE,wBACfuC,SAAWvC,EAAE,0BAA0BoC,KAAK,SAC5CI,oBAAsBxC,EAAE,eACxByC,SAAWzC,EAAE,sBACb0C,aAAe1C,EAAE,gBACjB2C,eAAiB3C,EAAE,uBACnB4C,gBAAkB5C,EAAE,yBACpBoB,SAAWpB,EAAE,gBACb6C,aAAe7C,EAAE,6BAWZ8C,MAAMC,KAAMC,YAEbC,KAIAC,UALAC,GAAKnD,EAAEoD,SAASC,eAAeN,OAE/BO,YAAcH,GAAGI,KAAK,aACtBC,WAAaL,GAAGI,KAAK,eACrBE,OAAS,GAKbN,GAAGI,KAAK,sBAAuB5B,eAAe+B,OAC9CP,GAAGI,KAAK,mBAAoB7B,YAAYgC,OACxCP,GAAGI,KAAK,aAAcvD,EAAE,kBAAkB0D,WAEtCD,OAASE,KAAKC,MAAMJ,YACtB,MAAMK,MAEO,UADfb,OAASA,OAAOc,iBAEZd,OAAS,IAGD,qBAARD,MAAuC,mBAARA,KAC/BE,KAAO,IAEPA,KAAO/B,SAASkB,KAAK,SACR,gBAATW,MAA0B5B,QAAQiB,KAAK,WACvCa,cA2Mc9B,aAClB4C,MAAOC,KACP7C,QAAQ8C,QAAQ,KAAO,SAChB9C,YAEP4C,MAAQ5C,QAAQ+C,MAAM,KACjBF,EAAI,EAAGA,EAAID,MAAMI,OAAQH,OACtBD,MAAMC,GAAGI,SAAS,YACXL,MAAMC,GAAGK,OAAO,EAAGN,MAAMC,GAAGG,OAAS,UAG7CJ,MAAMI,OAAS,EAAIJ,MAAM,GAAK,GAtN1BO,CAAiBnD,QAAQiB,KAAK,aAI7Cc,UAAYC,GAAGoB,KAAK,wBAEHrB,UAAUF,SAAWA,QAAUM,aAAeL,OAI/DE,GAAGI,KAAK,YAAaN,MAEhBC,WAIDO,OAAOR,KAAOA,KACdC,UAAUsB,OAAOxB,OAAQS,SAJzBP,UAAY,IAAIjD,GAAGwE,iBAAiBzB,OAAQD,gBAoB3C2B,aACD1B,OAAS5B,SAASsC,MAClBiB,OAAS3E,EAAE,cACX4E,UAAW,KACA,SAAX5B,QAAoD,KAA/B2B,OAAOpB,KAAK,oBAGS,IADnBI,KAAKC,MAAMe,OAAOpB,KAAK,gBACzBsB,mBACbD,UAAW,GAEjB,MAAOE,OACLC,MAAM,0BAGVH,WACA9B,MAAM,YAAaE,QACnBF,MAAM,mBAAoBE,kBAQzBgC,2BAA2BC,eAC5BC,QAAUD,UAAY,QAAU,OACpChD,sBAAsBkD,IAAI,UAAWD,SACrChD,sBAAsBiD,IAAI,UAAWD,SACjCD,WAAarD,OAAOQ,KAAK,YACzBU,MAAM,cAAe,gBA+CpBsC,2BAA2BC,QAASC,cACrCC,cAAeC,QAZfC,wBAeC,IAAIC,gBAxCwBC,aAE7BzC,UADA0C,MAAQ,CAAC,cAAe,sBAExBhE,OAAOQ,KAAK,eACR,IAAI4B,EAAI,EAAGA,EAAI4B,MAAMzB,OAAQH,KAE7Bd,UADKlD,EAAEoD,SAASC,eAAeuC,MAAM5B,KACtBO,KAAK,wBACHoB,QACbzC,UAAU2C,UACH3C,YAAcyC,SACrBzC,UAAU4C,OA6BtBC,EAA4B,GACZ3F,iBACZmF,cAAgBnF,iBAAiBsF,KACjCF,QAAUF,SAASI,KAAOJ,SAASI,KAAOH,cAAc,GACpDA,cAAcpB,OAAS,IAEvBqB,SADAQ,EAAST,cAAc,IACNC,UAErBxF,EAAEuF,cAAc,IAAInD,KAAKmD,cAAc,GAAIC,SAG/C3D,UAAUO,KAAK,WAAW,GAC1BlC,IAAI+F,WAAW,2BAA4B,oBAAoBC,MAAK,SAAUC,OA2C7DC,MAAOC,iBAAkBC,KAEtCC,WA5CA/D,oBAAoB8D,MA0CPF,MA1CwBf,QA0CjBgB,iBA1C0BF,EA0CRG,KA1CWhB,SAASkB,aA4C1DD,WAAa,2CACjBA,YAAcF,iBACdE,YAAcH,MAAQ,SAAWE,UA3CjCtB,4BAA2B,GA9BvBS,oBAAsB3D,aAAaM,KAAK,WAE5CL,eAAeK,KAAK,YAAaqD,qBACjCzD,oBAAoBI,KAAK,YAAaqD,8BAiFjCgB,gBAAgBf,IAAKgB,OACtBC,OAAOC,eAAe,iBAAmBD,OAAOE,cAGpD3G,IAAI+F,WAAWP,IAAK,oBAAoBQ,MAAK,SAASC,OAC9CW,QAAUX,EAAErF,QAAQ,MAAO,KAC3B4F,QACAI,SAAW,KAAOJ,OAEtB3B,MAAM+B,qBAgCLC,8BACD1B,QAAU9D,UAAUyF,SAAS,mBAAmBC,OAEpC,KAAZ5B,SAA8B,cAAZA,UAElB9D,UAAUyF,SAAS,sBAAsB5E,KAAK,WAAY,YAG1DpC,EAAEkH,QAAQC,EAAEC,IAAIC,QAAU,qCACtB,CACIC,MAAOjC,QACPkC,SAAUhF,SACViF,QAASL,EAAEC,IAAII,UAEnB,SAAUC,YAENzH,EAAE,oCAAoC0H,QAClCD,QAAQE,QACRvC,2BAA2BC,QAASoC,SACpC/C,SAEAvE,aAAekF,QACfrF,EAAE,kCAAkC0H,YAEnC,KACKE,qBAzGLC,aAAc/C,WACzB8C,YAAcjE,KAAKC,MAAMkB,cAC/B5E,IAAI+F,WAAW,kBAAmB,oBAAoBC,MAAK,SAASC,GAChEjG,IAAI+F,WAAW2B,YAAY7C,MAAO,mBAAoB8C,cAAc3B,MAAK,SAAShG,KAC9EuG,gBAAgB,yBAA0BvG,SACtC4H,aAAe3B,EAAI,KACvB2B,cAAgB5H,IAAM,KACtB4H,cAAgB,aAAevF,SAAW,YAAcsF,aACxDxH,SAAS+B,KAAK,QAAS0F,oBAGxBF,YA8F6BG,CAAY1C,QAASoC,QAAQ3C,OAG7C3E,eAAiBkF,SAAiC,uBAAtBuC,YAAY9C,kBA4IrCkD,YAAaJ,YAAavC,SACjDnF,IAAI+F,WAAW,qBAAsB,mBACjC,CAAEgC,QAAUD,YAAaE,OAAS7C,QAAS8C,aAAeP,YAAYQ,SAC/DlC,MAAK,SAAShG,KACrBF,EAAE,oCAAoCqI,OAAOrI,EAAE,MAAQE,IAAM,YA/I7CoI,CAAkBnI,aAAcyH,YAAavC,SAC7CrF,EAAE,sBAAsB0D,IAAIvD,mBAI1CoI,MAAK,WAIH9B,gBAAgB,2BAChBpG,SAAS+B,KAAK,QAAS,wCACvBlC,IAAI+F,WAAW,aAAc,oBAAoBC,MAAK,SAASC,GAC3D9F,SAAS+B,KAAK,QAAS+D,mBA4B9BqC,kCACDC,MAAQrH,SAAS4F,SAAS,mBAAmBC,OACjDjH,EAAEkH,QAAQC,EAAEC,IAAIC,QAAU,qCACtB,CACIjG,SAAUqH,MACVlB,SAAUhF,SACViF,QAASL,EAAEC,IAAII,UAEnB,SAAUkB,YAIFC,MAHAC,oBAAsB/F,aAAaa,MACnCmF,oBAAsB7I,EAAE,wBACxB8I,eAAiB9I,EAAE,iDAAmD0I,OAAOK,YAAc,aAE/FF,oBAAoBnB,QACpBmB,oBAAoBR,OAAOK,OAAOM,QACC,GAA/BN,OAAOO,cAAc9E,QAA8C,KAA/ByE,oBAAoBM,QACxDrG,aAAaa,IAAI,IACjB1D,EAAE,+BAA+BmJ,SAEE,GAA/BT,OAAOO,cAAc9E,SACrB0E,oBAAoBR,OAAOS,gBAC3BH,MAAQ3I,WArCSoJ,iBAEKC,MAAOrF,EADzCsC,KAAO,8DACPgD,KAAOF,YAAYG,kBACvBjD,MAAQ,WAAagD,KAAK,GAAK,YAAcA,KAAK,GAAK,YAAcA,KAAK,GAAK,eAC1EtF,EAAI,EAAGA,EAAIoF,YAAYH,cAAc9E,OAAQH,IAE9CsC,MAAQ,YADR+C,MAAQD,YAAYH,cAAcjF,IACP,GAAK,YAAcqF,MAAM,GAAK,YAAcA,MAAM,GAAK,sBAEtF/C,KAAQ,mBA6BkBkD,CAA4Bd,SACtCG,oBAAoBR,OAAOM,OAC3BA,MAAMQ,OACNL,eAAeW,OAAM,WACbX,eAAexC,QAAUoC,OAAOK,aAChCJ,MAAMe,OACNZ,eAAexC,KAAKoC,OAAOiB,eAE3BhB,MAAMQ,OACNL,eAAexC,KAAKoC,OAAOK,kBAIvC/I,EAAE,+BAA+B0J,OAC7B9H,OAAOQ,KAAK,YACZU,MAAM,kBAAmB,WAIvCyF,MAAK,WAEH9B,gBAAgB,sCAQfmD,4BACkB,MAAnBnH,SAASiB,MACThB,aAAagH,OAEbhH,aAAayG,gBAQZU,iBACkB,QAAnBzI,SAASsC,OACTgB,SAiD2B,GAA/BrC,cAAcD,KAAK,WAEnBZ,iBAAiBsI,WAAW,UACO,GAA/BzH,cAAcD,KAAK,WAEnBlC,IAAI+F,WAAW,sBAAuB,oBAAoBC,MAAK,SAASC,GACpEpB,MAAMoB,MAEV9D,cAAcD,KAAK,YAAY,GAC/BP,UAAUO,KAAK,YAAY,KArC3Bd,YAAc,KACY,KAFFqB,eAAeP,KAAK,WAG5Cd,YAActB,EAAE,MAAQ2C,eAAeP,KAAK,SAAW,QACvDpC,EAAE,kCAAkCqI,OAAO/G,cAuCnDsB,gBAAgBR,KAAK,UAErBjC,aAAeoB,UAAUyF,SAAS,mBAAmBC,OAErDjC,2BAA2B7C,cACtBA,cAIDuC,SACAxE,IAAI+F,WAAW,mBAAoB,oBAAoBC,MAAK,SAASC,GACjE3D,oBAAoB8D,KAAK,MAAQH,EAAI,YAJzCY,0BAQJ6C,4BAEIhI,OAAOQ,KAAK,aACZU,MAAM,oBAAqB,OAC3BA,MAAM,kBAAmB,QAG7B0F,8BAIA3G,UAAUkI,GAAG,UAAU,WACAlI,UAAUO,KAAK,WAG9B4C,4BAA2B,GAE3B9E,IAAI+F,WAAW,kBAAmB,oBAAoBC,MAAK,SAASC,GAC5DQ,OAAOqD,QAAQ7D,GACfnB,4BAA2B,GAE3BnD,UAAUO,KAAK,WAAW,SAM1CjB,QAAQ4I,GAAG,SAAUF,gBACrB3I,SAAS6I,GAAG,UAAU,WAjGdnI,OAAOQ,KAAK,YACZU,MAAM,cAAe,OAkGzB+G,oBAGJtI,UAAUwI,GAAG,UAAU,WACflI,UAAUO,KAAK,WAEflC,IAAI+F,WAAW,wBAAyB,oBAAoBC,MAAK,SAAUC,GACnEQ,OAAOqD,QAAQ7D,IACfY,6BAIRA,6BAIRnF,OAAOmI,GAAG,UAAU,WACEnI,OAAOQ,KAAK,YAE1BU,MAAM,cAAe,OACrBA,MAAM,oBAAqB,OAC3BA,MAAM,kBAAmB,SAEzBA,MAAM,cAAe,IACrBA,MAAM,oBAAqB,IAC3BA,MAAM,kBAAmB,QAIjCrB,mBAAmBsI,GAAG,UAAU,WACxBtI,mBAAmBwI,GAAG,aACtBxD,gBAAgB,iCAIxBrF,SAAS2I,GAAG,UAAU,WAClBrF,SACA8D,iCAGJ/F,SAASsH,GAAG,SAAUH,2BAGtBvH,cAAc0H,GAAG,UAAU,WACY,KAA/B1H,cAAcD,KAAK,SACnBZ,iBAAiB+B,KAAK,SAAU,KAEhC/B,iBAAiBsI,WAAW,aAOrB,IAAII,kBAAkB,WACjCxF,YAEKyF,QAAQ7H,WAAW8H,IAAI,GAAI,aAAe,IAInDpK,EAAE,iCAAiCyJ,OAAM,eACjCY,OAASrK,EAAEsK,MAAMC,KAAK,sBACtBC,WAAaH,OAAO9G,KAAK,MAAMzC,QAAQ,UAAW,IACtDd,EAAE,gBAAkBwK,YAAY9G,IAAI2G,OAAOpD,QAC3CjH,EAAE,qBAAuBwK,YAAYlE,KAAK+D,OAAOpD,QACjDjH,EAAE,YAAcwK,YAAYC,SAAS,SACrCzK,EAAEsK,MAAMlI,KAAK,YAAY,MAI7BpC,EAAE,gBAAgByJ,OAAM,WACpBlI,UAAUa,KAAK,YAAY"}
\ No newline at end of file
From b237d1399445e4976eb98e1e2f5303d3289edf32 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Thu, 2 Feb 2023 18:59:49 +1300
Subject: [PATCH 057/188] Fix failing phpunit tests.
---
tests/helper.php | 2 +-
tests/ui_parameters_test.php | 14 ++++++++------
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/tests/helper.php b/tests/helper.php
index e61a660d4..3376bf982 100644
--- a/tests/helper.php
+++ b/tests/helper.php
@@ -1292,7 +1292,7 @@ private function make_coderunner_question($type, $name, $questiontext,
$coderunner->templateparamsevald = null;
$coderunner->uiparameters = null;
$coderunner->hoisttemplateparams = 0;
- $form->extractcodefromjson = 1;
+ $coderunner->extractcodefromjson = 1;
$coderunner->twigall = 0;
$coderunner->prototypetype = 0;
$coderunner->name = $name;
diff --git a/tests/ui_parameters_test.php b/tests/ui_parameters_test.php
index f5b9b70bf..0400129ba 100644
--- a/tests/ui_parameters_test.php
+++ b/tests/ui_parameters_test.php
@@ -52,7 +52,11 @@ public function test_params() {
$this->assertContains('noderadius', array_keys($paramsarray));
$this->assertEquals(30, $paramsarray['noderadius']);
$aceparams = new \qtype_coderunner_ui_parameters('ace');
- $this->assertEmpty($aceparams->all_names());
+ $this->assertEquals(true, $aceparams->value('auto_switch_light_dark'));
+ $this->assertEquals("14px", $aceparams->value('font_size'));
+ $this->assertEquals(true, $aceparams->value('import_from_scratchpad'));
+ $this->assertEquals(false, $aceparams->value('live_autocompletion'));
+ $this->assertEquals("textmate", $aceparams->value('theme'));
}
// Test that we can get a list of all plugins and their parameter lists.
@@ -61,11 +65,9 @@ public function test_plugin_list() {
$names = $plugins->all_names();
$this->assertContains('ace', $names);
$this->assertContains('graph', $names);
- $aceparams = $plugins->parameters('ace');
- $this->assertEquals(0, $aceparams->length());
- $graphparams = $plugins->parameters('graph');
- $this->assertEquals(26, $graphparams->value('noderadius'));
- $this->assertContains('ace', $plugins->all_with_no_params());
+ $this->assertContains('gapfiller', $names);
+ $this->assertContains('html', $names);
+ $this->assertContains('scratchpad', $names);
}
// Test the dropdown list for the plugins.
From d4ce0348c3b5bd9b4fe97d91819599acaa4a4fc3 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Sat, 4 Feb 2023 16:49:13 +1300
Subject: [PATCH 058/188] Revamp handling of sample-attachments so that Preview
can load the sample answer attachments correctly.
---
classes/bulk_tester.php | 2 +-
classes/coderunner_files.php | 74 -----------------------------
question.php | 92 ++++++++++++++++++++++++++++++++++--
3 files changed, 90 insertions(+), 78 deletions(-)
delete mode 100644 classes/coderunner_files.php
diff --git a/classes/bulk_tester.php b/classes/bulk_tester.php
index 3c9116832..941c144d4 100644
--- a/classes/bulk_tester.php
+++ b/classes/bulk_tester.php
@@ -289,7 +289,7 @@ private function test_question($question) {
core_php_time_limit::raise(60); // Prevent PHP timeouts.
gc_collect_cycles(); // Because PHP's default memory management is rubbish.
$question->start_attempt(null);
- $response = $question->get_correct_answer();
+ $response = $question->get_correct_response();
// Check if it's a multilanguage question; if so need to determine
// what language (either specified by answer_language template param, or
// the AceLang default or the first).
diff --git a/classes/coderunner_files.php b/classes/coderunner_files.php
deleted file mode 100644
index 339365ecc..000000000
--- a/classes/coderunner_files.php
+++ /dev/null
@@ -1,74 +0,0 @@
-.
-
-/**
- * CodeRunner files class which implements question_response_files to
- * allow access to files.
- *
- * @package qtype_coderunner
- * @copyright Richard Lobb, 2021, The University of Canterbury
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace qtype_coderunner;
-
-/**
- * This class implements {@link question_response_files} interface which allows
- * bulk testing and previews to use sample answer files by retrieving files associated
- * with a particular question. Can also be constructed with a different filearea
- * for future use.
- */
-class coderunner_files implements \question_response_files {
- /** @var type The contextid of the question. */
- protected $contextid;
- /** @var string The string name of the filearea */
- protected $filearea;
- /** @var int The question id number. */
- protected $questionid;
-
- /**
- * Constructor.
- *
- * @param type $contextid The contextid of the question.
- * @param string $filearea The name of the filearea.
- * @param int $questionid The id of the question.
- */
- public function __construct($contextid, $filearea, $questionid) {
- $this->contextid = $contextid;
- $this->filearea = $filearea;
- $this->questionid = $questionid;
- }
-
- /**
- * Returns all the CodeRunner files in the specified filearea, questionid and
- * contextid.
- *
- * @return array of files An array of all the files requested.
- */
- public function get_files() {
- $fs = get_file_storage();
- return $fs->get_area_files($this->contextid, 'qtype_coderunner', $this->filearea, $this->questionid);
- }
-
- /**
- * Returns a string for the object to satisfy other Moodle checks.
- *
- * @return string A string for the object.
- */
- public function __toString() {
- return "coderunnerquestionresponsefiles";
- }
-}
\ No newline at end of file
diff --git a/question.php b/question.php
index 2c003c52e..9b7bc7f2f 100644
--- a/question.php
+++ b/question.php
@@ -342,7 +342,6 @@ public function get_expected_data() {
}
-
public function summarise_response(array $response) {
if (isset($response['answer'])) {
return $response['answer'];
@@ -467,7 +466,11 @@ public function is_same_response(array $prevresponse, array $newresponse) {
public function get_correct_response() {
- return $this->get_correct_answer();
+ $response = $this->get_correct_answer();
+ if ($this->attachments) {
+ $response['attachments'] = $this->make_attachments_saver();
+ }
+ return $response;
}
@@ -480,7 +483,6 @@ public function get_correct_answer() {
// Get any sample question files first.
$context = qtype_coderunner::question_context($this);
$contextid = $context->id;
- $answer['attachments'] = new coderunner_files($contextid, 'samplefile', $this->id);
// For multilanguage questions we also need to specify the language.
// Use the answer_language template parameter value if given, otherwise
// run with the default.
@@ -497,6 +499,90 @@ public function get_correct_answer() {
}
+ /**
+ * Creates an empty draft area for attachments.
+ * @return int The draft area's itemid.
+ */
+ protected function make_attachment_draft_area() {
+ $draftid = 0;
+ $contextid = 0;
+
+ $component = 'question';
+ $filearea = 'response_attachments';
+
+ // Create an empty file area.
+ file_prepare_draft_area($draftid, $contextid, $component, $filearea, null);
+ return $draftid;
+ }
+
+
+ /**
+ * Adds the given file to the given draft area.
+ * @param int $draftid The itemid for the draft area in which the file should be created.
+ * @param string $file The file to be added.
+ */
+ protected function make_attachment($draftid, $file) {
+ global $USER;
+
+ $fs = get_file_storage();
+ $usercontext = context_user::instance($USER->id);
+
+ // Create the file in the provided draft area.
+ $fileinfo = array(
+ 'contextid' => $usercontext->id,
+ 'component' => 'user',
+ 'filearea' => 'draft',
+ 'itemid' => $draftid,
+ 'filepath' => '/',
+ 'filename' => $file->get_filename(),
+ );
+ $fs->create_file_from_string($fileinfo, $file->get_content());
+ }
+
+
+ /**
+ * Generates a draft file area that contains the sample answer attachments.
+ * @return int The itemid of the generated draft file area or null if there are
+ * no sample answer attachments.
+ */
+ public function make_attachments() {
+ $fs = get_file_storage();
+ $files = $fs->get_area_files($this->contextid, 'qtype_coderunner', 'samplefile', $this->id);
+ $usefulfiles = [];
+ foreach ($files as $file) { // Filter out useless '.' files.
+ if ($file->get_filename() !== '.') {
+ $usefulfiles[] = $file;
+ }
+ }
+
+ if (count($usefulfiles) > 0) {
+ $draftid = $this->make_attachment_draft_area();
+ foreach ($usefulfiles as $file) {
+ $this->make_attachment($draftid, $file);
+ }
+ return $draftid;
+ } else {
+ return null;
+ }
+ }
+
+
+ /**
+ * Generates a question_file_saver that contains all the sample answer attachments.
+ *
+ * @return question_file_saver a question_file_saver that contains the
+ * sample answer attachments.
+ */
+ public function make_attachments_saver() {
+ $attachments = $this->make_attachments();
+ if ($attachments) {
+ return new question_file_saver($attachments, 'question', 'response_attachments');
+ } else {
+ return null;
+ }
+ }
+
+
public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
if ($component == 'question' && $filearea == 'response_attachments') {
// Response attachments visible if the question has them.
From cb66afc44fe4e96d0b4c47da0bba9558d442e669 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Tue, 7 Feb 2023 15:58:00 +1300
Subject: [PATCH 059/188] Change default to false for auto-switching theme
according to media setting ("prefers light/dark").
---
amd/build/ui_ace.min.js | 2 +-
amd/build/ui_ace.min.js.map | 2 +-
amd/src/ui_ace.js | 3 +--
amd/src/ui_ace.json | 2 +-
4 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/amd/build/ui_ace.min.js b/amd/build/ui_ace.min.js
index 52801a983..b2ca7b514 100644
--- a/amd/build/ui_ace.min.js
+++ b/amd/build/ui_ace.min.js
@@ -14,6 +14,6 @@
* @copyright Richard Lobb, 2015, 2017, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_ace",["jquery"],(function($){function AceWrapper(textareaId,w,h,params){var session,code,textarea=$(document.getElementById(textareaId)),wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=textarea[0]===document.activeElement,lang=params.lang,t=this;try{window.ace.require("ace/ext/language_tools"),this.modelist=window.ace.require("ace/ext/modelist"),this.textareaId=textareaId,this.textarea=textarea,this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),textarea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({enableBasicAutocompletion:!0,enableLiveAutocompletion:params.live_autocompletion,fontSize:params.font_size?params.font_size:"14px",newLineMode:"unix"}),this.editor.$blockScrolling=1/0,session=this.editor.getSession(),code=this.textarea.val(),(void 0===params.import_from_scratchpad||params.import_from_scratchpad)&&(code=this.extract_from_json_maybe(code)),session.setValue(code);var userTheme=window.localStorage.getItem("qtype_coderunner.ace.theme"),consider_prefers=(void 0===params.auto_switch_light_dark||params.auto_switch_light_dark)&&window.matchMedia;null!==userTheme?this.editor.setTheme(userTheme):consider_prefers&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):consider_prefers&&window.matchMedia("(prefers-color-scheme: light)").matches?this.editor.setTheme("ace/theme/textmate"):params.theme?this.editor.setTheme("ace/theme/"+params.theme):this.editor.setTheme("ace/theme/textmate"),this.fixSlowLoad(),this.setLanguage(lang),this.setEventHandlers(textarea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.fail=!1}catch(err){this.fail=!0}}return AceWrapper.prototype.extract_from_json_maybe=function(code){try{code=JSON.parse(code).answer_code[0]}catch(err){}return code},AceWrapper.prototype.failed=function(){return this.fail},AceWrapper.prototype.failMessage=function(){return"ace_ui_notready"},AceWrapper.prototype.sync=function(){var thisThemeNow=this.editor.getOption("theme"),thisEditorKey="qtype_coderunner.".concat(this.textareaId,".theme"),lastTheme=window.sessionStorage.getItem(thisEditorKey),globalTheme=window.localStorage.getItem("qtype_coderunner.ace.theme");null!==lastTheme&&lastTheme!=thisThemeNow?window.localStorage.setItem("qtype_coderunner.ace.theme",thisThemeNow):globalTheme&&globalTheme!==thisThemeNow&&(this.editor.setOption("theme",globalTheme),function(name){throw new TypeError('"'+name+'" is read-only')}("thisThemeNow")),window.sessionStorage.setItem(thisEditorKey,thisThemeNow)},AceWrapper.prototype.syncIntervalSecs=function(){return 2},AceWrapper.prototype.setLanguage=function(language){var session=this.editor.getSession(),mode=this.findMode(language);mode&&session.setMode(mode.mode)},AceWrapper.prototype.getElement=function(){return this.editNode},AceWrapper.prototype.captureTab=function(){this.capturingTab=!0,this.editor.commands.bindKeys({Tab:"indent","Shift-Tab":"outdent"})},AceWrapper.prototype.releaseTab=function(){this.capturingTab=!1,this.editor.commands.bindKeys({Tab:null,"Shift-Tab":null})},AceWrapper.prototype.fixSlowLoad=function(){var observer=new IntersectionObserver((function(){$(document).trigger("mousemove")})),editNode=this.editNode.get(0);observer.observe(editNode)},AceWrapper.prototype.setEventHandlers=function(){var t=this;this.editor.getSession().on("change",(function(){t.textarea.val(t.editor.getSession().getValue()),t.contents_changed=!0})),this.editor.on("blur",(function(){t.contents_changed&&t.textarea.trigger("change")})),this.editor.on("mousedown",(function(){t.clickInProgress=!0})),this.editor.on("focus",(function(){t.clickInProgress?t.captureTab():t.releaseTab()})),this.editor.on("click",(function(){t.clickInProgress=!1})),this.editor.container.addEventListener("keydown",(function(e){void 0!==e.which&&0===e.which||(77===e.keyCode&&e.ctrlKey&&!e.altKey?(t.capturingTab?t.releaseTab():t.captureTab(),e.preventDefault()):27===e.keyCode?t.releaseTab():e.shiftKey||e.ctrlKey||e.altKey||9==e.keyCode||t.captureTab())}),!0)},AceWrapper.prototype.destroy=function(){var focused;this.fail||(focused=this.editor.isFocused(),this.textarea.val(this.editor.getSession().getValue()),this.editor.destroy(),$(this.editNode).remove(),focused&&(this.textarea.focus(),this.textarea[0].selectionStart=this.textarea[0].value.length))},AceWrapper.prototype.hasFocus=function(){return this.editor.isFocused()},AceWrapper.prototype.findMode=function(language){var candidate,filename,result,candidates,nameMap={octave:"matlab",nodejs:"javascript","c#":"cs"};if("string"==typeof language){language.toLowerCase()in nameMap&&(language=nameMap[language.toLowerCase()]),candidates=[language,language.replace(/\d+$/,"")];for(var i=0;i
"),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),textarea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({enableBasicAutocompletion:!0,enableLiveAutocompletion:params.live_autocompletion,fontSize:params.font_size?params.font_size:"14px",newLineMode:"unix"}),this.editor.$blockScrolling=1/0,session=this.editor.getSession(),code=this.textarea.val(),(void 0===params.import_from_scratchpad||params.import_from_scratchpad)&&(code=this.extract_from_json_maybe(code)),session.setValue(code);var userTheme=window.localStorage.getItem("qtype_coderunner.ace.theme"),consider_prefers=params.auto_switch_light_dark&&window.matchMedia;null!==userTheme?this.editor.setTheme(userTheme):consider_prefers&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):consider_prefers&&window.matchMedia("(prefers-color-scheme: light)").matches?this.editor.setTheme("ace/theme/textmate"):params.theme?this.editor.setTheme("ace/theme/"+params.theme):this.editor.setTheme("ace/theme/textmate"),this.fixSlowLoad(),this.setLanguage(lang),this.setEventHandlers(textarea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.fail=!1}catch(err){this.fail=!0}}return AceWrapper.prototype.extract_from_json_maybe=function(code){try{code=JSON.parse(code).answer_code[0]}catch(err){}return code},AceWrapper.prototype.failed=function(){return this.fail},AceWrapper.prototype.failMessage=function(){return"ace_ui_notready"},AceWrapper.prototype.sync=function(){var thisThemeNow=this.editor.getOption("theme"),thisEditorKey="qtype_coderunner.".concat(this.textareaId,".theme"),lastTheme=window.sessionStorage.getItem(thisEditorKey),globalTheme=window.localStorage.getItem("qtype_coderunner.ace.theme");null!==lastTheme&&lastTheme!=thisThemeNow?window.localStorage.setItem("qtype_coderunner.ace.theme",thisThemeNow):globalTheme&&globalTheme!==thisThemeNow&&(this.editor.setOption("theme",globalTheme),function(name){throw new TypeError('"'+name+'" is read-only')}("thisThemeNow")),window.sessionStorage.setItem(thisEditorKey,thisThemeNow)},AceWrapper.prototype.syncIntervalSecs=function(){return 2},AceWrapper.prototype.setLanguage=function(language){var session=this.editor.getSession(),mode=this.findMode(language);mode&&session.setMode(mode.mode)},AceWrapper.prototype.getElement=function(){return this.editNode},AceWrapper.prototype.captureTab=function(){this.capturingTab=!0,this.editor.commands.bindKeys({Tab:"indent","Shift-Tab":"outdent"})},AceWrapper.prototype.releaseTab=function(){this.capturingTab=!1,this.editor.commands.bindKeys({Tab:null,"Shift-Tab":null})},AceWrapper.prototype.fixSlowLoad=function(){var observer=new IntersectionObserver((function(){$(document).trigger("mousemove")})),editNode=this.editNode.get(0);observer.observe(editNode)},AceWrapper.prototype.setEventHandlers=function(){var t=this;this.editor.getSession().on("change",(function(){t.textarea.val(t.editor.getSession().getValue()),t.contents_changed=!0})),this.editor.on("blur",(function(){t.contents_changed&&t.textarea.trigger("change")})),this.editor.on("mousedown",(function(){t.clickInProgress=!0})),this.editor.on("focus",(function(){t.clickInProgress?t.captureTab():t.releaseTab()})),this.editor.on("click",(function(){t.clickInProgress=!1})),this.editor.container.addEventListener("keydown",(function(e){void 0!==e.which&&0===e.which||(77===e.keyCode&&e.ctrlKey&&!e.altKey?(t.capturingTab?t.releaseTab():t.captureTab(),e.preventDefault()):27===e.keyCode?t.releaseTab():e.shiftKey||e.ctrlKey||e.altKey||9==e.keyCode||t.captureTab())}),!0)},AceWrapper.prototype.destroy=function(){var focused;this.fail||(focused=this.editor.isFocused(),this.textarea.val(this.editor.getSession().getValue()),this.editor.destroy(),$(this.editNode).remove(),focused&&(this.textarea.focus(),this.textarea[0].selectionStart=this.textarea[0].value.length))},AceWrapper.prototype.hasFocus=function(){return this.editor.isFocused()},AceWrapper.prototype.findMode=function(language){var candidate,filename,result,candidates,nameMap={octave:"matlab",nodejs:"javascript","c#":"cs"};if("string"==typeof language){language.toLowerCase()in nameMap&&(language=nameMap[language.toLowerCase()]),candidates=[language,language.replace(/\d+$/,"")];for(var i=0;i.\n\n/**\n * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n code,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n this.textareaId = textareaId;\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n enableLiveAutocompletion: params.live_autocompletion,\n fontSize: params.font_size ? params.font_size : \"14px\",\n newLineMode: \"unix\",\n });\n\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n code = this.textarea.val();\n if (params.import_from_scratchpad === undefined || params.import_from_scratchpad) {\n code = this.extract_from_json_maybe(code);\n }\n session.setValue(code);\n\n // If there's a user-defined theme in local storage, use that.\n // Otherwise use the 'prefers-color-scheme' option if given or\n // the question/system defaults if not.\n const userTheme = window.localStorage.getItem('qtype_coderunner.ace.theme');\n const consider_prefers = (params.auto_switch_light_dark === undefined || params.auto_switch_light_dark)\n && window.matchMedia;\n if (userTheme !== null) {\n this.editor.setTheme(userTheme);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: light)').matches) {\n this.editor.setTheme(ACE_LIGHT_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.fixSlowLoad();\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n AceWrapper.prototype.extract_from_json_maybe = function(code) {\n // If the given code looks like JSON from the Scratchpad UI,\n // extract and return the answer_code attribute.\n try {\n const jsonObj = JSON.parse(code);\n code = jsonObj.answer_code[0];\n } catch(err) {}\n\n return code;\n };\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // The data is always sync'd to the text area. But here we use sync to\n // poll the value of the current theme and record in browser local\n // storage if the value for this particular Ace instance has changed\n // (as recorded in session storage). The theme to use for all Ace windows\n // from now on in this browser is recorded in browser local storage.\n const thisThemeNow = this.editor.getOption('theme');\n const thisEditorKey = `qtype_coderunner.${this.textareaId}.theme`;\n const lastTheme = window.sessionStorage.getItem(thisEditorKey);\n const globalKey = 'qtype_coderunner.ace.theme';\n const globalTheme = window.localStorage.getItem(globalKey);\n\n if (lastTheme !== null && lastTheme != thisThemeNow) {\n // The theme in this Ace window has been changed by the user.\n window.localStorage.setItem(globalKey, thisThemeNow);\n } else if (globalTheme && globalTheme !== thisThemeNow) {\n // A theme has been defined by another Ace window since we loaded.\n // Switch to that theme.\n this.editor.setOption('theme', globalTheme);\n thisThemeNow = globalTheme;\n }\n window.sessionStorage.setItem(thisEditorKey, thisThemeNow);\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 2;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n // Sometimes Ace editors do not load until the mouse is moved. To fix this,\n // 'move' the mouse using JQuery when the editor div enters the viewport.\n AceWrapper.prototype.fixSlowLoad = function () {\n const observer = new IntersectionObserver( () => {\n $(document).trigger('mousemove');\n });\n const editNode = this.editNode.get(0); // Non-JQuerry node.\n observer.observe(editNode);\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","session","code","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","enableLiveAutocompletion","live_autocompletion","fontSize","font_size","newLineMode","$blockScrolling","Infinity","getSession","val","undefined","import_from_scratchpad","extract_from_json_maybe","setValue","userTheme","localStorage","getItem","consider_prefers","auto_switch_light_dark","matchMedia","setTheme","matches","theme","fixSlowLoad","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","JSON","parse","answer_code","failed","failMessage","sync","thisThemeNow","getOption","thisEditorKey","lastTheme","sessionStorage","globalTheme","setItem","setOption","syncIntervalSecs","language","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","observer","IntersectionObserver","trigger","observe","getValue","container","addEventListener","e","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_ace.min.js","sources":["../src/ui_ace.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 * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n code,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n this.textareaId = textareaId;\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n enableLiveAutocompletion: params.live_autocompletion,\n fontSize: params.font_size ? params.font_size : \"14px\",\n newLineMode: \"unix\",\n });\n\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n code = this.textarea.val();\n if (params.import_from_scratchpad === undefined || params.import_from_scratchpad) {\n code = this.extract_from_json_maybe(code);\n }\n session.setValue(code);\n\n // If there's a user-defined theme in local storage, use that.\n // Otherwise use the 'prefers-color-scheme' option if given or\n // the question/system defaults if not.\n const userTheme = window.localStorage.getItem('qtype_coderunner.ace.theme');\n const consider_prefers = params.auto_switch_light_dark && window.matchMedia;\n if (userTheme !== null) {\n this.editor.setTheme(userTheme);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: light)').matches) {\n this.editor.setTheme(ACE_LIGHT_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.fixSlowLoad();\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n AceWrapper.prototype.extract_from_json_maybe = function(code) {\n // If the given code looks like JSON from the Scratchpad UI,\n // extract and return the answer_code attribute.\n try {\n const jsonObj = JSON.parse(code);\n code = jsonObj.answer_code[0];\n } catch(err) {}\n\n return code;\n };\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // The data is always sync'd to the text area. But here we use sync to\n // poll the value of the current theme and record in browser local\n // storage if the value for this particular Ace instance has changed\n // (as recorded in session storage). The theme to use for all Ace windows\n // from now on in this browser is recorded in browser local storage.\n const thisThemeNow = this.editor.getOption('theme');\n const thisEditorKey = `qtype_coderunner.${this.textareaId}.theme`;\n const lastTheme = window.sessionStorage.getItem(thisEditorKey);\n const globalKey = 'qtype_coderunner.ace.theme';\n const globalTheme = window.localStorage.getItem(globalKey);\n\n if (lastTheme !== null && lastTheme != thisThemeNow) {\n // The theme in this Ace window has been changed by the user.\n window.localStorage.setItem(globalKey, thisThemeNow);\n } else if (globalTheme && globalTheme !== thisThemeNow) {\n // A theme has been defined by another Ace window since we loaded.\n // Switch to that theme.\n this.editor.setOption('theme', globalTheme);\n thisThemeNow = globalTheme;\n }\n window.sessionStorage.setItem(thisEditorKey, thisThemeNow);\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 2;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n // Sometimes Ace editors do not load until the mouse is moved. To fix this,\n // 'move' the mouse using JQuery when the editor div enters the viewport.\n AceWrapper.prototype.fixSlowLoad = function () {\n const observer = new IntersectionObserver( () => {\n $(document).trigger('mousemove');\n });\n const editNode = this.editNode.get(0); // Non-JQuerry node.\n observer.observe(editNode);\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","session","code","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","enableLiveAutocompletion","live_autocompletion","fontSize","font_size","newLineMode","$blockScrolling","Infinity","getSession","val","undefined","import_from_scratchpad","extract_from_json_maybe","setValue","userTheme","localStorage","getItem","consider_prefers","auto_switch_light_dark","matchMedia","setTheme","matches","theme","fixSlowLoad","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","JSON","parse","answer_code","failed","failMessage","sync","thisThemeNow","getOption","thisEditorKey","lastTheme","sessionStorage","globalTheme","setItem","setOption","syncIntervalSecs","language","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","observer","IntersectionObserver","trigger","observe","getValue","container","addEventListener","e","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/ui_ace.js b/amd/src/ui_ace.js
index a287ff643..a83a2d55f 100644
--- a/amd/src/ui_ace.js
+++ b/amd/src/ui_ace.js
@@ -98,8 +98,7 @@ define(['jquery'], function($) {
// Otherwise use the 'prefers-color-scheme' option if given or
// the question/system defaults if not.
const userTheme = window.localStorage.getItem('qtype_coderunner.ace.theme');
- const consider_prefers = (params.auto_switch_light_dark === undefined || params.auto_switch_light_dark)
- && window.matchMedia;
+ const consider_prefers = params.auto_switch_light_dark && window.matchMedia;
if (userTheme !== null) {
this.editor.setTheme(userTheme);
} else if (consider_prefers && window.matchMedia('(prefers-color-scheme: dark)').matches) {
diff --git a/amd/src/ui_ace.json b/amd/src/ui_ace.json
index 2b687c47f..dfc27a370 100644
--- a/amd/src/ui_ace.json
+++ b/amd/src/ui_ace.json
@@ -3,7 +3,7 @@
"parameters": {
"auto_switch_light_dark": {
"type": "boolean",
- "default": true
+ "default": false
},
"font_size": {
"type": "string",
From 944bd71da17356c4cc58712e56188258b5acf306 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Tue, 7 Feb 2023 17:30:21 +1300
Subject: [PATCH 060/188] Provide more-meaningful response summary for
questions using the Scratchpad UI or similar.
---
question.php | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/question.php b/question.php
index 9b7bc7f2f..9c669a256 100644
--- a/question.php
+++ b/question.php
@@ -344,7 +344,14 @@ public function get_expected_data() {
public function summarise_response(array $response) {
if (isset($response['answer'])) {
- return $response['answer'];
+ $ans = $response['answer'];
+ if ($this->extractcodefromjson) {
+ $json = json_decode($ans, true);
+ if ($json !== null and isset($json[constants::ANSWER_CODE_KEY])) {
+ $ans = $json[constants::ANSWER_CODE_KEY][0];
+ }
+ }
+ return $ans;
} else {
return null;
}
From 390259169258d2005eef7e9f36b41b751e8ce9bd Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Tue, 7 Feb 2023 17:30:59 +1300
Subject: [PATCH 061/188] Adjust test to reflect changed default value of
auto_switch_light_dark parameter.
---
tests/ui_parameters_test.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/ui_parameters_test.php b/tests/ui_parameters_test.php
index 0400129ba..ea6a9a879 100644
--- a/tests/ui_parameters_test.php
+++ b/tests/ui_parameters_test.php
@@ -52,7 +52,7 @@ public function test_params() {
$this->assertContains('noderadius', array_keys($paramsarray));
$this->assertEquals(30, $paramsarray['noderadius']);
$aceparams = new \qtype_coderunner_ui_parameters('ace');
- $this->assertEquals(true, $aceparams->value('auto_switch_light_dark'));
+ $this->assertEquals(false, $aceparams->value('auto_switch_light_dark'));
$this->assertEquals("14px", $aceparams->value('font_size'));
$this->assertEquals(true, $aceparams->value('import_from_scratchpad'));
$this->assertEquals(false, $aceparams->value('live_autocompletion'));
From ef067b021cbd7a96edd1c8a9a749d24111f1405d Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Wed, 8 Feb 2023 11:32:55 +1300
Subject: [PATCH 062/188] Update documentation of HTML-UI.
---
Readme.md | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/Readme.md b/Readme.md
index 432905caa..15ebcc41e 100644
--- a/Readme.md
+++ b/Readme.md
@@ -2502,8 +2502,15 @@ with any other named HTML elements in the page. It is recommended that a prefix
of some sort, such as `crui_`, be used with all names.
When authoring a question that uses the Html UI, the answer and answer preload
-fields are *not* controlled by the UI, but are displayed as raw text.
-If data is to be entered into these fields,
+fields are by default also controlled by the UI. While this is most user-friendly presentation,
+it does not allow you to include Twig code in those fields. If you need
+to use Twig there, you must turn off the use of the UI within the question
+editing page by setting the UI parameter `enable_in_editor` to false:
+
+ {"enable_in_editor": false}
+
+The underlying serialisation is then displayed as raw JSON text.
+If data is to be entered into the HTML fields,
it must be of the form
{"": "",...}
@@ -2512,7 +2519,9 @@ where fieldValueList is a list of all the values to be assigned to the fields
with the given name, in document order. For complex UIs it is easiest to turn
off validate on save, save the question, preview it, enter the right answers into
all fields, type CTRL-ALT-M to switch off the UI and expose the serialisation,
-then copy that serialisation back into the author form.
+then copy that serialisation back into the author form. But this rigmarole is
+only necessary when you need to use Twig within the answer or sample answer,
+which is rare.
It is possible that the question author might want a dynamic answer box in
which the student can add extra fields. A simple example of this is the Table UI,
From 94a945754ff2a635257f6973f0017e48bf8291ea Mon Sep 17 00:00:00 2001
From: James Napier <61300251+jimbonothing64@users.noreply.github.com>
Date: Tue, 14 Feb 2023 14:21:27 +1300
Subject: [PATCH 063/188] Scratchpad Development (#165)
Change default delimiters for wrapper code and add UI parameter to allow per-question customisation of delimiters. Plus other tweaks and documentation changes.
---
ReadmeScratchpadUi.md | 228 +++---------------
amd/build/authorform.min.js | 2 +-
amd/build/authorform.min.js.map | 2 +-
amd/build/graphutil.min.js | 2 +-
amd/build/graphutil.min.js.map | 2 +-
amd/build/outputdisplayarea.min.js | 27 ++-
amd/build/outputdisplayarea.min.js.map | 2 +-
amd/build/textareas.min.js | 2 +-
amd/build/textareas.min.js.map | 2 +-
amd/build/ui_ace.min.js | 2 +-
amd/build/ui_ace.min.js.map | 2 +-
amd/build/ui_ace_gapfiller.min.js | 2 +-
amd/build/ui_ace_gapfiller.min.js.map | 2 +-
amd/build/ui_scratchpad.min.js | 56 ++++-
amd/build/ui_scratchpad.min.js.map | 2 +-
amd/build/userinterfacewrapper.min.js | 2 +-
amd/build/userinterfacewrapper.min.js.map | 2 +-
amd/src/outputdisplayarea.js | 50 ++--
amd/src/ui_scratchpad.js | 51 ++--
amd/src/ui_scratchpad.json | 12 +
lang/en/qtype_coderunner.php | 91 ++++---
.../ace_scratchpad_compatibility.feature | 74 ++++++
tests/behat/behat_coderunner.php | 47 +++-
tests/behat/scratchpad_ui.feature | 14 +-
tests/behat/scratchpad_ui_params.feature | 93 ++++++-
tests/fixtures/input_mpl_wrapper_json.py | 6 +-
tests/fixtures/input_wrapper_json.py | 83 +++++++
27 files changed, 563 insertions(+), 297 deletions(-)
create mode 100644 tests/behat/ace_scratchpad_compatibility.feature
create mode 100644 tests/fixtures/input_wrapper_json.py
diff --git a/ReadmeScratchpadUi.md b/ReadmeScratchpadUi.md
index 3897e14d3..656a6f66c 100644
--- a/ReadmeScratchpadUi.md
+++ b/ReadmeScratchpadUi.md
@@ -1,7 +1,7 @@
## Scratchpad UI
-The **Scratchpad UI** is an extension of the **Ace UI**:
-- The Scratchpad UI is designed to allow the execution of code in the CodeRunner question in a manner similar to an IDE.
-- The Scratchpad UI contains two editor boxes, one on top of another, allowing users to enter and edit code in both.
+The **Scratchpad UI** is an extension to the **Ace UI**:
+- It is designed to allow the execution of code in the CodeRunner question in a manner similar to an IDE.
+- It contains two editor boxes, one on top of another, allowing users to enter and edit code in both.
By default, only the top editor is visible and the **Scratchpad Area**, which contains the bottom editor, is hidden.
Clicking the **Scratchpad Button** shows the **Scratchpad Area**.
@@ -18,8 +18,25 @@ The Run Button has some limitations when using its default configuration:
Note: *These features can be supported, see wrappers...*
+
+### Switching a Question to Use the Scratchpad UI
+
+To switch an Ace UI question to use the Scratchpad UI:
+ 1. edit the question;
+ 2. make sure the "Ace/Scratchpad compliant" tick-box is checked;
+ 3. tick customise (second option, in first section);
+ 4. in the "Customisation" section, change "Input UIs" from "Ace" to "Scratchpad";
+ 5. save the question.
+
### Serialisation
-The UI state serialises to JSON, with fields:
+
+Pressing CTRL ALT M will disable the plugin, exposing the underlying serialisation.
+For most UIs this serialisation is passed into the question template as STUDENT_ANSWER.
+When "Ace/Scratchpad compliant" is ticked STUDENT_ANSWER is set to the value of the first editor instead.
+
+Note: The following information is not relevant unless you un-tick the "Ace/Scratchpad compliant" tick-box.
+
+The serialisation for this plugin is JSON, with fields:
- `answer_code`: `[""]` A list containing a string with answer code from the first editor;
- `test_code`: `[""]` A list containing a string with containing answer code from the second editor;
- `show_hide`: `["1"]` when scratchpad is visible, otherwise `[""]`;
@@ -44,32 +61,38 @@ A valid serialisation is one with all four specified fields. All other serialisa
- `text`: display program output as text, html escaped;
- `json`: display program output, when it is json, see next section...
- `html`: display program output as raw html.
-- `disable_scratchpad`: disable the scratchpad, effectively revert back to Ace UI from student perspective.
+- `open_delimiter`: The opening delimiter to use when inserting answer or Scratchpad code. It will replace the default value `{|`.
+- `close_delimiter`: The closing delimiter to use when inserting answer or Scratchpad code. It will replace the default value `|}`.
+- `disable_scratchpad`: disable the scratchpad, reverting to Ace UI from student perspective.
- `invert_prefix`: inverts meaning of prefix_ans serialisation -- `'1'` means un-ticked, vice versa. This can be used to swap the default state.
-- `params` : **THESE ARE NOT WELL DOCUMENTED**
+- `escape`: when `true` code will be JSON escaped (minus outer quotes `"`) before being inserted into the wrapper.
+- `params` : parameters for the sandbox webservice.
-
-### Advanced Customization: Wrappers
-A wrapper is be used to wrap code before it is run using the sandbox.
+### Wrappers
+A wrapper is used to wrap code before it is run using the sandbox.
A wrapper could be used to enclose the code in a function call, or to run the program as a subprocess after manipulation.
-Some tasks that require this include: running languages installed on Jobe but not supported by coderunner; reading standard input during runs; or displaying Matplotlib graphs.
+Some tasks that require this are: running languages installed on Jobe but not supported by coderunner; reading standard input during runs; or displaying Matplotlib graphs.
-You can insert the answer code and scratchpad code into the wrapper using `{{ ANSWER_CODE }}` and `{{ SCRATCHPAD_CODE }}` respectively.
-If the **Prefix with Answer** checkbox is unchecked `{{ ANSWER_CODE }}` will be replaced with an empty string `''`.
+You can insert the answer code and scratchpad code into the wrapper using `{| ANSWER_CODE |}` and `{| SCRATCHPAD_CODE |}` respectively.
+If the **Prefix with Answer** checkbox is unchecked `{| ANSWER_CODE |}` will be replaced with an empty string `''`.
The default configuration uses the following wrapper:
```
-{{ ANSWER_CODE }}
-{{ SCRATCHPAD_CODE }}
+{| ANSWER_CODE |}
+{| SCRATCHPAD_CODE |}
```
+Whitespace is ignored between the delimiters (`{|`,`|}`) and the variable name, e.g. `{|ANSWER_CODE |}` will be replaced.
+You can change the delimiters using the `open_delimiter` and `close_delimiter` UI Parameters.
+
-Three UI parameters are of particular importance when writing wrappers:
+Four UI parameters are of particular importance when writing wrappers:
- `wrapper_src` sets the location of the wrapper code.
- `run_lang` sets the language the Sandbox Webservice uses to run code when the **Run Button** is pressed.
- `output_display_mode` controls how run output is displayed, see below.
+- `escape` will escape (JSON escape with `"` removed from start and end) `ANSWER_CODE` and `SCRATCHPAD_CODE` before insertion into wrapper. Useful when inserting code into a string. NOTE: _single quotes `'` are NOT escaped.
There are three modes of displaying program run output, set by `output_display_mode`:
- `text`: Display the output as text, html escaped. **(default)**
@@ -88,178 +111,3 @@ There are three modes of displaying program run output, set by `output_display_m
Note: JSON is the preferred display mode; wrapper debugging is much simpler than HTML mode.
HTML output is only recommended if you are trying to display HTML elements and for very advanced users, enter at your own risk...
-
-[//]: # (## Wrapper tutorial (TBD))
-
-[//]: # (#### Wrappers I: Running code in unsupported languages)
-
-[//]: # (Wrappers can be in a different language to their question; you can set the Run language, using `run_lang`. )
-
-[//]: # (This changes the language the sandbox service uses to run the wrapper.)
-
-[//]: # (This would be invisible to the student answering the question. )
-
-[//]: # (Below is an example of a C program being wrapped using Python (see the multi-language question for further inspiration):)
-
-[//]: # ( ```)
-
-[//]: # ( import subprocess)
-
-[//]: # ( )
-[//]: # (student_answer = """{{ ANSWER_CODE }}""")
-
-[//]: # (test_code = """{{ SCRATCHPAD_CODE }}""")
-
-[//]: # (all_code = student_answer + '\n' + test_code)
-
-[//]: # ( filename = '__tester__.c')
-
-[//]: # ( with open(filename, "w") as src:)
-
-[//]: # ( print(all_code, file=src))
-
-[//]: # ()
-[//]: # (cflags = "-std=c99 -Wall -Werror")
-
-[//]: # (return_code = subprocess.call("gcc {0} -o __tester__ __tester__.c".format(cflags).split()))
-
-[//]: # (if return_code != 0:)
-
-[//]: # ( raise Exception("** Compilation failed. Testing aborted **"))
-
-[//]: # (exec_command = ["./__tester__"])
-
-[//]: # ( )
-[//]: # ( output = subprocess.check_output(exec_command, universal_newlines=True))
-
-[//]: # (print(output))
-
-[//]: # ( ```)
-
-[//]: # (Note: When writing wrappers it is recommended to use a scripting language with strong string manipulation features.)
-
-[//]: # ()
-[//]: # (#### Wrappers II: Displaying on-textual run output)
-
-[//]: # (The `html_output` parameter, in conjunction with a wrapper, can be used to display graphical/non-textual output in the output display area. Using HTML output, it is possible to insert images and input boxes.)
-
-[//]: # ()
-[//]: # (Example of a wrapper to display `Matplotlib` graphs in the output display area:)
-
-[//]: # (```)
-
-[//]: # (import subprocess, base64, html, os, tempfile)
-
-[//]: # ()
-[//]: # ()
-[//]: # (def make_data_uri(filename):)
-
-[//]: # ( with open(filename, "br") as fin:)
-
-[//]: # ( contents = fin.read())
-
-[//]: # ( contents_b64 = base64.b64encode(contents).decode("utf8"))
-
-[//]: # ( return "data:image/png;base64,{}".format(contents_b64))
-
-[//]: # ()
-[//]: # ()
-[//]: # (code = r"""{{ ANSWER_CODE }})
-
-[//]: # ({{ SCRATCHPAD_CODE }})
-
-[//]: # (""")
-
-[//]: # ()
-[//]: # (prefix = """import os, tempfile)
-
-[//]: # (os.environ["MPLCONFIGDIR"] = tempfile.mkdtemp())
-
-[//]: # (import matplotlib as _mpl)
-
-[//]: # (_mpl.use("Agg"))
-
-[//]: # (""")
-
-[//]: # ()
-[//]: # (suffix = """)
-
-[//]: # (figs = _mpl.pyplot.get_fignums())
-
-[//]: # (for i, fig in enumerate(figs):)
-
-[//]: # ( _mpl.pyplot.figure(fig))
-
-[//]: # ( filename = f'image{i}.png')
-
-[//]: # ( _mpl.pyplot.savefig(filename, bbox_inches='tight'))
-
-[//]: # (""")
-
-[//]: # ()
-[//]: # (prog_to_exec = prefix + code + suffix)
-
-[//]: # ()
-[//]: # (with open('prog.py', 'w') as outfile:)
-
-[//]: # ( outfile.write(prog_to_exec))
-
-[//]: # ()
-[//]: # (result = subprocess.run(['python3', 'prog.py'], capture_output=True, text=True))
-
-[//]: # (print('
'))
-
-[//]: # ()
-[//]: # (for fname in os.listdir():)
-
-[//]: # ( if fname.endswith('png'):)
-
-[//]: # ( print(f''))
-
-[//]: # (```)
-
-[//]: # (#### Wrappers III: Reading stdin during runs)
-
-[//]: # (A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally)
-
-[//]: # (developed for use in the Scratchpad UI. It has three modes of operation:)
-
-[//]: # (- 'text': Just display the output as text, html escaped.)
-
-[//]: # (- 'json': The recommended way to display programs that use stdin or output images (or both).)
-
-[//]: # ( - Accepts JSON in the CRWS response output with fields:)
-
-[//]: # ( - `"returncode"`: Error/return code from running program.)
-
-[//]: # ( - `"stdout"`: Stdout text from running program.)
-
-[//]: # ( - `"stderr"`: Error text from running program.)
-
-[//]: # ( - `"files"`: Images encoded in base64 text encoding. These will be displayed above any stdout text.)
-
-[//]: # ( - When input from stdin is required the returncode 42 should be returned, raise this)
-
-[//]: # ( any time the program asks for input. An (html) input will be added after the last stdout received.)
-
-[//]: # ( When enter is pressed, runCode is called with value of the input added to the stdin string.)
-
-[//]: # ( This repeats until returncode is no longer 42.)
-
-[//]: # (- 'html': Display program output as raw html inside the output area.)
-
-[//]: # ( - This can be used to show images and insert other HTML tags (and beyond).)
-
-[//]: # ( - Giving an `` tag the class 'coderunner-run-input' will add an event that)
-
-[//]: # ( on pressing enter will call the runCode method again with the value of that input field added to stdin.)
-
-[//]: # ( This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.)
\ No newline at end of file
diff --git a/amd/build/authorform.min.js b/amd/build/authorform.min.js
index 1d7c6c937..20d22f825 100644
--- a/amd/build/authorform.min.js
+++ b/amd/build/authorform.min.js
@@ -5,6 +5,6 @@
* @copyright Richard Lobb, 2015, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/authorform",["jquery","qtype_coderunner/userinterfacewrapper","core/str"],(function($,ui,str){var currentQtype="",JSON_TO_FORM_MAP={template:["#id_template","value",""],iscombinatortemplate:["#id_iscombinatortemplate","checked","",function(value){return"1"===value}],cputimelimitsecs:["#id_cputimelimitsecs","value",""],memlimitmb:["#id_memlimitmb","value",""],sandbox:["#id_sandbox","value","DEFAULT"],sandboxparams:["#id_sandboxparams","value",""],testsplitterre:["#id_testsplitterre","value","",function(splitter){return splitter.replace("\n","\\n")}],allowmultiplestdins:["#id_allowmultiplestdins","checked","",function(value){return"1"===value}],grader:["#id_grader","value","EqualityGrader"],resultcolumns:["#id_resultcolumns","value",""],language:["#id_language","value",""],acelang:["#id_acelang","value",""],uiplugin:["#id_uiplugin","value","ace"]};return{initEditForm:function(){var messagePara,typeCombo=$("#id_coderunnertype"),prototypeDisplay=$("#id_isprototype"),template=$("#id_template"),evaluatePerStudent=$("#id_templateparamsevalpertry"),globalextra=$("#id_globalextra"),prototypeextra=$("#id_prototypeextra"),useace=$("#id_useace"),language=$("#id_language"),acelang=$("#id_acelang"),customise=$("#id_customise"),isCombinator=$("#id_iscombinatortemplate"),testSplitterRe=$("#id_testsplitterre"),allowMultipleStdins=$("#id_allowmultiplestdins"),customisationFieldSet=$("#id_customisationheader"),advancedCustomisation=$("#id_advancedcustomisationheader"),isCustomised=customise.prop("checked"),prototypeType=$("#id_prototypetype"),preloadHdr=$("#id_answerpreloadhdr"),courseId=$('input[name="courseid"]').prop("value"),questiontypeHelpDiv=$("#qtype-help"),precheck=$("select#id_precheck"),testtypedivs=$("div.testtype"),brokenQuestion=$("#id_broken_question"),badQuestionLoad=$("#id_bad_question_load"),uiplugin=$("#id_uiplugin"),uiparameters=$("#id_uiparameters");function setUi(taId,uiname){var lang,uiWrapper,ta=$(document.getElementById(taId)),currentLang=ta.attr("data-lang"),paramsJson=ta.attr("data-params"),params={};ta.attr("data-prototypeextra",prototypeextra.val()),ta.attr("data-globalextra",globalextra.val()),ta.attr("data-test0",$("#id_testcode_0").val());try{params=JSON.parse(paramsJson)}catch(err){}"none"===(uiname=uiname.toLowerCase())&&(uiname=""),"id_templateparams"==taId||"id_uiparameters"==taId?lang="":(lang=language.prop("value"),"id_template"!==taId&&acelang.prop("value")&&(lang=function(acelang){var langs,i;if(acelang.indexOf(",")<0)return acelang;for(langs=acelang.split(","),i=0;i0?langs[0]:""}(acelang.prop("value")))),(uiWrapper=ta.data("current-ui-wrapper"))&&uiWrapper.uiname===uiname&¤tLang==lang||(ta.attr("data-lang",lang),uiWrapper?(params.lang=lang,uiWrapper.loadUi(uiname,params)):uiWrapper=new ui.InterfaceWrapper(uiname,taId))}function setUis(){var uiname=uiplugin.val(),answer=$("#id_answer"),enableUi=!0;if("html"===uiname&&""!==answer.attr("data-params"))try{!1===JSON.parse(answer.attr("data-params")).enable_in_editor&&(enableUi=!1)}catch(error){alert("Invalid UI parameters.")}enableUi&&(setUi("id_answer",uiname),setUi("id_answerpreload",uiname))}function setCustomisationVisibility(isVisible){var display=isVisible?"block":"none";customisationFieldSet.css("display",display),advancedCustomisation.css("display",display),isVisible&&useace.prop("checked")&&setUi("id_template","ace")}function copyFieldsFromQuestionType(newType,response){var formspecifier,attrval,isCombinatorEnabled;for(var key in function(stateOn){var uiWrapper,taIds=["id_template","id_uiparameters"];if(useace.prop("checked"))for(var i=0;i3&&(attrval=(0,formspecifier[3])(attrval)),$(formspecifier[0]).prop(formspecifier[1],attrval);customise.prop("checked",!1),str.get_string("coderunner_question_type","qtype_coderunner").then((function(s){var title,coderunner_descr,html,resultHtml;questiontypeHelpDiv.html((title=newType,coderunner_descr=s,html=response.questiontext,resultHtml='
")}))):loadCustomisationFields(),set_testtype_visibilities(),useace.prop("checked")&&(setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")),loadUiParametersDescription(),customise.on("change",(function(){customise.prop("checked")?setCustomisationVisibility(!0):str.get_string("confirm_proceed","qtype_coderunner").then((function(s){window.confirm(s)?setCustomisationVisibility(!1):customise.prop("checked",!0)}))})),acelang.on("change",check_ace_lang),language.on("change",(function(){useace.prop("checked")&&setUi("id_template","ace"),check_ace_lang()})),typeCombo.on("change",(function(){customise.prop("checked")?str.get_string("question_type_changed","qtype_coderunner").then((function(s){window.confirm(s)&&loadCustomisationFields()})):loadCustomisationFields()})),useace.on("change",(function(){useace.prop("checked")?(setUi("id_template","ace"),setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")):(setUi("id_template",""),setUi("id_templateparams",""),setUi("id_uiparameters",""))})),evaluatePerStudent.on("change",(function(){evaluatePerStudent.is(":checked")&&langStringAlert("templateparamsusingsandbox")})),uiplugin.on("change",(function(){setUis(),loadUiParametersDescription()})),precheck.on("change",set_testtype_visibilities),prototypeType.on("change",(function(){"0"==prototypeType.prop("value")?prototypeDisplay.attr("hidden","1"):prototypeDisplay.removeAttr("hidden")})),new MutationObserver((function(){setUis()})).observe(preloadHdr.get(0),{attributes:!0}),$("button.replaceexpectedwithgot").click((function(){var gotPre=$(this).prev('pre[id^="id_got_"]'),testCaseId=gotPre.attr("id").replace("id_got_","");$("#id_expected_"+testCaseId).val(gotPre.text()),$("#id_fail_expected_"+testCaseId).html(gotPre.text()),$(".failrow_"+testCaseId).addClass("fixed"),$(this).prop("disabled",!0)})),$(".btn-primary").click((function(){typeCombo.prop("disabled",!1)}))}}}));
//# sourceMappingURL=authorform.min.js.map
\ No newline at end of file
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
index adb230c7d..02b9de9e9 100644
--- a/amd/build/authorform.min.js.map
+++ b/amd/build/authorform.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * The sample answer and answer preload have the data-params attribute which contains\n * the UI params in a JSON from the current question merged with the prototype.\n * Both of them are identical and are changed simultaneously; only checking\n * answer as state is identical.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n *\n */\n function setUis() {\n let uiname = uiplugin.val();\n let answer = $('#id_answer');\n let enableUi = true;\n if (uiname === 'html' && answer.attr('data-params') !== '') {\n try {\n let answerparams = JSON.parse(answer.attr('data-params'));\n if (answerparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","messagePara","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","answer","enableUi","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","trim","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * The sample answer and answer preload have the data-params attribute which contains\n * the UI params in a JSON from the current question merged with the prototype.\n * Both of them are identical and are changed simultaneously; only checking\n * answer as state is identical.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n *\n */\n function setUis() {\n let uiname = uiplugin.val();\n let answer = $('#id_answer');\n let enableUi = true;\n if (uiname === 'html' && answer.attr('data-params') !== '') {\n try {\n let answerparams = JSON.parse(answer.attr('data-params'));\n if (answerparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","answer","enableUi","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","trim","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/graphutil.min.js b/amd/build/graphutil.min.js
index feb02fc20..93b742731 100644
--- a/amd/build/graphutil.min.js
+++ b/amd/build/graphutil.min.js
@@ -1,3 +1,3 @@
-define("qtype_coderunner/graphutil",(function(){function Util(){this.greekLetterNames=["Alpha","Beta","Gamma","Delta","Epsilon","Zeta","Eta","Theta","Iota","Kappa","Lambda","Mu","Nu","Xi","Omicron","Pi","Rho","Sigma","Tau","Upsilon","Phi","Chi","Psi","Omega"]}return Util.prototype.convertLatexShortcuts=function(text){for(var i=0;i16)))).replace(new RegExp("\\\\"+name.toLowerCase(),"g"),String.fromCharCode(945+i+(i>16)))}for(i=0;i<10;i++)text=text.replace(new RegExp("_"+i,"g"),String.fromCharCode(8320+i));return text=text.replace(new RegExp("_a","g"),String.fromCharCode(8336))},Util.prototype.drawArrow=function(c,x,y,angle){var dx=Math.cos(angle),dy=Math.sin(angle);c.beginPath(),c.moveTo(x,y),c.lineTo(x-8*dx+5*dy,y-8*dy-5*dx),c.lineTo(x-8*dx-5*dy,y-8*dy+5*dx),c.fill()},Util.prototype.det=function(a,b,c,d,e,f,g,h,i){return a*e*i+b*f*g+c*d*h-a*f*h-b*d*i-c*e*g},Util.prototype.vectorMagnitude=function(v){return Math.sqrt(v.x*v.x+v.y*v.y)},Util.prototype.scalarProjection=function(a,b){return(a.x*b.x+a.y*b.y)/this.vectorMagnitude(b)},Util.prototype.isCCW=function(a,b){return a.x*b.y-b.x*a.y>0},Util.prototype.circleFromThreePoints=function(x1,y1,x2,y2,x3,y3){var a=this.det(x1,y1,1,x2,y2,1,x3,y3,1),bx=-this.det(x1*x1+y1*y1,y1,1,x2*x2+y2*y2,y2,1,x3*x3+y3*y3,y3,1),by=this.det(x1*x1+y1*y1,x1,1,x2*x2+y2*y2,x2,1,x3*x3+y3*y3,x3,1),c=-this.det(x1*x1+y1*y1,x1,y1,x2*x2+y2*y2,x2,y2,x3*x3+y3*y3,x3,y3);return{x:-bx/(2*a),y:-by/(2*a),radius:Math.sqrt(bx*bx+by*by-4*a*c)/(2*Math.abs(a))}},Util.prototype.isInside=function(pos,rect){return pos.x>rect.x&&pos.xrect.y},Util.prototype.crossBrowserKey=function(e){return(e=e||window.event).which||e.keyCode},Util.prototype.crossBrowserRelativeMousePos=function(e){var rect=e.target.getBoundingClientRect();return{x:e.clientX-rect.left,y:e.clientY-rect.top}},new Util}));
+define("qtype_coderunner/graphutil",(function(){function Util(){this.greekLetterNames=["Alpha","Beta","Gamma","Delta","Epsilon","Zeta","Eta","Theta","Iota","Kappa","Lambda","Mu","Nu","Xi","Omicron","Pi","Rho","Sigma","Tau","Upsilon","Phi","Chi","Psi","Omega"]}return Util.prototype.convertLatexShortcuts=function(text){for(var i=0;i16)))).replace(new RegExp("\\\\"+name.toLowerCase(),"g"),String.fromCharCode(945+i+(i>16)))}for(i=0;i<10;i++)text=text.replace(new RegExp("_"+i,"g"),String.fromCharCode(8320+i));return text=text.replace(new RegExp("_a","g"),String.fromCharCode(8336))},Util.prototype.drawArrow=function(c,x,y,angle){var dx=Math.cos(angle),dy=Math.sin(angle);c.beginPath(),c.moveTo(x,y),c.lineTo(x-8*dx+5*dy,y-8*dy-5*dx),c.lineTo(x-8*dx-5*dy,y-8*dy+5*dx),c.fill()},Util.prototype.det=function(a,b,c,d,e,f,g,h,i){return a*e*i+b*f*g+c*d*h-a*f*h-b*d*i-c*e*g},Util.prototype.vectorMagnitude=function(v){return Math.sqrt(v.x*v.x+v.y*v.y)},Util.prototype.scalarProjection=function(a,b){return(a.x*b.x+a.y*b.y)/this.vectorMagnitude(b)},Util.prototype.isCCW=function(a,b){return a.x*b.y-b.x*a.y>0},Util.prototype.circleFromThreePoints=function(x1,y1,x2,y2,x3,y3){var a=this.det(x1,y1,1,x2,y2,1,x3,y3,1),bx=-this.det(x1*x1+y1*y1,y1,1,x2*x2+y2*y2,y2,1,x3*x3+y3*y3,y3,1),by=this.det(x1*x1+y1*y1,x1,1,x2*x2+y2*y2,x2,1,x3*x3+y3*y3,x3,1),c=-this.det(x1*x1+y1*y1,x1,y1,x2*x2+y2*y2,x2,y2,x3*x3+y3*y3,x3,y3);return{x:-bx/(2*a),y:-by/(2*a),radius:Math.sqrt(bx*bx+by*by-4*a*c)/(2*Math.abs(a))}},Util.prototype.isInside=function(pos,rect){return pos.x>rect.x&&pos.xrect.y},Util.prototype.crossBrowserKey=function(e){return(e=e||window.event).which||e.keyCode},Util.prototype.crossBrowserRelativeMousePos=function(e){const rect=e.target.getBoundingClientRect();return{x:e.clientX-rect.left,y:e.clientY-rect.top}},new Util}));
//# sourceMappingURL=graphutil.min.js.map
\ No newline at end of file
diff --git a/amd/build/graphutil.min.js.map b/amd/build/graphutil.min.js.map
index 5f11864b8..23ac65002 100644
--- a/amd/build/graphutil.min.js.map
+++ b/amd/build/graphutil.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"graphutil.min.js","sources":["../src/graphutil.js"],"sourcesContent":["/***********************************************************************\n *\n * Utility functions/data/constants for the ui_graph module.\n *\n ***********************************************************************/\n// Most of this code is taken from Finite State Machine Designer:\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(function() {\n /**\n * Contstructor for the Util class.\n */\n function Util() {\n\n this.greekLetterNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',\n 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda',\n 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',\n 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega' ];\n }\n\n Util.prototype.convertLatexShortcuts = function(text) {\n // Html greek characters.\n for(var i = 0; i < this.greekLetterNames.length; i++) {\n var name = this.greekLetterNames[i];\n text = text.replace(new RegExp('\\\\\\\\' + name, 'g'), String.fromCharCode(913 + i + (i > 16)));\n text = text.replace(new RegExp('\\\\\\\\' + name.toLowerCase(), 'g'), String.fromCharCode(945 + i + (i > 16)));\n }\n\n // Subscripts.\n for(var i = 0; i < 10; i++) {\n text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));\n }\n text = text.replace(new RegExp('_a', 'g'), String.fromCharCode(8336));\n return text;\n };\n\n Util.prototype.drawArrow = function(c, x, y, angle) {\n // Draw an arrow head on the graphics context c at (x, y) with given angle.\n\n var dx = Math.cos(angle);\n var dy = Math.sin(angle);\n c.beginPath();\n c.moveTo(x, y);\n c.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);\n c.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);\n c.fill();\n };\n\n Util.prototype.det = function(a, b, c, d, e, f, g, h, i) {\n // Determinant of given matrix elements.\n return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g;\n };\n\n Util.prototype.vectorMagnitude = function(v){\n // Returns magnitude (length) of a vector v\n return Math.sqrt(v.x * v.x + v.y * v.y);\n };\n\n Util.prototype.scalarProjection = function(a, b) {\n // Returns scalar projection of vector a onto vector b\n return (a.x * b.x + a.y * b.y) / this.vectorMagnitude(b);\n };\n\n Util.prototype.isCCW = function(a, b) {\n // Returns true iff vector b is in a counter-clockwise orientation relative to a\n return (a.x * b.y) - (b.x * a.y) > 0;\n };\n\n Util.prototype.circleFromThreePoints = function(x1, y1, x2, y2, x3, y3) {\n // Return {x, y, radius} of circle through (x1, y1), (x2, y2), (x3, y3).\n var a = this.det(x1, y1, 1, x2, y2, 1, x3, y3, 1);\n var bx = -this.det(x1 * x1 + y1 * y1, y1, 1, x2 * x2 + y2 * y2, y2, 1, x3 * x3 + y3 * y3, y3, 1);\n var by = this.det(x1 * x1 + y1 * y1, x1, 1, x2 * x2 + y2 * y2, x2, 1, x3 * x3 + y3 * y3, x3, 1);\n var c = -this.det(x1 * x1 + y1 * y1, x1, y1, x2 * x2 + y2 * y2, x2, y2, x3 * x3 + y3 * y3, x3, y3);\n return {\n 'x': -bx / (2 * a),\n 'y': -by / (2 * a),\n 'radius': Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))\n };\n };\n\n Util.prototype.isInside = function(pos, rect) {\n // True iff given point pos is inside rectangle.\n return pos.x > rect.x && pos.x < rect.x + rect.width && pos.y < rect.y + rect.height && pos.y > rect.y;\n };\n\n Util.prototype.crossBrowserKey = function(e) {\n // Return which key was pressed, given the event, in a browser-independent way.\n e = e || window.event;\n return e.which || e.keyCode;\n };\n\n\n Util.prototype.crossBrowserRelativeMousePos = function(e) {\n // Earlier complex version was breaking in Moodle 4, so replaced\n // with this much simpler version that should work with all modern\n // browsers.\n const rect = e.target.getBoundingClientRect();\n const x = e.clientX - rect.left; //x position within the element.\n const y = e.clientY - rect.top; //y position within the element.\n return {'x': x, 'y': y};\n };\n\n return new Util();\n});\n"],"names":["define","Util","greekLetterNames","prototype","convertLatexShortcuts","text","i","this","length","name","replace","RegExp","String","fromCharCode","toLowerCase","drawArrow","c","x","y","angle","dx","Math","cos","dy","sin","beginPath","moveTo","lineTo","fill","det","a","b","d","e","f","g","h","vectorMagnitude","v","sqrt","scalarProjection","isCCW","circleFromThreePoints","x1","y1","x2","y2","x3","y3","bx","by","abs","isInside","pos","rect","width","height","crossBrowserKey","window","event","which","keyCode","crossBrowserRelativeMousePos","target","getBoundingClientRect","clientX","left","clientY","top"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"graphutil.min.js","sources":["../src/graphutil.js"],"sourcesContent":["/***********************************************************************\n *\n * Utility functions/data/constants for the ui_graph module.\n *\n ***********************************************************************/\n// Most of this code is taken from Finite State Machine Designer:\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(function() {\n /**\n * Contstructor for the Util class.\n */\n function Util() {\n\n this.greekLetterNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',\n 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda',\n 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',\n 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega' ];\n }\n\n Util.prototype.convertLatexShortcuts = function(text) {\n // Html greek characters.\n for(var i = 0; i < this.greekLetterNames.length; i++) {\n var name = this.greekLetterNames[i];\n text = text.replace(new RegExp('\\\\\\\\' + name, 'g'), String.fromCharCode(913 + i + (i > 16)));\n text = text.replace(new RegExp('\\\\\\\\' + name.toLowerCase(), 'g'), String.fromCharCode(945 + i + (i > 16)));\n }\n\n // Subscripts.\n for(var i = 0; i < 10; i++) {\n text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));\n }\n text = text.replace(new RegExp('_a', 'g'), String.fromCharCode(8336));\n return text;\n };\n\n Util.prototype.drawArrow = function(c, x, y, angle) {\n // Draw an arrow head on the graphics context c at (x, y) with given angle.\n\n var dx = Math.cos(angle);\n var dy = Math.sin(angle);\n c.beginPath();\n c.moveTo(x, y);\n c.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);\n c.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);\n c.fill();\n };\n\n Util.prototype.det = function(a, b, c, d, e, f, g, h, i) {\n // Determinant of given matrix elements.\n return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g;\n };\n\n Util.prototype.vectorMagnitude = function(v){\n // Returns magnitude (length) of a vector v\n return Math.sqrt(v.x * v.x + v.y * v.y);\n };\n\n Util.prototype.scalarProjection = function(a, b) {\n // Returns scalar projection of vector a onto vector b\n return (a.x * b.x + a.y * b.y) / this.vectorMagnitude(b);\n };\n\n Util.prototype.isCCW = function(a, b) {\n // Returns true iff vector b is in a counter-clockwise orientation relative to a\n return (a.x * b.y) - (b.x * a.y) > 0;\n };\n\n Util.prototype.circleFromThreePoints = function(x1, y1, x2, y2, x3, y3) {\n // Return {x, y, radius} of circle through (x1, y1), (x2, y2), (x3, y3).\n var a = this.det(x1, y1, 1, x2, y2, 1, x3, y3, 1);\n var bx = -this.det(x1 * x1 + y1 * y1, y1, 1, x2 * x2 + y2 * y2, y2, 1, x3 * x3 + y3 * y3, y3, 1);\n var by = this.det(x1 * x1 + y1 * y1, x1, 1, x2 * x2 + y2 * y2, x2, 1, x3 * x3 + y3 * y3, x3, 1);\n var c = -this.det(x1 * x1 + y1 * y1, x1, y1, x2 * x2 + y2 * y2, x2, y2, x3 * x3 + y3 * y3, x3, y3);\n return {\n 'x': -bx / (2 * a),\n 'y': -by / (2 * a),\n 'radius': Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))\n };\n };\n\n Util.prototype.isInside = function(pos, rect) {\n // True iff given point pos is inside rectangle.\n return pos.x > rect.x && pos.x < rect.x + rect.width && pos.y < rect.y + rect.height && pos.y > rect.y;\n };\n\n Util.prototype.crossBrowserKey = function(e) {\n // Return which key was pressed, given the event, in a browser-independent way.\n e = e || window.event;\n return e.which || e.keyCode;\n };\n\n\n Util.prototype.crossBrowserRelativeMousePos = function(e) {\n // Earlier complex version was breaking in Moodle 4, so replaced\n // with this much simpler version that should work with all modern\n // browsers.\n const rect = e.target.getBoundingClientRect();\n const x = e.clientX - rect.left; //x position within the element.\n const y = e.clientY - rect.top; //y position within the element.\n return {'x': x, 'y': y};\n };\n\n return new Util();\n});\n"],"names":["define","Util","greekLetterNames","prototype","convertLatexShortcuts","text","i","this","length","name","replace","RegExp","String","fromCharCode","toLowerCase","drawArrow","c","x","y","angle","dx","Math","cos","dy","sin","beginPath","moveTo","lineTo","fill","det","a","b","d","e","f","g","h","vectorMagnitude","v","sqrt","scalarProjection","isCCW","circleFromThreePoints","x1","y1","x2","y2","x3","y3","bx","by","abs","isInside","pos","rect","width","height","crossBrowserKey","window","event","which","keyCode","crossBrowserRelativeMousePos","target","getBoundingClientRect","clientX","left","clientY","top"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/outputdisplayarea.min.js b/amd/build/outputdisplayarea.min.js
index 713ce73c5..cbda09b8b 100644
--- a/amd/build/outputdisplayarea.min.js
+++ b/amd/build/outputdisplayarea.min.js
@@ -1,3 +1,28 @@
-define("qtype_coderunner/outputdisplayarea",["exports","core/ajax","core/str"],(function(_exports,_ajax,_str){var obj;function _slicedToArray(arr,i){return function(arr){if(Array.isArray(arr))return arr}(arr)||function(arr,i){var _i=null==arr?null:"undefined"!=typeof Symbol&&arr[Symbol.iterator]||arr["@@iterator"];if(null==_i)return;var _s,_e,_arr=[],_n=!0,_d=!1;try{for(_i=_i.call(arr);!(_n=(_s=_i.next()).done)&&(_arr.push(_s.value),!i||_arr.length!==i);_n=!0);}catch(err){_d=!0,_e=err}finally{try{_n||null==_i.return||_i.return()}finally{if(_d)throw _e}}return _arr}(arr,i)||_unsupportedIterableToArray(arr,i)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function _toConsumableArray(arr){return function(arr){if(Array.isArray(arr))return _arrayLikeToArray(arr)}(arr)||function(iter){if("undefined"!=typeof Symbol&&null!=iter[Symbol.iterator]||null!=iter["@@iterator"])return Array.from(iter)}(arr)||_unsupportedIterableToArray(arr)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function _unsupportedIterableToArray(o,minLen){if(o){if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);return"Object"===n&&o.constructor&&(n=o.constructor.name),"Map"===n||"Set"===n?Array.from(o):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?_arrayLikeToArray(o,minLen):void 0}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i1&&void 0!==arguments[1]?arguments[1]:"png",image=document.createElement("img");return image.src="data:image/".concat(type,";base64,").concat(base64),image},OutputDisplayArea=function(){function OutputDisplayArea(displayAreaId,outputMode,lang,sandboxParams){!function(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}(this,OutputDisplayArea),this.displayAreaId=displayAreaId,this.lang=lang,this.mode=outputMode,this.sandboxParams=sandboxParams,this.textDisplay=document.getElementById(displayAreaId+"-text"),this.imageDisplay=document.getElementById(displayAreaId+"-images"),this.prevRunSettings=null}var Constructor,protoProps,staticProps;return Constructor=OutputDisplayArea,protoProps=[{key:"clearDisplay",value:function(){this.textDisplay.innerHTML="",this.imageDisplay.innerHTML=""}},{key:"displayText",value:function(response){this.textDisplay.innerText=combinedOutput(response)}},{key:"displayHtml",value:function(response){this.textDisplay.innerHTML=combinedOutput(response);var inputEl=this.textDisplay.querySelector(".coderunner-run-input");inputEl&&this.addInputEvents(inputEl)}},{key:"displayJson",value:function(response){var result=this.validateJson(response.output),text=result.stdout;42!==result.returncode&&(text+=result.stderr),13==result.returncode&&(text+="\n*** Timeout error ***\n");var numImages=this.displayImages(result.files);""===text.trim()&&42!==result.returncode?0==numImages&&this.displayNoOutput(null):this.textDisplay.innerText=text,42===result.returncode&&this.addInput()}},{key:"validateJson",value:function(jsonString){var result=null;try{result=JSON.parse(jsonString)}catch(e){window.alert("Error parsing display JSON output: \n ".concat(jsonString,"\n JSON Parsing error Msg: \n ").concat(e.message,"\n The question author must fix this!"))}var missing=function(obj,props){return props.filter((function(prop){return!obj.hasOwnProperty(prop)}))}(result,JSON_DISPLAY_PROPS);return missing.length>0&&window.alert("Display JSON (in response.result) is missing the following fields:\n ".concat(missing.join(),"\n The question author must fix this!")),result}},{key:"displayNoOutput",value:function(response){var isNoOutput=!response||0===combinedOutput(response).length;if(isNoOutput||null===response){var span=document.createElement("span");span.style.color="red",span.innerText="< No output! >",this.clearDisplay(),this.textDisplay.append(span)}return isNoOutput}},{key:"display",value:function(response){var error=function(response){for(var ERROR_RESPONSES=[[1,0,"error_access_denied"],[2,0,"error_unknown_language"],[3,0,"error_access_denied"],[4,0,"error_submission_limit_reached"],[5,0,"error_sandbox_server_overload"],[0,11,""],[0,12,""],[0,13,"error_timeout"],[0,15,""],[0,17,"error_memory_limit"],[0,21,"error_sandbox_server_overload"],[0,30,"error_excessive_output"]],i=0;i2&&void 0!==arguments[2]&&arguments[2];this.prevRunSettings=[code,stdin],shouldClearDisplay&&this.clearDisplay(),_ajax.default.call([{methodname:"qtype_coderunner_run_in_sandbox",args:{contextid:M.cfg.contextid,sourcecode:code,language:this.lang,stdin:stdin,params:JSON.stringify(this.sandboxParams)},done:function(responseJson){var response=JSON.parse(responseJson);_this.display(response)},fail:function(error){alert(error.message)}}])}},{key:"addInput",value:function(){var inputId="".concat(this.displayAreaId,"-input-field");this.textDisplay.innerHTML+='');var inputEl=document.getElementById(inputId);this.addInputEvents(inputEl)}},{key:"addInputEvents",value:function(inputEl){var _this2=this;inputEl.focus(),inputEl.addEventListener("keydown",(function(e){13===e.keyCode&&e.preventDefault()})),inputEl.addEventListener("keyup",(function(e){if(13===e.keyCode){var line=inputEl.value;inputEl.remove(),_this2.textDisplay.innterHTML+=line,_this2.prevRunSettings[1]+=line+"\n",_this2.runCode.apply(_this2,_toConsumableArray(_this2.prevRunSettings).concat([!1]))}}))}},{key:"displayImages",value:function(files){for(var numImages=0,_i=0,_Object$entries=Object.entries(files);_i<_Object$entries.length;_i++){var _Object$entries$_i=_slicedToArray(_Object$entries[_i],2),fname=_Object$entries$_i[0],fcontents=_Object$entries$_i[1],fileType=fname.split(".")[1];if(fileType){var image=getImage(fcontents,fileType);this.imageDisplay.append(image),numImages+=1}else window.alert("Could not read filename correctly: ".concat(fname))}return numImages}}],protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),OutputDisplayArea}();_exports.OutputDisplayArea=OutputDisplayArea}));
+define("qtype_coderunner/outputdisplayarea",["exports","core/ajax","core/str"],(function(_exports,_ajax,_str){var obj;
+/**
+ * A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally
+ * developed for use in the Scratchpad UI. It has three modes of operation:
+ * - 'text': Just display the output as text, html escaped.
+ * - 'json': The recommended way to display programs that use stdin or output images (or both).
+ * - Accepts JSON in the CRWS response output with fields:
+ * - "returncode": Error/return code from running program.
+ * - "stdout": Stdout text from running program.
+ * - "stderr": Error text from running program.
+ * - "files": An object containing filenames mapped to base64 encoded images.
+ * These will be displayed below any stdout text.
+ * - When input from stdin is required the returncode 42 should be returned, raise this
+ * any time the program asks for input. An (html) input will be added after the last stdout received.
+ * When enter is pressed, runCode is called with value of the input added to the stdin string.
+ * This repeats until returncode is no longer 42.
+ * - 'html': Display program output as raw html inside the output area.
+ * - This can be used to show images and insert other HTML tags (and beyond).
+ * - Giving an tag the class 'coderunner-run-input' will add an event that
+ * on pressing enter will call the runCode method again with the value of that input field added to stdin.
+ * This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.
+ *
+ * @module qtype_coderunner/outputdisplayarea
+ * @copyright James Napier, 2023, The University of Canterbury
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.OutputDisplayArea=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};const JSON_DISPLAY_PROPS=["returncode","stdout","stderr","files"],setLangString=async function(langStringName,node,callback){let additionalText=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"",message=await(0,_str.get_string)(langStringName,"qtype_coderunner");langStringName.includes("error")&&(message="*** "+message+" ***\n"),additionalText&&(message+=additionalText),callback instanceof Function?callback(node,message):node.innerText=message},combinedOutput=response=>response.cmpinfo+response.output+response.stderr,getImage=function(base64){let type=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"png";const image=document.createElement("img");return image.src="data:image/".concat(type,";base64,").concat(base64),image};_exports.OutputDisplayArea=class{constructor(displayAreaId,outputMode,lang,sandboxParams){this.displayAreaId=displayAreaId,this.lang=lang,this.mode=outputMode,this.sandboxParams=sandboxParams,this.textDisplay=document.getElementById(displayAreaId+"-text"),this.imageDisplay=document.getElementById(displayAreaId+"-images"),this.prevRunSettings=null}clearDisplay(){this.textDisplay.innerHTML="",this.imageDisplay.innerHTML=""}displayText(response){this.textDisplay.innerText=combinedOutput(response)}displayHtml(response){this.textDisplay.innerHTML=combinedOutput(response);const inputEl=this.textDisplay.querySelector(".coderunner-run-input");inputEl&&this.addInputEvents(inputEl)}displayJson(response){const result=this.validateJson(response.output);let text=result.stdout;42!==result.returncode&&(text+=result.stderr),13==result.returncode&&setLangString("error_timeout",this.textDisplay,((node,msg)=>{node.innerText+=msg}));const numImages=this.displayImages(result.files);""===text.trim()&&42!==result.returncode?0==numImages&&this.displayNoOutput(null):this.textDisplay.innerText=text,42===result.returncode&&this.addInput()}validateJson(jsonString){let result=null;try{result=JSON.parse(jsonString)}catch(e){window.alert("Error parsing display JSON output: \n"+"'".concat(jsonString,"\n'")+"Error Msg: \n"+" ".concat(e.message," \n")+"The question author must fix this!")}const missing=((obj,props)=>props.filter((prop=>!obj.hasOwnProperty(prop))))(result,JSON_DISPLAY_PROPS);return missing.length>0&&window.alert("Display JSON (in response.result) is missing the following fields: \n"+"".concat(missing.join()," \n")+"The question author must fix this!"),result}displayNoOutput(response){const isNoOutput=!response||0===combinedOutput(response).length;if(isNoOutput||null===response){const span=document.createElement("span");span.style.color="red",setLangString("nooutput",span),this.clearDisplay(),this.textDisplay.append(span)}return isNoOutput}display(response){const error=(response=>{const ERROR_RESPONSES=[[1,0,"error_access_denied"],[2,0,"error_unknown_language"],[3,0,"error_access_denied"],[4,0,"error_submission_limit_reached"],[5,0,"error_sandbox_server_overload"],[0,11,""],[0,12,""],[0,13,"error_timeout"],[0,15,""],[0,17,"error_memory_limit"],[0,21,"error_sandbox_server_overload"],[0,30,"error_excessive_output"]];for(let i=0;i2&&void 0!==arguments[2]&&arguments[2];this.prevRunSettings=[code,stdin],shouldClearDisplay&&this.clearDisplay(),_ajax.default.call([{methodname:"qtype_coderunner_run_in_sandbox",args:{contextid:M.cfg.contextid,sourcecode:code,language:this.lang,stdin:stdin,params:JSON.stringify(this.sandboxParams)},done:responseJson=>{const response=JSON.parse(responseJson);this.display(response)},fail:error=>{alert(error.message)}}])}addInput(){const inputId="".concat(this.displayAreaId,"-input-field");this.textDisplay.innerHTML+='');const inputEl=document.getElementById(inputId);setLangString("enter_to_submit",inputEl,((node,msg)=>{node.placeholder+=msg})),this.addInputEvents(inputEl)}addInputEvents(inputEl){inputEl.focus(),inputEl.addEventListener("keydown",(e=>{13===e.keyCode&&e.preventDefault()})),inputEl.addEventListener("keyup",(e=>{if(13===e.keyCode){const line=inputEl.value;inputEl.remove(),this.textDisplay.innterHTML+=line,this.prevRunSettings[1]+=line+"\n",this.runCode(...this.prevRunSettings,!1)}}))}displayImages(files){let numImages=0;for(const[fname,fcontents]of Object.entries(files)){const fileType=fname.split(".")[1];if(fileType){const image=getImage(fcontents,fileType);this.imageDisplay.append(image),numImages+=1}else window.alert('Could not read filename correctly: "'.concat(fname,'"'))}return numImages}}}));
//# sourceMappingURL=outputdisplayarea.min.js.map
\ No newline at end of file
diff --git a/amd/build/outputdisplayarea.min.js.map b/amd/build/outputdisplayarea.min.js.map
index 470205f68..97c596809 100644
--- a/amd/build/outputdisplayarea.min.js.map
+++ b/amd/build/outputdisplayarea.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"outputdisplayarea.min.js","sources":["../src/outputdisplayarea.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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/**\n * A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally\n * developed for use in the Scratchpad UI. It has three modes of operation:\n * - 'text': Just display the output as text, html escaped.\n * - 'json': The recommended way to display programs that use stdin or output images (or both).\n * - Accepts JSON in the CRWS response output with fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * - When input from stdin is required the returncode 42 should be returned, raise this\n * any time the program asks for input. An (html) input will be added after the last stdout received.\n * When enter is pressed, runCode is called with value of the input added to the stdin string.\n * This repeats until returncode is no longer 42.\n * - 'html': Display program output as raw html inside the output area.\n * - This can be used to show images and insert other HTML tags (and beyond).\n * - Giving an tag the class 'coderunner-run-input' will add an event that\n * on pressing enter will call the runCode method again with the value of that input field added to stdin.\n * This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.\n *\n * @module qtype_coderunner/outputdisplayarea\n * @copyright James Napier, 2023, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\nimport {get_string as getLangString} from 'core/str';\n\n\nconst ENTER_KEY = 13;\nconst INPUT_INTERRUPT = 42;\nconst RESULT_SUCCESS = 15;\nconst INPUT_CLASS = 'coderunner-run-input';\nconst JSON_DISPLAY_PROPS = ['returncode', 'stdout', 'stderr', 'files'];\n\n\n/**\n * Get the specified language string using\n * AJAX and plug it into the given textarea\n * @param {string} langStringName The language string name.\n * @param {DOMnode} display area into which the error message\n * should be plugged.\n * @param {string} additionalText Extra text to follow the result code.\n */\nconst setLangString = async(langStringName, display, additionalText) => {\n const message = await getLangString(langStringName, 'qtype_coderunner');\n display.innerText = \"*** \" + message + \" ***\\n\";\n if (additionalText) {\n display.innerText += additionalText;\n }\n};\n\nconst diagnoseWebserviceResponse = (response) => {\n // Table of error conditions.\n // Each row is response.error, response.result, langstring\n // response.result is ignored if response.error is non-zero.\n // Any condition not in the table is deemed an \"Unknown runtime error\".\n const ERROR_RESPONSES = [\n [1, 0, 'error_access_denied'], // Sandbox AUTH_ERROR\n [2, 0, 'error_unknown_language'], // Sandbox WRONG_LANG_ID\n [3, 0, 'error_access_denied'], // Sandbox ACCESS_DENIED\n [4, 0, 'error_submission_limit_reached'], // Sandbox SUBMISSION_LIMIT_EXCEEDED\n [5, 0, 'error_sandbox_server_overload'], // Sandbox SERVER_OVERLOAD\n [0, 11, ''], // RESULT_COMPILATION_ERROR\n [0, 12, ''], // RESULT_RUNTIME_ERROR\n [0, 13, 'error_timeout'], // RESULT TIME_LIMIT\n [0, RESULT_SUCCESS, ''], // RESULT_SUCCESS\n [0, 17, 'error_memory_limit'], // RESULT_MEMORY_LIMIT\n [0, 21, 'error_sandbox_server_overload'], // RESULT_SERVER_OVERLOAD\n [0, 30, 'error_excessive_output'] // RESULT OUTPUT_LIMIT\n ];\n for (let i = 0; i < ERROR_RESPONSES.length; i++) {\n let row = ERROR_RESPONSES[i];\n if (row[0] == response.error && (response.error != 0 || response.result == row[1])) {\n return row[2];\n }\n }\n return 'error_unknown_runtime'; // We're dead, Fred.\n};\n\n/**\n * Concatenates the cmpinfo, stdout and stderr fields of the sandbox\n * response, truncating both stdout and stderr to a given maximum length\n * if necessary (in which case '... (truncated)' is appended.\n * @param {object} response Sandbox response object\n */\nconst combinedOutput = (response) => {\n return response.cmpinfo + response.output + response.stderr;\n};\n\n/**\n * Check whether obj has the properties in props, returns missing properties.\n * @param {object} obj to check properties of\n * @param {array} props to check for.\n * @returns {array} of missing properties.\n */\nconst missingProperties = (obj, props) => {\n return props.filter(prop => !obj.hasOwnProperty(prop));\n};\n\n/**\n * Insert a base64 encoded string into HTML image.\n * @param {string} base64 encoded string.\n * @param {string} type of encoded image file.\n * @returns {*|jQuery|HTMLElement} image tag containing encoded image from string.\n */\nconst getImage = (base64, type = 'png') => {\n const image = document.createElement('img');\n image.src = `data:image/${type};base64,${base64}`;\n return image;\n};\n\n/**\n * Constructor for OutputDisplayArea object. For use with the output_displayarea template.\n * @param {string} displayAreaId The id of the display area div, this should match the 'id'\n * from the template.\n * @param {string} outputMode The mode being used for output, must be text, html or json.\n * @param {string} lang The language to run code with.\n * @param {string} sandboxParams The sandbox params to run code with.\n */\nclass OutputDisplayArea {\n constructor(displayAreaId, outputMode, lang, sandboxParams) {\n this.displayAreaId = displayAreaId;\n this.lang = lang;\n this.mode = outputMode;\n this.sandboxParams = sandboxParams;\n\n this.textDisplay = document.getElementById(displayAreaId + '-text');\n this.imageDisplay = document.getElementById(displayAreaId + '-images');\n\n this.prevRunSettings = null;\n }\n\n /**\n * Clear the display of any images and text.\n */\n clearDisplay() {\n this.textDisplay.innerHTML = \"\";\n this.imageDisplay.innerHTML = \"\";\n }\n\n /**\n * Display text from a CRWS response to the display (escaped).\n * @param {object} response Coderunner webservice response JSON.\n */\n displayText(response) {\n this.textDisplay.innerText = combinedOutput(response);\n }\n\n /**\n * Display HTML from a CRWS response to the display (un-escaped).\n * Find the first HTML input element with the input class and\n * add event listeners to handle reading stdin.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing HTML.\n */\n displayHtml(response) {\n this.textDisplay.innerHTML = combinedOutput(response);\n const inputEl = this.textDisplay.querySelector('.' + INPUT_CLASS);\n if (inputEl) {\n this.addInputEvents(inputEl);\n }\n }\n\n /**\n * Display JSON from a CRWS response to the display.\n * Assumes response.output will be a JSON with the fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * NOTE: See file header/readme for more info.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing JSON string.\n */\n displayJson(response) {\n const result = this.validateJson(response.output);\n let text = result.stdout;\n\n if (result.returncode !== INPUT_INTERRUPT) {\n text += result.stderr;\n }\n if (result.returncode == 13) { // Timeout\n text += \"\\n*** Timeout error ***\\n\";\n } // TODO: Langstring\n\n const numImages = this.displayImages(result.files);\n\n if (text.trim() === '' && result.returncode !== INPUT_INTERRUPT) {\n if (numImages == 0) {\n this.displayNoOutput(null);\n }\n } else {\n this.textDisplay.innerText = text;\n }\n\n if (result.returncode === INPUT_INTERRUPT) {\n this.addInput();\n }\n }\n\n /**\n * Validate JSON to display, make sure it is valid json and has required fields.\n * @param {string} jsonString string of JSON to be displayed.\n * @returns {object} JSON as object\n */\n validateJson(jsonString) {\n let result = null;\n try {\n result = JSON.parse(jsonString);\n } catch (e) {\n window.alert(\n `Error parsing display JSON output: \n ${jsonString}\n JSON Parsing error Msg: \n ${e.message}\n The question author must fix this!`\n );\n }\n\n const missing = missingProperties(result, JSON_DISPLAY_PROPS);\n if (missing.length > 0) {\n window.alert(\n `Display JSON (in response.result) is missing the following fields:\n ${missing.join()}\n The question author must fix this!`\n );\n }\n return result;\n }\n\n /**\n * Display no output message if no output to display or response is null.\n * @param {object} response Coderunner webservice response JSON, set to null to force\n * display of no output message.\n */\n displayNoOutput(response) {\n const isNoOutput = response ? combinedOutput(response).length === 0 : true;\n if (isNoOutput || response === null) {\n const span = document.createElement('span');\n span.style.color = 'red';\n span.innerText = '< No output! >';\n this.clearDisplay();\n this.textDisplay.append(span);\n }\n return isNoOutput;\n }\n /**\n * Display response using the current display mode.\n * @param {object} response Coderunner webservice response JSON.\n */\n display(response) {\n const error = diagnoseWebserviceResponse(response);\n if (error !== '') {\n setLangString(error, this.textDisplay);\n return;\n }\n if (this.displayNoOutput(response)) {\n return;\n }\n\n if (this.mode === 'json') {\n this.displayJson(response);\n } else if (this.mode === 'html') {\n this.displayHtml(response);\n } else if (this.mode === 'text') {\n this.displayText(response);\n } else {\n throw Error(`Invalid outputMode given: \"${this.mode}\"`);\n }\n }\n\n /**\n * Run code using the Coderunner webservice and then display the output\n * using the selected mode. This function uses AJAX to asynchronously run and\n * display code.\n * @param {string} code to be run.\n * @param {string} stdin to be fed into the program.\n * @param {boolean} shouldClearDisplay will reset the display before displaying.\n * Use false when doing stdin runs.\n */\n runCode(code, stdin, shouldClearDisplay = false) {\n this.prevRunSettings = [code, stdin];\n if (shouldClearDisplay) {\n this.clearDisplay();\n }\n ajax.call([{\n methodname: 'qtype_coderunner_run_in_sandbox',\n args: {\n contextid: M.cfg.contextid, // Moodle context ID\n sourcecode: code,\n language: this.lang,\n stdin: stdin,\n params: JSON.stringify(this.sandboxParams) // Sandbox params\n },\n done: (responseJson) => {\n const response = JSON.parse(responseJson);\n this.display(response);\n },\n fail: (error) => {\n alert(error.message);\n }\n }]);\n }\n\n /**\n * Add an input field with event listeners to support running again\n * with new stdin entered by user.\n */\n addInput() {\n const inputId = `${this.displayAreaId}-input-field`;\n this.textDisplay.innerHTML += ``;\n const inputEl = document.getElementById(inputId);\n this.addInputEvents(inputEl);\n }\n\n /**\n * Add event listeners to inputEl overriding enter key to:\n * - Prevent form-submit.\n * - Call runCode again, adding value in inputEl to stdin.\n * @param {node} inputEl to add event listeners to.\n */\n addInputEvents(inputEl) {\n inputEl.focus();\n\n inputEl.addEventListener('keydown', (e) => {\n if (e.keyCode === ENTER_KEY) {\n e.preventDefault(); // Do NOT form submit.\n }\n });\n inputEl.addEventListener('keyup', (e) => {\n if (e.keyCode === ENTER_KEY) {\n const line = inputEl.value;\n inputEl.remove();\n this.textDisplay.innterHTML += line; // Perhaps this should be sanitized.\n this.prevRunSettings[1] += line + '\\n';\n this.runCode(...this.prevRunSettings, false);\n }\n });\n }\n\n /**\n * Take the files from a JSON response and display them.\n * @param {object} files from response, in filename: filecontents pairs.\n * @returns {number} number of images displayed.\n */\n displayImages(files) {\n let numImages = 0;\n for (const [fname, fcontents] of Object.entries(files)) {\n const fileType = fname.split('.')[1];\n if (fileType) {\n const image = getImage(fcontents, fileType);\n this.imageDisplay.append(image);\n numImages += 1;\n } else {\n window.alert(`Could not read filename correctly: ${fname}`);\n }\n }\n return numImages;\n }\n}\n\n\nexport {\n OutputDisplayArea\n};\n"],"names":["JSON_DISPLAY_PROPS","setLangString","langStringName","display","additionalText","message","innerText","combinedOutput","response","cmpinfo","output","stderr","getImage","base64","type","image","document","createElement","src","OutputDisplayArea","displayAreaId","outputMode","lang","sandboxParams","mode","textDisplay","getElementById","imageDisplay","prevRunSettings","innerHTML","inputEl","this","querySelector","addInputEvents","result","validateJson","text","stdout","returncode","numImages","displayImages","files","trim","displayNoOutput","addInput","jsonString","JSON","parse","e","window","alert","missing","obj","props","filter","prop","hasOwnProperty","missingProperties","length","join","isNoOutput","span","style","color","clearDisplay","append","error","ERROR_RESPONSES","i","row","diagnoseWebserviceResponse","displayJson","displayHtml","Error","displayText","code","stdin","shouldClearDisplay","call","methodname","args","contextid","M","cfg","sourcecode","language","params","stringify","done","responseJson","_this","fail","inputId","focus","addEventListener","keyCode","preventDefault","line","value","remove","_this2","innterHTML","runCode","Object","entries","fname","fcontents","fileType","split"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"outputdisplayarea.min.js","sources":["../src/outputdisplayarea.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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/**\n * A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally\n * developed for use in the Scratchpad UI. It has three modes of operation:\n * - 'text': Just display the output as text, html escaped.\n * - 'json': The recommended way to display programs that use stdin or output images (or both).\n * - Accepts JSON in the CRWS response output with fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * - When input from stdin is required the returncode 42 should be returned, raise this\n * any time the program asks for input. An (html) input will be added after the last stdout received.\n * When enter is pressed, runCode is called with value of the input added to the stdin string.\n * This repeats until returncode is no longer 42.\n * - 'html': Display program output as raw html inside the output area.\n * - This can be used to show images and insert other HTML tags (and beyond).\n * - Giving an tag the class 'coderunner-run-input' will add an event that\n * on pressing enter will call the runCode method again with the value of that input field added to stdin.\n * This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.\n *\n * @module qtype_coderunner/outputdisplayarea\n * @copyright James Napier, 2023, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\nimport {get_string as getLangString} from 'core/str';\n\n\nconst ENTER_KEY = 13;\nconst INPUT_INTERRUPT = 42;\nconst RESULT_SUCCESS = 15;\nconst INPUT_CLASS = 'coderunner-run-input';\nconst JSON_DISPLAY_PROPS = ['returncode', 'stdout', 'stderr', 'files'];\n\n\n/**\n * Get the specified language string using\n * AJAX and plug it into the given textarea\n * @param {string} langStringName The language string name.\n * @param {DOMnode} node area into which the error message\n * should be plugged.\n * @param {function} callback Callback function, with two arguments: node, message.\n * @param {string} additionalText Extra text to follow the result code.\n */\nconst setLangString = async(langStringName, node, callback, additionalText = '') => {\n let message = await getLangString(langStringName, 'qtype_coderunner');\n if (langStringName.includes('error')) {\n message = \"*** \" + message + \" ***\\n\";\n }\n if (additionalText) {\n message += additionalText;\n }\n if (callback instanceof Function) {\n callback(node, message);\n } else {\n node.innerText = message;\n }\n};\n\nconst diagnoseWebserviceResponse = (response) => {\n // Table of error conditions.\n // Each row is response.error, response.result, langstring\n // response.result is ignored if response.error is non-zero.\n // Any condition not in the table is deemed an \"Unknown runtime error\".\n const ERROR_RESPONSES = [\n [1, 0, 'error_access_denied'], // Sandbox AUTH_ERROR\n [2, 0, 'error_unknown_language'], // Sandbox WRONG_LANG_ID\n [3, 0, 'error_access_denied'], // Sandbox ACCESS_DENIED\n [4, 0, 'error_submission_limit_reached'], // Sandbox SUBMISSION_LIMIT_EXCEEDED\n [5, 0, 'error_sandbox_server_overload'], // Sandbox SERVER_OVERLOAD\n [0, 11, ''], // RESULT_COMPILATION_ERROR\n [0, 12, ''], // RESULT_RUNTIME_ERROR\n [0, 13, 'error_timeout'], // RESULT TIME_LIMIT\n [0, RESULT_SUCCESS, ''], // RESULT_SUCCESS\n [0, 17, 'error_memory_limit'], // RESULT_MEMORY_LIMIT\n [0, 21, 'error_sandbox_server_overload'], // RESULT_SERVER_OVERLOAD\n [0, 30, 'error_excessive_output'] // RESULT OUTPUT_LIMIT\n ];\n for (let i = 0; i < ERROR_RESPONSES.length; i++) {\n let row = ERROR_RESPONSES[i];\n if (row[0] == response.error && (response.error != 0 || response.result == row[1])) {\n return row[2];\n }\n }\n return 'error_unknown_runtime'; // We're dead, Fred.\n};\n\n/**\n * Concatenates the cmpinfo, stdout and stderr fields of the sandbox\n * response, truncating both stdout and stderr to a given maximum length\n * if necessary (in which case '... (truncated)' is appended.\n * @param {object} response Sandbox response object\n */\nconst combinedOutput = (response) => {\n return response.cmpinfo + response.output + response.stderr;\n};\n\n/**\n * Check whether obj has the properties in props, returns missing properties.\n * @param {object} obj to check properties of\n * @param {array} props to check for.\n * @returns {array} of missing properties.\n */\nconst missingProperties = (obj, props) => {\n return props.filter(prop => !obj.hasOwnProperty(prop));\n};\n\n/**\n * Insert a base64 encoded string into HTML image.\n * @param {string} base64 encoded string.\n * @param {string} type of encoded image file.\n * @returns {*|jQuery|HTMLElement} image tag containing encoded image from string.\n */\nconst getImage = (base64, type = 'png') => {\n const image = document.createElement('img');\n image.src = `data:image/${type};base64,${base64}`;\n return image;\n};\n\n/**\n * Constructor for OutputDisplayArea object. For use with the output_displayarea template.\n * @param {string} displayAreaId The id of the display area div, this should match the 'id'\n * from the template.\n * @param {string} outputMode The mode being used for output, must be text, html or json.\n * @param {string} lang The language to run code with.\n * @param {string} sandboxParams The sandbox params to run code with.\n */\nclass OutputDisplayArea {\n constructor(displayAreaId, outputMode, lang, sandboxParams) {\n this.displayAreaId = displayAreaId;\n this.lang = lang;\n this.mode = outputMode;\n this.sandboxParams = sandboxParams;\n\n this.textDisplay = document.getElementById(displayAreaId + '-text');\n this.imageDisplay = document.getElementById(displayAreaId + '-images');\n\n this.prevRunSettings = null;\n }\n\n /**\n * Clear the display of any images and text.\n */\n clearDisplay() {\n this.textDisplay.innerHTML = \"\";\n this.imageDisplay.innerHTML = \"\";\n }\n\n /**\n * Display text from a CRWS response to the display (escaped).\n * @param {object} response Coderunner webservice response JSON.\n */\n displayText(response) {\n this.textDisplay.innerText = combinedOutput(response);\n }\n\n /**\n * Display HTML from a CRWS response to the display (un-escaped).\n * Find the first HTML input element with the input class and\n * add event listeners to handle reading stdin.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing HTML.\n */\n displayHtml(response) {\n this.textDisplay.innerHTML = combinedOutput(response);\n const inputEl = this.textDisplay.querySelector('.' + INPUT_CLASS);\n if (inputEl) {\n this.addInputEvents(inputEl);\n }\n }\n\n /**\n * Display JSON from a CRWS response to the display.\n * Assumes response.output will be a JSON with the fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * NOTE: See file header/readme for more info.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing JSON string.\n */\n displayJson(response) {\n const result = this.validateJson(response.output);\n\n let text = result.stdout;\n\n if (result.returncode !== INPUT_INTERRUPT) {\n text += result.stderr;\n }\n if (result.returncode == 13) { // Timeout\n setLangString('error_timeout', this.textDisplay, (node, msg) => {\n node.innerText += msg;\n });\n }\n\n const numImages = this.displayImages(result.files);\n if (text.trim() === '' && result.returncode !== INPUT_INTERRUPT) {\n if (numImages == 0) {\n this.displayNoOutput(null);\n }\n } else {\n this.textDisplay.innerText = text;\n }\n if (result.returncode === INPUT_INTERRUPT) {\n this.addInput();\n }\n }\n\n /**\n * Validate JSON to display, make sure it is valid json and has required fields.\n * @param {string} jsonString string of JSON to be displayed.\n * @returns {object} JSON as object\n */\n validateJson(jsonString) {\n let result = null;\n try {\n result = JSON.parse(jsonString);\n } catch (e) {\n window.alert(\n `Error parsing display JSON output: \\n` +\n `'${jsonString}\\n'` +\n `Error Msg: \\n` +\n ` ${e.message} \\n` +\n `The question author must fix this!`\n );\n }\n\n const missing = missingProperties(result, JSON_DISPLAY_PROPS);\n if (missing.length > 0) {\n window.alert(\n `Display JSON (in response.result) is missing the following fields: \\n` +\n `${missing.join()} \\n` +\n `The question author must fix this!`\n );\n }\n return result;\n }\n\n /**\n * Display no output message if no output to display or response is null.\n * @param {object} response Coderunner webservice response JSON, set to null to force\n * display of no output message.\n */\n displayNoOutput(response) {\n const isNoOutput = response ? combinedOutput(response).length === 0 : true;\n if (isNoOutput || response === null) {\n const span = document.createElement('span');\n span.style.color = 'red';\n setLangString('nooutput', span);\n this.clearDisplay();\n this.textDisplay.append(span);\n }\n return isNoOutput;\n }\n /**\n * Display response using the current display mode.\n * @param {object} response Coderunner webservice response JSON.\n */\n display(response) {\n const error = diagnoseWebserviceResponse(response);\n if (error !== '') {\n setLangString(error, this.textDisplay);\n return;\n }\n if (this.displayNoOutput(response)) {\n return;\n }\n\n if (this.mode === 'json') {\n this.displayJson(response);\n } else if (this.mode === 'html') {\n this.displayHtml(response);\n } else if (this.mode === 'text') {\n this.displayText(response);\n } else {\n throw Error(`Invalid outputMode given: \"${this.mode}\"`);\n }\n }\n\n /**\n * Run code using the Coderunner webservice and then display the output\n * using the selected mode. This function uses AJAX to asynchronously run and\n * display code.\n * @param {string} code to be run.\n * @param {string} stdin to be fed into the program.\n * @param {boolean} shouldClearDisplay will reset the display before displaying.\n * Use false when doing stdin runs.\n */\n runCode(code, stdin, shouldClearDisplay = false) {\n this.prevRunSettings = [code, stdin];\n if (shouldClearDisplay) {\n this.clearDisplay();\n }\n ajax.call([{\n methodname: 'qtype_coderunner_run_in_sandbox',\n args: {\n contextid: M.cfg.contextid, // Moodle context ID\n sourcecode: code,\n language: this.lang,\n stdin: stdin,\n params: JSON.stringify(this.sandboxParams) // Sandbox params\n },\n done: (responseJson) => {\n const response = JSON.parse(responseJson);\n this.display(response);\n },\n fail: (error) => {\n alert(error.message);\n }\n }]);\n }\n\n /**\n * Add an input field with event listeners to support running again\n * with new stdin entered by user.\n */\n addInput() {\n const inputId = `${this.displayAreaId}-input-field`;\n this.textDisplay.innerHTML += ``;\n const inputEl = document.getElementById(inputId);\n setLangString('enter_to_submit', inputEl, (node, msg) => {\n node.placeholder += msg;\n });\n this.addInputEvents(inputEl);\n }\n\n /**\n * Add event listeners to inputEl overriding enter key to:\n * - Prevent form-submit.\n * - Call runCode again, adding value in inputEl to stdin.\n * @param {node} inputEl to add event listeners to.\n */\n addInputEvents(inputEl) {\n inputEl.focus();\n\n inputEl.addEventListener('keydown', (e) => {\n if (e.keyCode === ENTER_KEY) {\n e.preventDefault(); // Do NOT form submit.\n }\n });\n inputEl.addEventListener('keyup', (e) => {\n if (e.keyCode === ENTER_KEY) {\n const line = inputEl.value;\n inputEl.remove();\n this.textDisplay.innterHTML += line; // Perhaps this should be sanitized.\n this.prevRunSettings[1] += line + '\\n';\n this.runCode(...this.prevRunSettings, false);\n }\n });\n }\n\n /**\n * Take the files from a JSON response and display them.\n * @param {object} files from response, in filename: filecontents pairs.\n * @returns {number} number of images displayed.\n */\n displayImages(files) {\n let numImages = 0;\n for (const [fname, fcontents] of Object.entries(files)) {\n const fileType = fname.split('.')[1];\n if (fileType) {\n const image = getImage(fcontents, fileType);\n this.imageDisplay.append(image);\n numImages += 1;\n } else {\n window.alert(`Could not read filename correctly: \"${fname}\"`);\n }\n }\n return numImages;\n }\n}\n\n\nexport {\n OutputDisplayArea\n};\n"],"names":["JSON_DISPLAY_PROPS","setLangString","async","langStringName","node","callback","additionalText","message","includes","Function","innerText","combinedOutput","response","cmpinfo","output","stderr","getImage","base64","type","image","document","createElement","src","constructor","displayAreaId","outputMode","lang","sandboxParams","mode","textDisplay","getElementById","imageDisplay","prevRunSettings","clearDisplay","innerHTML","displayText","displayHtml","inputEl","this","querySelector","addInputEvents","displayJson","result","validateJson","text","stdout","returncode","msg","numImages","displayImages","files","trim","displayNoOutput","addInput","jsonString","JSON","parse","e","window","alert","missing","obj","props","filter","prop","hasOwnProperty","missingProperties","length","join","isNoOutput","span","style","color","append","display","error","ERROR_RESPONSES","i","row","diagnoseWebserviceResponse","Error","runCode","code","stdin","shouldClearDisplay","call","methodname","args","contextid","M","cfg","sourcecode","language","params","stringify","done","responseJson","fail","inputId","placeholder","focus","addEventListener","keyCode","preventDefault","line","value","remove","innterHTML","fname","fcontents","Object","entries","fileType","split"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/textareas.min.js b/amd/build/textareas.min.js
index 3826a6463..f7c2df89a 100644
--- a/amd/build/textareas.min.js
+++ b/amd/build/textareas.min.js
@@ -6,6 +6,6 @@
* @copyright Richard Lobb, 2015, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/textareas",["jquery"],(function($){function initTextArea(){$(this).data("clickInProgress",!1),$(this).data("capturingTab",!0),$(this).on("mousedown",(function(){$(this).data("clickInProgress",!0)})),$(this).on("focusin",(function(){$(this).data("capturingTab",$(this).data("clickInProgress"))})),$(this).on("click",(function(){$(this).data("clickInProgress",!1)})),$(this).on("keydown",(function(e){if(!(window.hasOwnProperty("behattesting")&&window.behattesting||void 0!==e.which&&0===e.which))if(9==e.keyCode&&$(this).data("capturingTab"))(e.shiftKey||insertString(this," "))&&e.preventDefault();else if(13===e.keyCode&&void 0!==this.selectionStart){for(var before=this.value.substring(0,this.selectionStart),eol=before.lastIndexOf("\n"),line=before.substring(eol+1),indent="",i=0;i{div.is(":visible")?(div.hide(),hdr.innerText=hdrText.replace("▾","▸")):(div.show(),div.trigger("mousemove"),hdr.innerText=hdrText.replace("▸","▾"))}))}}}));
//# sourceMappingURL=textareas.min.js.map
\ No newline at end of file
diff --git a/amd/build/textareas.min.js.map b/amd/build/textareas.min.js.map
index a8cbe49a4..617c95e64 100644
--- a/amd/build/textareas.min.js.map
+++ b/amd/build/textareas.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"textareas.min.js","sources":["../src/textareas.js"],"sourcesContent":["/**\n * 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/**\n * JavaScript for handling textareas and form actions in CodeRunner question\n * editing forms and student question answering forms.\n *\n * @module qtype_coderunner/textareas\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Function to initialise all code-input text-areas in a page.\n * Used by the form editor but can't be used for question text areas as\n * renderer.php is called once for each question in a quiz, and there is\n * no communication between the questions.\n */\n function setupAllTAs() {\n $('textarea.edit_code').each(initTextArea);\n }\n\n /**\n * Initialise a particular text area (TA), given its ID (which may contain\n * colons, so can't use jQuery selector).\n * @param {string} taId The ID of the textarea html element.\n */\n function initQuestionTA(taId) {\n $(document.getElementById(taId)).each(initTextArea);\n }\n\n\n /**\n * Set up to expand or collapse the question author's answer when the user clicks\n * on the show/hide answer button.\n * @param {string} linkId The ID of the link that the user clicks.\n * @param {sting} divId The ID of the div to be shown or hidden.\n */\n function setupShowHideAnswer(linkId, divId) {\n let link = $(document.getElementById(linkId)),\n div = $(document.getElementById(divId)),\n hdr = link.children()[0], // The
element.\n hdrText = hdr.innerText; // Its body.\n link.click(() => {\n if (div.is(\":visible\")) {\n div.hide();\n hdr.innerText = hdrText.replace(\"\\u25BE\", \"\\u25B8\");\n } else {\n div.show();\n div.trigger('mousemove'); // So user sees contents.\n hdr.innerText = hdrText.replace(\"\\u25B8\", \"\\u25BE\");\n }\n });\n }\n\n /**\n * Set up the JavaScript to handle a text area 'this'.\n * It just does rudimentary autoindent on return and replaces tabs with\n * 4 spaces always.\n * For info on key handling browser inconsistencies see\n * http://unixpapa.com/js/key.html\n * If the AceEditor is handling a text area, this code is unused as\n * the actual textarea is hidden by Ace.\n */\n function initTextArea() {\n var TAB = 9,\n ENTER = 13,\n ESC = 27,\n KEY_M = 77;\n\n $(this).data('clickInProgress', false);\n $(this).data('capturingTab', true);\n\n $(this).on('mousedown', function() {\n /*\n * Event order seems to be (\\ is where the mouse button is pressed, / released):\n * Chrome: \\ mousedown, mouseup, focusin / click.\n * Firefox/IE: \\ mousedown, focusin / mouseup, click.\n */\n $(this).data('clickInProgress', true);\n });\n\n $(this).on('focusin', function() {\n /*\n * At first, pressing TAB moves focus.\n */\n $(this).data('capturingTab', $(this).data('clickInProgress'));\n });\n\n $(this).on('click', function() {\n $(this).data('clickInProgress', false);\n });\n\n $(this).on('keydown', function(e) {\n /*\n * Don't autoindent when behat testing in progress.\n */\n if (window.hasOwnProperty('behattesting') && window.behattesting) { return; }\n\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode == TAB && $(this).data('capturingTab')) {\n /*\n * Ignore SHIFT/TAB. Insert 4 spaces on TAB.\n */\n if (e.shiftKey || insertString(this, \" \")) {\n e.preventDefault();\n }\n }\n else if (e.keyCode === ENTER && this.selectionStart !== undefined) {\n /*\n * Handle autoindent only on non-IE.\n */\n var before = this.value.substring(0, this.selectionStart);\n var eol = before.lastIndexOf(\"\\n\");\n var line = before.substring(eol + 1); // Take from eol to end.\n var indent = \"\";\n for (var i = 0; i < line.length && line.charAt(i) === ' '; i++) {\n indent = indent + \" \";\n }\n if (insertString(this, \"\\n\" + indent)) {\n e.preventDefault();\n }\n /*\n * Once the user has started typing, TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n else if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n /*\n * CTRL + M toggles TAB capturing mode.\n * This is the short-cut recommended by\n * https:www.w3.org/TR/wai-aria-practices/#richtext.\n */\n $(this).data('capturingTab', !$(this).data('capturingTab'));\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n /*\n * ESC always stops capturing TAB.\n */\n $(this).data('capturingTab', false);\n }\n else if (!(e.ctrlKey || e.altKey)) {\n /*\n * Once the user has started typing (not modifier keys) TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n }\n });\n }\n\n /**\n * Insert into the given textarea ta the given string sToInsert.\n * @param {html_element} ta The textarea to be updated.\n * @param {string} sToInsert The string to be inserted at the current selection\n * point.\n */\n function insertString(ta, sToInsert) {\n if (ta.selectionStart !== undefined) { // Firefox etc.\n var before = ta.value.substring(0, ta.selectionStart);\n var selSave = ta.selectionEnd;\n var after = ta.value.substring(ta.selectionEnd, ta.value.length);\n\n /**\n * Update the text field.\n */\n var tmp = ta.scrollTop; // Inhibit annoying auto-scroll.\n ta.value = before + sToInsert + after;\n var pos = selSave + sToInsert.length;\n ta.selectionStart = pos;\n ta.selectionEnd = pos;\n ta.scrollTop = tmp;\n return true;\n\n }\n else if (document.selection && document.selection.createRange) { // IE.\n /*\n * TODO: check if this still works. OLD CODE.\n */\n var r = document.selection.createRange();\n var dr = r.duplicate();\n dr.moveToElementText(ta);\n dr.setEndPoint(\"EndToEnd\", r);\n r.text = sToInsert;\n return true;\n }\n /*\n * Other browsers we can't handle.\n */\n else {\n return false;\n }\n }\n\n return {\n setupAllTAs: setupAllTAs,\n initQuestionTA: initQuestionTA,\n setupShowHideAnswer: setupShowHideAnswer,\n };\n});\n"],"names":["define","$","initTextArea","this","data","on","e","window","hasOwnProperty","behattesting","undefined","which","keyCode","shiftKey","insertString","preventDefault","selectionStart","before","value","substring","eol","lastIndexOf","line","indent","i","length","charAt","ctrlKey","altKey","ta","sToInsert","selSave","selectionEnd","after","tmp","scrollTop","pos","document","selection","createRange","r","dr","duplicate","moveToElementText","setEndPoint","text","setupAllTAs","each","initQuestionTA","taId","getElementById","setupShowHideAnswer","linkId","divId","link","div","hdr","children","hdrText","innerText","click","is","hide","replace","show","trigger"],"mappings":";;;;;;;;AA2BAA,oCAAO,CAAC,WAAW,SAASC,YAsDfC,eAMLD,EAAEE,MAAMC,KAAK,mBAAmB,GAChCH,EAAEE,MAAMC,KAAK,gBAAgB,GAE7BH,EAAEE,MAAME,GAAG,aAAa,WAMpBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,WAIlBJ,EAAEE,MAAMC,KAAK,eAAgBH,EAAEE,MAAMC,KAAK,uBAG9CH,EAAEE,MAAME,GAAG,SAAS,WAChBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,SAASC,QAIvBC,OAAOC,eAAe,iBAAmBD,OAAOE,mBAEpCC,IAAZJ,EAAEK,OAAmC,IAAZL,EAAEK,UAlCzB,GAmCEL,EAAEM,SAAkBX,EAAEE,MAAMC,KAAK,iBAI7BE,EAAEO,UAAYC,aAAaX,KAAM,UACjCG,EAAES,sBAGL,GA1CD,KA0CKT,EAAEM,cAA6CF,IAAxBP,KAAKa,eAA8B,SAI3DC,OAASd,KAAKe,MAAMC,UAAU,EAAGhB,KAAKa,gBACtCI,IAAMH,OAAOI,YAAY,MACzBC,KAAOL,OAAOE,UAAUC,IAAM,GAC9BG,OAAS,GACJC,EAAI,EAAGA,EAAIF,KAAKG,QAA6B,MAAnBH,KAAKI,OAAOF,GAAYA,IACvDD,QAAkB,IAElBT,aAAaX,KAAM,KAAOoB,SAC1BjB,EAAES,iBAKNd,EAAEE,MAAMC,KAAK,gBAAgB,QAzD7B,KA2DKE,EAAEM,SAAqBN,EAAEqB,UAAYrB,EAAEsB,QAM5C3B,EAAEE,MAAMC,KAAK,gBAAiBH,EAAEE,MAAMC,KAAK,iBAC3CE,EAAES,kBAnEJ,KAqEOT,EAAEM,QAIPX,EAAEE,MAAMC,KAAK,gBAAgB,GAEtBE,EAAEqB,SAAWrB,EAAEsB,QAItB3B,EAAEE,MAAMC,KAAK,gBAAgB,eAYpCU,aAAae,GAAIC,mBACIpB,IAAtBmB,GAAGb,eAA8B,KAC7BC,OAASY,GAAGX,MAAMC,UAAU,EAAGU,GAAGb,gBAClCe,QAAUF,GAAGG,aACbC,MAAQJ,GAAGX,MAAMC,UAAUU,GAAGG,aAAcH,GAAGX,MAAMO,QAKrDS,IAAML,GAAGM,UACbN,GAAGX,MAAQD,OAASa,UAAYG,UAC5BG,IAAML,QAAUD,UAAUL,cAC9BI,GAAGb,eAAiBoB,IACpBP,GAAGG,aAAeI,IAClBP,GAAGM,UAAYD,KACR,EAGN,GAAIG,SAASC,WAAaD,SAASC,UAAUC,YAAa,KAIvDC,EAAIH,SAASC,UAAUC,cACvBE,GAAKD,EAAEE,mBACXD,GAAGE,kBAAkBd,IACrBY,GAAGG,YAAY,WAAYJ,GAC3BA,EAAEK,KAAOf,WACF,SAMA,QAIR,CACHgB,uBAjLA7C,EAAE,sBAAsB8C,KAAK7C,eAkL7B8C,wBA1KoBC,MACpBhD,EAAEoC,SAASa,eAAeD,OAAOF,KAAK7C,eA0KtCiD,6BAhKyBC,OAAQC,WAC7BC,KAAOrD,EAAEoC,SAASa,eAAeE,SACjCG,IAAMtD,EAAEoC,SAASa,eAAeG,QAChCG,IAAMF,KAAKG,WAAW,GACtBC,QAAUF,IAAIG,UAClBL,KAAKM,OAAM,WACHL,IAAIM,GAAG,aACPN,IAAIO,OACJN,IAAIG,UAAYD,QAAQK,QAAQ,IAAU,OAE1CR,IAAIS,OACJT,IAAIU,QAAQ,aACZT,IAAIG,UAAYD,QAAQK,QAAQ,IAAU"}
\ No newline at end of file
+{"version":3,"file":"textareas.min.js","sources":["../src/textareas.js"],"sourcesContent":["/**\n * 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/**\n * JavaScript for handling textareas and form actions in CodeRunner question\n * editing forms and student question answering forms.\n *\n * @module qtype_coderunner/textareas\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Function to initialise all code-input text-areas in a page.\n * Used by the form editor but can't be used for question text areas as\n * renderer.php is called once for each question in a quiz, and there is\n * no communication between the questions.\n */\n function setupAllTAs() {\n $('textarea.edit_code').each(initTextArea);\n }\n\n /**\n * Initialise a particular text area (TA), given its ID (which may contain\n * colons, so can't use jQuery selector).\n * @param {string} taId The ID of the textarea html element.\n */\n function initQuestionTA(taId) {\n $(document.getElementById(taId)).each(initTextArea);\n }\n\n\n /**\n * Set up to expand or collapse the question author's answer when the user clicks\n * on the show/hide answer button.\n * @param {string} linkId The ID of the link that the user clicks.\n * @param {sting} divId The ID of the div to be shown or hidden.\n */\n function setupShowHideAnswer(linkId, divId) {\n let link = $(document.getElementById(linkId)),\n div = $(document.getElementById(divId)),\n hdr = link.children()[0], // The
element.\n hdrText = hdr.innerText; // Its body.\n link.click(() => {\n if (div.is(\":visible\")) {\n div.hide();\n hdr.innerText = hdrText.replace(\"\\u25BE\", \"\\u25B8\");\n } else {\n div.show();\n div.trigger('mousemove'); // So user sees contents.\n hdr.innerText = hdrText.replace(\"\\u25B8\", \"\\u25BE\");\n }\n });\n }\n\n /**\n * Set up the JavaScript to handle a text area 'this'.\n * It just does rudimentary autoindent on return and replaces tabs with\n * 4 spaces always.\n * For info on key handling browser inconsistencies see\n * http://unixpapa.com/js/key.html\n * If the AceEditor is handling a text area, this code is unused as\n * the actual textarea is hidden by Ace.\n */\n function initTextArea() {\n var TAB = 9,\n ENTER = 13,\n ESC = 27,\n KEY_M = 77;\n\n $(this).data('clickInProgress', false);\n $(this).data('capturingTab', true);\n\n $(this).on('mousedown', function() {\n /*\n * Event order seems to be (\\ is where the mouse button is pressed, / released):\n * Chrome: \\ mousedown, mouseup, focusin / click.\n * Firefox/IE: \\ mousedown, focusin / mouseup, click.\n */\n $(this).data('clickInProgress', true);\n });\n\n $(this).on('focusin', function() {\n /*\n * At first, pressing TAB moves focus.\n */\n $(this).data('capturingTab', $(this).data('clickInProgress'));\n });\n\n $(this).on('click', function() {\n $(this).data('clickInProgress', false);\n });\n\n $(this).on('keydown', function(e) {\n /*\n * Don't autoindent when behat testing in progress.\n */\n if (window.hasOwnProperty('behattesting') && window.behattesting) { return; }\n\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode == TAB && $(this).data('capturingTab')) {\n /*\n * Ignore SHIFT/TAB. Insert 4 spaces on TAB.\n */\n if (e.shiftKey || insertString(this, \" \")) {\n e.preventDefault();\n }\n }\n else if (e.keyCode === ENTER && this.selectionStart !== undefined) {\n /*\n * Handle autoindent only on non-IE.\n */\n var before = this.value.substring(0, this.selectionStart);\n var eol = before.lastIndexOf(\"\\n\");\n var line = before.substring(eol + 1); // Take from eol to end.\n var indent = \"\";\n for (var i = 0; i < line.length && line.charAt(i) === ' '; i++) {\n indent = indent + \" \";\n }\n if (insertString(this, \"\\n\" + indent)) {\n e.preventDefault();\n }\n /*\n * Once the user has started typing, TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n else if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n /*\n * CTRL + M toggles TAB capturing mode.\n * This is the short-cut recommended by\n * https:www.w3.org/TR/wai-aria-practices/#richtext.\n */\n $(this).data('capturingTab', !$(this).data('capturingTab'));\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n /*\n * ESC always stops capturing TAB.\n */\n $(this).data('capturingTab', false);\n }\n else if (!(e.ctrlKey || e.altKey)) {\n /*\n * Once the user has started typing (not modifier keys) TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n }\n });\n }\n\n /**\n * Insert into the given textarea ta the given string sToInsert.\n * @param {html_element} ta The textarea to be updated.\n * @param {string} sToInsert The string to be inserted at the current selection\n * point.\n */\n function insertString(ta, sToInsert) {\n if (ta.selectionStart !== undefined) { // Firefox etc.\n var before = ta.value.substring(0, ta.selectionStart);\n var selSave = ta.selectionEnd;\n var after = ta.value.substring(ta.selectionEnd, ta.value.length);\n\n /**\n * Update the text field.\n */\n var tmp = ta.scrollTop; // Inhibit annoying auto-scroll.\n ta.value = before + sToInsert + after;\n var pos = selSave + sToInsert.length;\n ta.selectionStart = pos;\n ta.selectionEnd = pos;\n ta.scrollTop = tmp;\n return true;\n\n }\n else if (document.selection && document.selection.createRange) { // IE.\n /*\n * TODO: check if this still works. OLD CODE.\n */\n var r = document.selection.createRange();\n var dr = r.duplicate();\n dr.moveToElementText(ta);\n dr.setEndPoint(\"EndToEnd\", r);\n r.text = sToInsert;\n return true;\n }\n /*\n * Other browsers we can't handle.\n */\n else {\n return false;\n }\n }\n\n return {\n setupAllTAs: setupAllTAs,\n initQuestionTA: initQuestionTA,\n setupShowHideAnswer: setupShowHideAnswer,\n };\n});\n"],"names":["define","$","initTextArea","this","data","on","e","window","hasOwnProperty","behattesting","undefined","which","keyCode","shiftKey","insertString","preventDefault","selectionStart","before","value","substring","eol","lastIndexOf","line","indent","i","length","charAt","ctrlKey","altKey","ta","sToInsert","selSave","selectionEnd","after","tmp","scrollTop","pos","document","selection","createRange","r","dr","duplicate","moveToElementText","setEndPoint","text","setupAllTAs","each","initQuestionTA","taId","getElementById","setupShowHideAnswer","linkId","divId","link","div","hdr","children","hdrText","innerText","click","is","hide","replace","show","trigger"],"mappings":";;;;;;;;AA2BAA,oCAAO,CAAC,WAAW,SAASC,YAsDfC,eAMLD,EAAEE,MAAMC,KAAK,mBAAmB,GAChCH,EAAEE,MAAMC,KAAK,gBAAgB,GAE7BH,EAAEE,MAAME,GAAG,aAAa,WAMpBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,WAIlBJ,EAAEE,MAAMC,KAAK,eAAgBH,EAAEE,MAAMC,KAAK,uBAG9CH,EAAEE,MAAME,GAAG,SAAS,WAChBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,SAASC,QAIvBC,OAAOC,eAAe,iBAAmBD,OAAOE,mBAEpCC,IAAZJ,EAAEK,OAAmC,IAAZL,EAAEK,UAlCzB,GAmCEL,EAAEM,SAAkBX,EAAEE,MAAMC,KAAK,iBAI7BE,EAAEO,UAAYC,aAAaX,KAAM,UACjCG,EAAES,sBAGL,GA1CD,KA0CKT,EAAEM,cAA6CF,IAAxBP,KAAKa,eAA8B,SAI3DC,OAASd,KAAKe,MAAMC,UAAU,EAAGhB,KAAKa,gBACtCI,IAAMH,OAAOI,YAAY,MACzBC,KAAOL,OAAOE,UAAUC,IAAM,GAC9BG,OAAS,GACJC,EAAI,EAAGA,EAAIF,KAAKG,QAA6B,MAAnBH,KAAKI,OAAOF,GAAYA,IACvDD,QAAkB,IAElBT,aAAaX,KAAM,KAAOoB,SAC1BjB,EAAES,iBAKNd,EAAEE,MAAMC,KAAK,gBAAgB,QAzD7B,KA2DKE,EAAEM,SAAqBN,EAAEqB,UAAYrB,EAAEsB,QAM5C3B,EAAEE,MAAMC,KAAK,gBAAiBH,EAAEE,MAAMC,KAAK,iBAC3CE,EAAES,kBAnEJ,KAqEOT,EAAEM,QAIPX,EAAEE,MAAMC,KAAK,gBAAgB,GAEtBE,EAAEqB,SAAWrB,EAAEsB,QAItB3B,EAAEE,MAAMC,KAAK,gBAAgB,eAYpCU,aAAae,GAAIC,mBACIpB,IAAtBmB,GAAGb,eAA8B,KAC7BC,OAASY,GAAGX,MAAMC,UAAU,EAAGU,GAAGb,gBAClCe,QAAUF,GAAGG,aACbC,MAAQJ,GAAGX,MAAMC,UAAUU,GAAGG,aAAcH,GAAGX,MAAMO,QAKrDS,IAAML,GAAGM,UACbN,GAAGX,MAAQD,OAASa,UAAYG,UAC5BG,IAAML,QAAUD,UAAUL,cAC9BI,GAAGb,eAAiBoB,IACpBP,GAAGG,aAAeI,IAClBP,GAAGM,UAAYD,KACR,EAGN,GAAIG,SAASC,WAAaD,SAASC,UAAUC,YAAa,KAIvDC,EAAIH,SAASC,UAAUC,cACvBE,GAAKD,EAAEE,mBACXD,GAAGE,kBAAkBd,IACrBY,GAAGG,YAAY,WAAYJ,GAC3BA,EAAEK,KAAOf,WACF,SAMA,QAIR,CACHgB,uBAjLA7C,EAAE,sBAAsB8C,KAAK7C,eAkL7B8C,wBA1KoBC,MACpBhD,EAAEoC,SAASa,eAAeD,OAAOF,KAAK7C,eA0KtCiD,6BAhKyBC,OAAQC,WAC7BC,KAAOrD,EAAEoC,SAASa,eAAeE,SACjCG,IAAMtD,EAAEoC,SAASa,eAAeG,QAChCG,IAAMF,KAAKG,WAAW,GACtBC,QAAUF,IAAIG,UAClBL,KAAKM,OAAM,KACHL,IAAIM,GAAG,aACPN,IAAIO,OACJN,IAAIG,UAAYD,QAAQK,QAAQ,IAAU,OAE1CR,IAAIS,OACJT,IAAIU,QAAQ,aACZT,IAAIG,UAAYD,QAAQK,QAAQ,IAAU"}
\ No newline at end of file
diff --git a/amd/build/ui_ace.min.js b/amd/build/ui_ace.min.js
index b2ca7b514..db72c4da7 100644
--- a/amd/build/ui_ace.min.js
+++ b/amd/build/ui_ace.min.js
@@ -14,6 +14,6 @@
* @copyright Richard Lobb, 2015, 2017, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_ace",["jquery"],(function($){function AceWrapper(textareaId,w,h,params){var session,code,textarea=$(document.getElementById(textareaId)),wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=textarea[0]===document.activeElement,lang=params.lang,t=this;try{window.ace.require("ace/ext/language_tools"),this.modelist=window.ace.require("ace/ext/modelist"),this.textareaId=textareaId,this.textarea=textarea,this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),textarea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({enableBasicAutocompletion:!0,enableLiveAutocompletion:params.live_autocompletion,fontSize:params.font_size?params.font_size:"14px",newLineMode:"unix"}),this.editor.$blockScrolling=1/0,session=this.editor.getSession(),code=this.textarea.val(),(void 0===params.import_from_scratchpad||params.import_from_scratchpad)&&(code=this.extract_from_json_maybe(code)),session.setValue(code);var userTheme=window.localStorage.getItem("qtype_coderunner.ace.theme"),consider_prefers=params.auto_switch_light_dark&&window.matchMedia;null!==userTheme?this.editor.setTheme(userTheme):consider_prefers&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):consider_prefers&&window.matchMedia("(prefers-color-scheme: light)").matches?this.editor.setTheme("ace/theme/textmate"):params.theme?this.editor.setTheme("ace/theme/"+params.theme):this.editor.setTheme("ace/theme/textmate"),this.fixSlowLoad(),this.setLanguage(lang),this.setEventHandlers(textarea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.fail=!1}catch(err){this.fail=!0}}return AceWrapper.prototype.extract_from_json_maybe=function(code){try{code=JSON.parse(code).answer_code[0]}catch(err){}return code},AceWrapper.prototype.failed=function(){return this.fail},AceWrapper.prototype.failMessage=function(){return"ace_ui_notready"},AceWrapper.prototype.sync=function(){var thisThemeNow=this.editor.getOption("theme"),thisEditorKey="qtype_coderunner.".concat(this.textareaId,".theme"),lastTheme=window.sessionStorage.getItem(thisEditorKey),globalTheme=window.localStorage.getItem("qtype_coderunner.ace.theme");null!==lastTheme&&lastTheme!=thisThemeNow?window.localStorage.setItem("qtype_coderunner.ace.theme",thisThemeNow):globalTheme&&globalTheme!==thisThemeNow&&(this.editor.setOption("theme",globalTheme),function(name){throw new TypeError('"'+name+'" is read-only')}("thisThemeNow")),window.sessionStorage.setItem(thisEditorKey,thisThemeNow)},AceWrapper.prototype.syncIntervalSecs=function(){return 2},AceWrapper.prototype.setLanguage=function(language){var session=this.editor.getSession(),mode=this.findMode(language);mode&&session.setMode(mode.mode)},AceWrapper.prototype.getElement=function(){return this.editNode},AceWrapper.prototype.captureTab=function(){this.capturingTab=!0,this.editor.commands.bindKeys({Tab:"indent","Shift-Tab":"outdent"})},AceWrapper.prototype.releaseTab=function(){this.capturingTab=!1,this.editor.commands.bindKeys({Tab:null,"Shift-Tab":null})},AceWrapper.prototype.fixSlowLoad=function(){var observer=new IntersectionObserver((function(){$(document).trigger("mousemove")})),editNode=this.editNode.get(0);observer.observe(editNode)},AceWrapper.prototype.setEventHandlers=function(){var t=this;this.editor.getSession().on("change",(function(){t.textarea.val(t.editor.getSession().getValue()),t.contents_changed=!0})),this.editor.on("blur",(function(){t.contents_changed&&t.textarea.trigger("change")})),this.editor.on("mousedown",(function(){t.clickInProgress=!0})),this.editor.on("focus",(function(){t.clickInProgress?t.captureTab():t.releaseTab()})),this.editor.on("click",(function(){t.clickInProgress=!1})),this.editor.container.addEventListener("keydown",(function(e){void 0!==e.which&&0===e.which||(77===e.keyCode&&e.ctrlKey&&!e.altKey?(t.capturingTab?t.releaseTab():t.captureTab(),e.preventDefault()):27===e.keyCode?t.releaseTab():e.shiftKey||e.ctrlKey||e.altKey||9==e.keyCode||t.captureTab())}),!0)},AceWrapper.prototype.destroy=function(){var focused;this.fail||(focused=this.editor.isFocused(),this.textarea.val(this.editor.getSession().getValue()),this.editor.destroy(),$(this.editNode).remove(),focused&&(this.textarea.focus(),this.textarea[0].selectionStart=this.textarea[0].value.length))},AceWrapper.prototype.hasFocus=function(){return this.editor.isFocused()},AceWrapper.prototype.findMode=function(language){var candidate,filename,result,candidates,nameMap={octave:"matlab",nodejs:"javascript","c#":"cs"};if("string"==typeof language){language.toLowerCase()in nameMap&&(language=nameMap[language.toLowerCase()]),candidates=[language,language.replace(/\d+$/,"")];for(var i=0;i
"),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),textarea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({enableBasicAutocompletion:!0,enableLiveAutocompletion:params.live_autocompletion,fontSize:params.font_size?params.font_size:"14px",newLineMode:"unix"}),this.editor.$blockScrolling=1/0,session=this.editor.getSession(),code=this.textarea.val(),(void 0===params.import_from_scratchpad||params.import_from_scratchpad)&&(code=this.extract_from_json_maybe(code)),session.setValue(code);const userTheme=window.localStorage.getItem("qtype_coderunner.ace.theme"),consider_prefers=params.auto_switch_light_dark&&window.matchMedia;null!==userTheme?this.editor.setTheme(userTheme):consider_prefers&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):consider_prefers&&window.matchMedia("(prefers-color-scheme: light)").matches?this.editor.setTheme("ace/theme/textmate"):params.theme?this.editor.setTheme("ace/theme/"+params.theme):this.editor.setTheme("ace/theme/textmate"),this.fixSlowLoad(),this.setLanguage(lang),this.setEventHandlers(textarea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.fail=!1}catch(err){this.fail=!0}}return AceWrapper.prototype.extract_from_json_maybe=function(code){try{code=JSON.parse(code).answer_code[0]}catch(err){}return code},AceWrapper.prototype.failed=function(){return this.fail},AceWrapper.prototype.failMessage=function(){return"ace_ui_notready"},AceWrapper.prototype.sync=function(){const thisThemeNow=this.editor.getOption("theme"),thisEditorKey="qtype_coderunner.".concat(this.textareaId,".theme"),lastTheme=window.sessionStorage.getItem(thisEditorKey),globalTheme=window.localStorage.getItem("qtype_coderunner.ace.theme");null!==lastTheme&&lastTheme!=thisThemeNow?window.localStorage.setItem("qtype_coderunner.ace.theme",thisThemeNow):globalTheme&&globalTheme!==thisThemeNow&&(this.editor.setOption("theme",globalTheme),thisThemeNow=globalTheme),window.sessionStorage.setItem(thisEditorKey,thisThemeNow)},AceWrapper.prototype.syncIntervalSecs=function(){return 2},AceWrapper.prototype.setLanguage=function(language){var session=this.editor.getSession(),mode=this.findMode(language);mode&&session.setMode(mode.mode)},AceWrapper.prototype.getElement=function(){return this.editNode},AceWrapper.prototype.captureTab=function(){this.capturingTab=!0,this.editor.commands.bindKeys({Tab:"indent","Shift-Tab":"outdent"})},AceWrapper.prototype.releaseTab=function(){this.capturingTab=!1,this.editor.commands.bindKeys({Tab:null,"Shift-Tab":null})},AceWrapper.prototype.fixSlowLoad=function(){const observer=new IntersectionObserver((()=>{$(document).trigger("mousemove")})),editNode=this.editNode.get(0);observer.observe(editNode)},AceWrapper.prototype.setEventHandlers=function(){var t=this;this.editor.getSession().on("change",(function(){t.textarea.val(t.editor.getSession().getValue()),t.contents_changed=!0})),this.editor.on("blur",(function(){t.contents_changed&&t.textarea.trigger("change")})),this.editor.on("mousedown",(function(){t.clickInProgress=!0})),this.editor.on("focus",(function(){t.clickInProgress?t.captureTab():t.releaseTab()})),this.editor.on("click",(function(){t.clickInProgress=!1})),this.editor.container.addEventListener("keydown",(function(e){void 0!==e.which&&0===e.which||(77===e.keyCode&&e.ctrlKey&&!e.altKey?(t.capturingTab?t.releaseTab():t.captureTab(),e.preventDefault()):27===e.keyCode?t.releaseTab():e.shiftKey||e.ctrlKey||e.altKey||9==e.keyCode||t.captureTab())}),!0)},AceWrapper.prototype.destroy=function(){var focused;this.fail||(focused=this.editor.isFocused(),this.textarea.val(this.editor.getSession().getValue()),this.editor.destroy(),$(this.editNode).remove(),focused&&(this.textarea.focus(),this.textarea[0].selectionStart=this.textarea[0].value.length))},AceWrapper.prototype.hasFocus=function(){return this.editor.isFocused()},AceWrapper.prototype.findMode=function(language){var candidate,filename,result,candidates,nameMap={octave:"matlab",nodejs:"javascript","c#":"cs"};if("string"==typeof language){language.toLowerCase()in nameMap&&(language=nameMap[language.toLowerCase()]),candidates=[language,language.replace(/\d+$/,"")];for(var i=0;i.\n\n/**\n * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n code,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n this.textareaId = textareaId;\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n enableLiveAutocompletion: params.live_autocompletion,\n fontSize: params.font_size ? params.font_size : \"14px\",\n newLineMode: \"unix\",\n });\n\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n code = this.textarea.val();\n if (params.import_from_scratchpad === undefined || params.import_from_scratchpad) {\n code = this.extract_from_json_maybe(code);\n }\n session.setValue(code);\n\n // If there's a user-defined theme in local storage, use that.\n // Otherwise use the 'prefers-color-scheme' option if given or\n // the question/system defaults if not.\n const userTheme = window.localStorage.getItem('qtype_coderunner.ace.theme');\n const consider_prefers = params.auto_switch_light_dark && window.matchMedia;\n if (userTheme !== null) {\n this.editor.setTheme(userTheme);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: light)').matches) {\n this.editor.setTheme(ACE_LIGHT_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.fixSlowLoad();\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n AceWrapper.prototype.extract_from_json_maybe = function(code) {\n // If the given code looks like JSON from the Scratchpad UI,\n // extract and return the answer_code attribute.\n try {\n const jsonObj = JSON.parse(code);\n code = jsonObj.answer_code[0];\n } catch(err) {}\n\n return code;\n };\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // The data is always sync'd to the text area. But here we use sync to\n // poll the value of the current theme and record in browser local\n // storage if the value for this particular Ace instance has changed\n // (as recorded in session storage). The theme to use for all Ace windows\n // from now on in this browser is recorded in browser local storage.\n const thisThemeNow = this.editor.getOption('theme');\n const thisEditorKey = `qtype_coderunner.${this.textareaId}.theme`;\n const lastTheme = window.sessionStorage.getItem(thisEditorKey);\n const globalKey = 'qtype_coderunner.ace.theme';\n const globalTheme = window.localStorage.getItem(globalKey);\n\n if (lastTheme !== null && lastTheme != thisThemeNow) {\n // The theme in this Ace window has been changed by the user.\n window.localStorage.setItem(globalKey, thisThemeNow);\n } else if (globalTheme && globalTheme !== thisThemeNow) {\n // A theme has been defined by another Ace window since we loaded.\n // Switch to that theme.\n this.editor.setOption('theme', globalTheme);\n thisThemeNow = globalTheme;\n }\n window.sessionStorage.setItem(thisEditorKey, thisThemeNow);\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 2;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n // Sometimes Ace editors do not load until the mouse is moved. To fix this,\n // 'move' the mouse using JQuery when the editor div enters the viewport.\n AceWrapper.prototype.fixSlowLoad = function () {\n const observer = new IntersectionObserver( () => {\n $(document).trigger('mousemove');\n });\n const editNode = this.editNode.get(0); // Non-JQuerry node.\n observer.observe(editNode);\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","session","code","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","enableLiveAutocompletion","live_autocompletion","fontSize","font_size","newLineMode","$blockScrolling","Infinity","getSession","val","undefined","import_from_scratchpad","extract_from_json_maybe","setValue","userTheme","localStorage","getItem","consider_prefers","auto_switch_light_dark","matchMedia","setTheme","matches","theme","fixSlowLoad","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","JSON","parse","answer_code","failed","failMessage","sync","thisThemeNow","getOption","thisEditorKey","lastTheme","sessionStorage","globalTheme","setItem","setOption","syncIntervalSecs","language","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","observer","IntersectionObserver","trigger","observe","getValue","container","addEventListener","e","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_ace.min.js","sources":["../src/ui_ace.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 * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n code,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n this.textareaId = textareaId;\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n enableLiveAutocompletion: params.live_autocompletion,\n fontSize: params.font_size ? params.font_size : \"14px\",\n newLineMode: \"unix\",\n });\n\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n code = this.textarea.val();\n if (params.import_from_scratchpad === undefined || params.import_from_scratchpad) {\n code = this.extract_from_json_maybe(code);\n }\n session.setValue(code);\n\n // If there's a user-defined theme in local storage, use that.\n // Otherwise use the 'prefers-color-scheme' option if given or\n // the question/system defaults if not.\n const userTheme = window.localStorage.getItem('qtype_coderunner.ace.theme');\n const consider_prefers = params.auto_switch_light_dark && window.matchMedia;\n if (userTheme !== null) {\n this.editor.setTheme(userTheme);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: light)').matches) {\n this.editor.setTheme(ACE_LIGHT_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.fixSlowLoad();\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n AceWrapper.prototype.extract_from_json_maybe = function(code) {\n // If the given code looks like JSON from the Scratchpad UI,\n // extract and return the answer_code attribute.\n try {\n const jsonObj = JSON.parse(code);\n code = jsonObj.answer_code[0];\n } catch(err) {}\n\n return code;\n };\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // The data is always sync'd to the text area. But here we use sync to\n // poll the value of the current theme and record in browser local\n // storage if the value for this particular Ace instance has changed\n // (as recorded in session storage). The theme to use for all Ace windows\n // from now on in this browser is recorded in browser local storage.\n const thisThemeNow = this.editor.getOption('theme');\n const thisEditorKey = `qtype_coderunner.${this.textareaId}.theme`;\n const lastTheme = window.sessionStorage.getItem(thisEditorKey);\n const globalKey = 'qtype_coderunner.ace.theme';\n const globalTheme = window.localStorage.getItem(globalKey);\n\n if (lastTheme !== null && lastTheme != thisThemeNow) {\n // The theme in this Ace window has been changed by the user.\n window.localStorage.setItem(globalKey, thisThemeNow);\n } else if (globalTheme && globalTheme !== thisThemeNow) {\n // A theme has been defined by another Ace window since we loaded.\n // Switch to that theme.\n this.editor.setOption('theme', globalTheme);\n thisThemeNow = globalTheme;\n }\n window.sessionStorage.setItem(thisEditorKey, thisThemeNow);\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 2;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n // Sometimes Ace editors do not load until the mouse is moved. To fix this,\n // 'move' the mouse using JQuery when the editor div enters the viewport.\n AceWrapper.prototype.fixSlowLoad = function () {\n const observer = new IntersectionObserver( () => {\n $(document).trigger('mousemove');\n });\n const editNode = this.editNode.get(0); // Non-JQuerry node.\n observer.observe(editNode);\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","session","code","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","enableLiveAutocompletion","live_autocompletion","fontSize","font_size","newLineMode","$blockScrolling","Infinity","getSession","val","undefined","import_from_scratchpad","extract_from_json_maybe","setValue","userTheme","localStorage","getItem","consider_prefers","auto_switch_light_dark","matchMedia","setTheme","matches","theme","fixSlowLoad","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","JSON","parse","answer_code","failed","failMessage","sync","thisThemeNow","getOption","thisEditorKey","lastTheme","sessionStorage","globalTheme","setItem","setOption","syncIntervalSecs","language","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","observer","IntersectionObserver","trigger","observe","getValue","container","addEventListener","e","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_ace_gapfiller.min.js b/amd/build/ui_ace_gapfiller.min.js
index fa7b24b07..3cf7ed422 100644
--- a/amd/build/ui_ace_gapfiller.min.js
+++ b/amd/build/ui_ace_gapfiller.min.js
@@ -38,6 +38,6 @@
* @copyright Matthew Toohey, 2021, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_ace_gapfiller",["jquery"],(function($){var Range,validChars=/[ !"#$%&'()*+,`\-./0-9:;<=>?@A-Z\[\]\\^_a-z{}|~]/;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this,code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){var cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){var nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){var char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){var _char=e.args;validChars.test(_char)&&gap.insertChar(t.gaps,selectionRange.start,_char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){var maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+1=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(var i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(var i=start;i?@A-Z\[\]\\^_a-z{}|~]/;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this;let code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){let cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){let nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,selectionRange.start,char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){let maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+1=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(let i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(let i=start;i.\n\n/**\n * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","matchMedia","matches","setTheme","theme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_ace_gapfiller.min.js","sources":["../src/ui_ace_gapfiller.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 util.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 * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","matchMedia","matches","setTheme","theme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDAA,2CAAO,CAAC,WAAW,SAASC,OAEpBC,YAEEC,WAAa,4DAWVC,eAAeC,WAAYC,EAAGC,EAAGC,eACjCC,SAAWR,EAAES,SAASC,eAAeN,iBACtCO,QAAUX,EAAES,SAASC,eAAeN,WAAa,aACjDQ,QAAUC,KAAKL,SAAS,KAAOC,SAASK,cACxCC,KAAOR,SAASQ,KAChBC,EAAIH,SAEJI,KAAO,QACNV,SAAWA,cACXW,KAAO,QACPC,OAASZ,SAASa,WAAa,mBAC/BC,aAAe,EACA,gBAAhBR,KAAKM,QAA4C,UAAhBN,KAAKM,SACtCG,MAAM,oDACDH,OAAS,eAGdF,KADe,eAAfJ,KAAKM,OACEN,KAAKL,SAASe,KAAK,oBAEnBV,KAAKL,SAASe,KAAK,kBAI1BC,OAAOC,IAAIC,QAAQ,0BACnBzB,MAAQuB,OAAOC,IAAIC,QAAQ,aAAazB,WACnC0B,SAAWH,OAAOC,IAAIC,QAAQ,yBAE9BE,SAAU,OACVC,kBAAmB,OACnBC,cAAe,OACfC,iBAAkB,OAElBC,SAAWhC,EAAE,oBACbgC,SAASC,IAAI,CACdC,OAAQ,OACRC,OAAQ7B,EACR8B,MAAO,cAGNC,OAASb,OAAOC,IAAIa,KAAKzB,KAAKmB,SAASO,IAAI,IAC5C1B,KAAKL,SAASgC,KAAK,kBACdH,OAAOI,aAAY,QAGvBJ,OAAOK,WAAW,CACnBC,qBAAqB,EACrBC,aAAa,EACbC,2BAA2B,EAC3BC,YAAa,cAEZT,OAAOU,gBAAkBC,EAAAA,EAI1BxB,OAAOyB,YAAczB,OAAOyB,WAAW,gCAAgCC,aAClEb,OAAOc,SAjED,4BAkEJ5C,SAAS6C,WACXf,OAAOc,SAAS,aAAe5C,SAAS6C,YAExCf,OAAOc,SApEA,2BAuEXE,YAAYtC,WAEZuC,iBAAiBzC,KAAKL,eACtB+C,kBAMAlB,OAAOmB,SAASC,GAAG,eAAe,eAC/BC,OAAU/C,QAAQgD,KAAK,eACvBD,OAAOE,SAAS,uBAGpBF,OAAOG,SAAS,qBAEZjD,UACAI,EAAEqB,OAAOyB,QACT9C,EAAEqB,OAAO0B,mBAEb/C,EAAEgD,SAAWrD,QAAQgD,KAAK,iBAC1B3C,EAAEgD,SAASzC,KAAK,MAAO,OAASnB,YAEhCY,EAAEiD,YAActD,QAAQgD,KAAK,mBAC7B3C,EAAEiD,YAAY1C,KAAK,KAAM,OAASnB,qBAGjC8D,WAAWjD,WAGXoB,OAAO8B,SAASV,GAAG,QAAQ,SAASW,OACjCC,OAASrD,EAAEqB,OAAOiC,UAAUC,YAC5BC,YAAcJ,EAAEK,QAAQC,KACxBC,eAAiB3D,EAAEqB,OAAOuC,oBAE1BC,IAAM7D,EAAE8D,cAAcT,WAEtBG,YAAYO,WAAW,MAAO,IAClB,OAARF,KAAgC,cAAhBL,aAA+BH,OAAOW,SAAWH,IAAII,MAAMC,MAAMF,OAAOH,IAAIM,gBAE5FnE,EAAEqB,OAAO+C,aAAaf,OAAOgB,IAAKR,IAAII,MAAMK,IAAIN,OAAO,MAMnD,OAARH,IAEoB,cAAhBL,aACAxD,EAAEqB,OAAOiC,UAAUiB,iBAGpB,GAAoB,WAAhBf,YAA0B,KAE7BgB,QAAUxE,EAAEE,MAAM2D,IAAIY,MAAM,GAAKzE,EAAEE,KAAKwE,QAC5C1E,EAAEqB,OAAO+C,aAAaI,QAAQP,MAAMC,MAAMG,IAAKG,QAAQP,MAAMC,MAAMF,OAAOQ,QAAQL,UAClFnE,EAAEqB,OAAOiC,UAAUqB,sBAEhB,GAAoB,cAAhBnB,YAEPxD,EAAEqB,OAAOiC,UAAUsB,kBAAkB,IAAI3F,MAAM4E,IAAII,MAAMC,MAAMG,IAC1BR,IAAII,MAAMC,MAAMF,OAChBH,IAAII,MAAMC,MAAMG,IAChBR,IAAII,MAAMK,IAAIN,SAAS,QAEzD,GAAIhE,EAAEqB,OAAOiC,UAAUuB,UAAW,IAEjB,iBAAhBrB,YAAgC,KAC5BsB,KAAO1B,EAAE2B,KAET7F,WAAW8F,KAAKF,OAChBjB,IAAIoB,WAAWjF,EAAEE,KAAMmD,OAAQyB,UAEZ,cAAhBtB,YAEHH,OAAOW,OAASH,IAAII,MAAMC,MAAMF,QAAUH,IAAIM,SAAW,GACzDN,IAAIqB,WAAWlF,EAAEE,KAAM,CAACmE,IAAKhB,OAAOgB,IAAKL,OAAQX,OAAOW,OAAO,IAE5C,QAAhBR,aAEHH,OAAOW,OAASH,IAAII,MAAMC,MAAMF,OAASH,IAAIM,UAAYN,IAAIM,SAAW,GACxEN,IAAIqB,WAAWlF,EAAEE,KAAMmD,QAG/BrD,EAAEqB,OAAOiC,UAAUqB,sBAEhB,IAAK3E,EAAEqB,OAAOiC,UAAUuB,WAAahB,IAAIsB,YAAYxB,eAAeO,QAC7DL,IAAIsB,YAAYxB,eAAeW,OAIrB,iBAAhBd,aAAkD,cAAhBA,aACf,QAAhBA,aAAyC,UAAhBA,aACT,QAAhBA,cAEHK,IAAIuB,YAAYpF,EAAEE,KAAMyD,eAAeO,MAAMF,OAAQL,eAAeW,IAAIN,QACxEhE,EAAEqB,OAAOiC,UAAUqB,kBAGH,iBAAhBnB,aAAgC,KAC5BsB,KAAO1B,EAAE2B,KACT7F,WAAW8F,KAAKF,OAChBjB,IAAIoB,WAAWjF,EAAEE,KAAMyD,eAAeO,MAAOY,MAM7C,OAARjB,KAAgC,UAAhBL,aAChBK,IAAIwB,WAAWrF,EAAEE,KAAMyD,eAAeO,MAAMF,OAAQZ,EAAE2B,KAAKO,MAG/DlC,EAAEmC,iBACFnC,EAAEoC,qBAINxF,EAAEqB,OAAOiC,UAAUb,GAAG,gBAAgB,eAC9BY,OAASrD,EAAEqB,OAAOiC,UAAUC,YAC5BM,IAAM7D,EAAE8D,cAAcT,QACd,OAARQ,KACIR,OAAOW,OAASH,IAAII,MAAMC,MAAMF,OAAOH,IAAIM,UAC3CnE,EAAEqB,OAAO+C,aAAaP,IAAII,MAAMC,MAAMG,IAAKR,IAAII,MAAMC,MAAMF,OAAOH,IAAIM,kBAK7EsB,YAAc,UAGdpE,OAAOoB,GAAG,eAAe,SAASW,OAC/BC,OAASrD,EAAEqB,OAAOiC,UAAUC,YAC5BM,IAAM7D,EAAE8D,cAAcT,QACd,OAARQ,MACA7D,EAAEqB,OAAOiC,UAAUsB,kBAAkB,IAAI3F,MAAM4E,IAAII,MAAMC,MAAMG,IAChBR,IAAII,MAAMC,MAAMF,OAChBH,IAAII,MAAMC,MAAMG,IAChBR,IAAII,MAAMK,IAAIN,SAAS,GACtEhE,EAAEyF,YAAc5B,IAChBT,EAAEmC,iBACFnC,EAAEoC,2BAKLnE,OAAOoB,GAAG,SAAS,SAASW,GACzBpD,EAAEyF,cACFzF,EAAEqB,OAAO+C,aAAapE,EAAEyF,YAAYxB,MAAMC,MAAMG,IAAKrE,EAAEyF,YAAYxB,MAAMC,MAAMF,OAAOhE,EAAEyF,YAAYtB,UACpGnE,EAAEyF,YAAc,KAChBrC,EAAEmC,iBACFnC,EAAEoC,2BAILE,MAAO,OACPC,SAET,MAAMC,UAEGF,MAAO,YAsRXG,IAAIxE,OAAQgD,IAAKL,OAAQ8B,cAAUC,gEAAS/D,EAAAA,OAC5CX,OAASA,YAETyE,SAAWA,cACXC,SAAWA,cAEX9B,MAAQ,IAAIhF,MAAMoF,IAAKL,OAAQK,IAAKL,OAAO8B,eAC3C3B,SAAW,OAGX9C,OAAO2E,QAAQC,UAAUpG,KAAKoE,MAAO,kBAAmB,QAAQ,QAChE5C,OAAO2E,QAAQC,UAAUpG,KAAKoE,MAAO,qBAAsB,QAAQ,UAvR5E9E,eAAe+G,UAAUhD,WAAa,SAASjD,eAOlCkG,SAASC,WACVC,EAAyBC,OAAO,GAC3BC,EAAI,EAAGA,EAAIH,EAAE1B,OAAQ6B,IAAK,CAC/BF,EAAID,EAAEG,OACD,IAAIC,EAAI,EAAGA,EAHF,UAGe9B,OAAQ8B,IAC7BH,IAJM,UAISG,KACfH,EAAI,KAAOA,GAGnBC,QAAUD,SAEPC,YAjBNpG,KAAO,OAoBRuG,MAAQxG,KAAKyG,MAAM,SAEnBC,QAAUR,SAAS,MACnBS,SAAWT,SAAS,MACpBU,SAAW,IAAIC,OAAOH,QAAU,iCAAmCC,UAEnEG,cAAgB,OACf,IAAIR,EAAI,EAAGA,EAAIE,MAAM/B,OAAQ6B,IAAK,KAC/BS,KAAOP,MAAMF,GAAGG,MAAMG,UAC1BE,eAAiBC,KAAK,OAElBC,UAAYD,KAAK,GAAGtC,WACnB,IAAI8B,EAAI,EAAGA,EAAIQ,KAAKtC,OAAQ8B,GAAK,EAAG,KACjCU,OAASF,KAAKR,GAAGE,MAAM,KACvBZ,SAAWqB,SAASD,OAAO,IAC3BnB,SAAYmB,OAAOxC,OAAS,EAAIyC,SAASD,OAAO,IAAMlF,EAAAA,EAGtD6B,IAAM,IAAIgC,IAAIhG,KAAKwB,OAAQkF,EAAGU,UAAWnB,SAAUC,UACvDlC,IAAIY,MAAQ5E,KAAKQ,kBACZA,cAAgB,OAChBH,KAAKkH,KAAKvD,KAEfoD,WAAanB,SACbiB,eAAiB,IAAIM,OAAOvB,UACxBU,EAAI,EAAIQ,KAAKtC,SACbqC,eAAiBC,KAAKR,EAAE,GACxBS,WAAaD,KAAKR,EAAE,GAAG9B,QAK3B6B,EAAIE,MAAM/B,OAAO,IACjBqC,eAAiB,WAGpB1F,OAAO2E,QAAQsB,SAASP,gBAWjC5H,eAAe+G,UAAUpC,cAAgB,SAAST,YACzC,IAAIkD,EAAE,EAAGA,EAAI1G,KAAKK,KAAKwE,OAAQ6B,IAAK,KACjC1C,IAAMhE,KAAKK,KAAKqG,MAChB1C,IAAIsB,YAAY9B,eACTQ,WAGR,MAGX1E,eAAe+G,UAAUqB,OAAS,kBACvB1H,KAAK6F,MAGhBvG,eAAe+G,UAAUsB,YAAc,iBAC5B,mBAKXrI,eAAe+G,UAAUuB,KAAO,eACxBC,cAAgB,GAChBC,OAAQ,MAEP,IAAIpB,EAAE,EAAGA,EAAI1G,KAAKK,KAAKwE,OAAQ6B,IAAK,KAEjCqB,MADM/H,KAAKK,KAAKqG,GACJsB,UAChBH,cAAcN,KAAKQ,OACL,KAAVA,QACAD,OAAQ,GAGZA,WACKnI,SAASsI,IAAI,SAEbtI,SAASsI,IAAIC,KAAKC,UAAUN,iBAKzCvI,eAAe+G,UAAUP,OAAS,eAC1BsC,QAAUpI,KAAKL,SAASsI,SACxBG,gBAEQf,OAASa,KAAKG,MAAMD,aACnB,IAAI1B,EAAI,EAAGA,EAAI1G,KAAKK,KAAKwE,OAAQ6B,IAAK,KACnCqB,MAAQrB,EAAIW,OAAOxC,OAASwC,OAAOX,GAAI,WACtCrG,KAAKqG,GAAGlB,WAAWxF,KAAKK,KAAML,KAAKK,KAAKqG,GAAGtC,MAAMC,MAAMF,OAAQ4D,QAE1E,MAAMxE,MAMhBjE,eAAe+G,UAAU7D,YAAc,SAAS8F,cACxCnC,QAAUnG,KAAKwB,OAAO+G,aACtBC,KAAOxI,KAAKyI,SAASH,UACrBE,MACArC,QAAQuC,QAAQF,KAAKA,OAI7BlJ,eAAe+G,UAAUsC,WAAa,kBAC3B3I,KAAKmB,UAGhB7B,eAAe+G,UAAU3D,WAAa,gBAC7BzB,cAAe,OACfO,OAAO8B,SAASsF,SAAS,KAAQ,qBAAuB,aAGjEtJ,eAAe+G,UAAUwC,WAAa,gBAC7B5H,cAAe,OACfO,OAAO8B,SAASsF,SAAS,KAAQ,iBAAmB,QAG7DtJ,eAAe+G,UAAU5D,iBAAmB,eAIpCtC,EAAIH,UAEHwB,OAAO+G,aAAa3F,GAAG,UAAU,WAClCzC,EAAEa,kBAAmB,UAGpBQ,OAAOoB,GAAG,QAAQ,WACfzC,EAAEa,kBACFb,EAAER,SAASmJ,QAAQ,kBAItBtH,OAAOoB,GAAG,aAAa,WAIxBzC,EAAEe,iBAAkB,UAGnBM,OAAOoB,GAAG,SAAS,WAChBzC,EAAEe,gBACFf,EAAEuC,aAEFvC,EAAE0I,qBAILrH,OAAOoB,GAAG,SAAS,WACpBzC,EAAEe,iBAAkB,UAGnBM,OAAOuH,UAAUC,iBAAiB,WAAW,SAASzF,QACvC0F,IAAZ1F,EAAE2F,OAAmC,IAAZ3F,EAAE2F,QAjCvB,KAkCA3F,EAAE4F,SAAqB5F,EAAE6F,UAAY7F,EAAE8F,QACnClJ,EAAEc,aACFd,EAAE0I,aAEF1I,EAAEuC,aAENa,EAAEmC,kBAzCJ,KA2COnC,EAAE4F,QACPhJ,EAAE0I,aAEKtF,EAAE+F,UAAY/F,EAAE6F,SAAW7F,EAAE8F,QA/CtC,GA+CgD9F,EAAE4F,SAChDhJ,EAAEuC,iBAGX,IAGPpD,eAAe+G,UAAUkD,QAAU,eAE3BxJ,aADC6H,OAEA5H,KAAK6F,OAEN9F,QAAUC,KAAKwB,OAAOgI,iBACjBhI,OAAO+H,UACZpK,EAAEa,KAAKmB,UAAUsI,SACb1J,eACKJ,SAASsD,aACTtD,SAAS,GAAG+J,eAAiB1J,KAAKL,SAAS,GAAGoI,MAAMlD,UAKrEvF,eAAe+G,UAAUsD,SAAW,kBACzB3J,KAAKwB,OAAOgI,aAGvBlK,eAAe+G,UAAUoC,SAAW,SAAUH,cACtCsB,UACAC,SACApD,OACAqD,WACAC,QAAU,QACI,gBACA,kBACJ,SAGU,iBAAbzB,UAGPA,SAAS0B,gBAAiBD,UAC1BzB,SAAWyB,QAAQzB,SAAS0B,gBAGhCF,WAAa,CAACxB,SAAUA,SAAS2B,QAAQ,OAAQ,SAC5C,IAAIvD,EAAI,EAAGA,EAAIoD,WAAWjF,OAAQ6B,OAEnCmD,SAAW,UADXD,UAAYE,WAAWpD,KAEvBD,OAASzG,KAAKc,SAASoJ,YAAYN,YAC/B5J,KAAKc,SAASoJ,YAAYN,UAAUI,gBACpChK,KAAKc,SAASqJ,eAAeN,WAC7B7J,KAAKc,SAASqJ,eAAeN,SAASG,iBAEZ,SAAhBvD,OAAO5C,YACV4C,SAMnBnH,eAAe+G,UAAUhF,OAAS,SAAS7B,EAAGC,QACrC0B,SAASiJ,YAAY3K,QACrB0B,SAASkJ,WAAW7K,QACpBgC,OAAOH,UA0BhB2E,IAAIK,UAAUf,YAAc,SAAS9B,eACzBA,OAAOgB,KAAOxE,KAAKoE,MAAMC,MAAMG,KAAOhB,OAAOW,QAAUnE,KAAKoE,MAAMC,MAAMF,QACxEX,OAAOgB,KAAOxE,KAAKoE,MAAMK,IAAID,KAAOhB,OAAOW,QAAUnE,KAAKoE,MAAMK,IAAIN,QAGhF6B,IAAIK,UAAUiE,SAAW,kBACbtK,KAAKoE,MAAMK,IAAIN,OAAOnE,KAAKoE,MAAMC,MAAMF,QAGnD6B,IAAIK,UAAUkE,YAAc,SAASlK,KAAMmK,YAClCpG,MAAMK,IAAIN,QAAUqG,UAGpB,IAAI9D,EAAE,EAAGA,EAAIrG,KAAKwE,OAAQ6B,IAAK,KAC5B+D,MAAQpK,KAAKqG,GACb+D,MAAMrG,MAAMC,MAAMG,MAAQxE,KAAKoE,MAAMC,MAAMG,KAAOiG,MAAMrG,MAAMC,MAAMF,OAASnE,KAAKoE,MAAMK,IAAIN,SAC5FsG,MAAMrG,MAAMC,MAAMF,QAAUqG,MAC5BC,MAAMrG,MAAMK,IAAIN,QAAUqG,YAI7BhJ,OAAOkJ,2BACPlJ,OAAOmJ,wBAGhB3E,IAAIK,UAAUjB,WAAa,SAAS/E,KAAMuK,IAAK3F,MACvCjF,KAAKsE,WAAatE,KAAKsK,YAActK,KAAKsK,WAAatK,KAAKkG,eACvDqE,YAAYlK,KAAM,QAClBiE,UAAY,OACZ9C,OAAO2E,QAAQ0E,OAAOD,IAAK3F,OACzBjF,KAAKsE,SAAWtE,KAAKkG,gBACvB1E,OAAO2E,QAAQsD,OAAO,IAAIrK,MAAMwL,IAAIpG,IAAKxE,KAAKoE,MAAMK,IAAIN,OAAO,EAAGyG,IAAIpG,IAAKxE,KAAKoE,MAAMK,IAAIN,cAC1FG,UAAY,OACZ9C,OAAO2E,QAAQ0E,OAAOD,IAAK3F,QAIxCe,IAAIK,UAAUhB,WAAa,SAAShF,KAAMuK,UACjCtG,UAAY,OACZ9C,OAAO2E,QAAQsD,OAAO,IAAIrK,MAAMwL,IAAIpG,IAAKoG,IAAIzG,OAAQyG,IAAIpG,IAAKoG,IAAIzG,OAAO,IAE1EnE,KAAKsE,UAAYtE,KAAKiG,cACjBsE,YAAYlK,MAAO,QAGnBmB,OAAO2E,QAAQ0E,OAAO,CAACrG,IAAKoG,IAAIpG,IAAKL,OAAQnE,KAAKoE,MAAMK,IAAIN,OAAO,GA1jB/D,MA8jBjB6B,IAAIK,UAAUd,YAAc,SAASlF,KAAMgE,MAAOI,SACzC,IAAIiC,EAAIrC,MAAOqC,EAAIjC,IAAKiC,IACrBrC,MAAQrE,KAAKoE,MAAMC,MAAMF,OAAOnE,KAAKsE,eAChCe,WAAWhF,KAAM,CAACmE,IAAKxE,KAAKoE,MAAMC,MAAMG,IAAKL,OAAQE,SAKtE2B,IAAIK,UAAUb,WAAa,SAASnF,KAAMgE,MAAOoB,UACxC,IAAIiB,EAAI,EAAGA,EAAIjB,KAAKZ,OAAQ6B,IACzBrC,MAAMqC,EAAI1G,KAAKoE,MAAMC,MAAMF,OAAOnE,KAAKkG,eAClCd,WAAW/E,KAAM,CAACmE,IAAKxE,KAAKoE,MAAMC,MAAMG,IAAKL,OAAQE,MAAMqC,GAAIjB,KAAKiB,KAKrFV,IAAIK,UAAU2B,QAAU,kBACbhI,KAAKwB,OAAO2E,QAAQ2E,aAAa,IAAI1L,MAAMY,KAAKoE,MAAMC,MAAMG,IAAKxE,KAAKoE,MAAMC,MAAMF,OACjDnE,KAAKoE,MAAMK,IAAID,IAAKxE,KAAKoE,MAAMC,MAAMF,OAAOnE,KAAKsE,YAItF,CACHyG,YAAazL"}
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js b/amd/build/ui_scratchpad.min.js
index d6971232e..7567f02a2 100644
--- a/amd/build/ui_scratchpad.min.js
+++ b/amd/build/ui_scratchpad.min.js
@@ -1,3 +1,57 @@
-define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_templates,_userinterfacewrapper,_outputdisplayarea){var obj;function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}function _defineProperties(target,props){for(var i=0;iarr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0}))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}},{key:"getElement",value:function(){return this.outerDiv}},{key:"handleRunButtonClick",value:function(){if(null!==this.outputDisplay){this.sync();var answerCode,testCode,prefixAns,template,preloadString=this.textArea.value,serial=this.readJson(preloadString),code=(answerCode=serial.answer_code,testCode=serial.test_code,prefixAns=serial.prefix_ans[0],(template=this.runWrapper)||(template="{{ ANSWER_CODE }}\n{{ SCRATCHPAD_CODE }}"),prefixAns||(answerCode=""),(template=template.replaceAll("{{ ANSWER_CODE }}",answerCode)).replaceAll("{{ SCRATCHPAD_CODE }}",testCode));this.outputDisplay.runCode(code,"",!0)}}},{key:"updateContext",value:function(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}},{key:"readJson",value:function(preloadString){var serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch(_unused){serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}},{key:"reload",value:(fn=regeneratorRuntime.mark((function _callee(){var preloadString,preload,_yield$Templates$rend,html;return regeneratorRuntime.wrap((function(_context){for(;;)switch(_context.prev=_context.next){case 0:preloadString=this.textArea.value,_context.prev=1,preload=this.readJson(preloadString),_context.next=10;break;case 5:return _context.prev=5,_context.t0=_context.catch(1),this.fail=!0,this.failString="scratchpad_ui_invalidserialisation",_context.abrupt("return");case 10:return this.updateContext(preload),_context.prev=11,_context.next=14,_templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);case 14:_yield$Templates$rend=_context.sent,html=_yield$Templates$rend.html,this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners(),_context.next=26;break;case 22:_context.prev=22,_context.t1=_context.catch(11),this.fail=!0,this.failString="scratchpad_ui_templateloadfail";case 26:case"end":return _context.stop()}}),_callee,this,[[1,5],[11,22]])})),_reload=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(){return _reload.apply(this,arguments)})},{key:"drawUi",value:function(html){var wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}},{key:"addAceUis",value:function(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}},{key:"addEventListeners",value:function(){var _this=this,runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(function(){return _this.handleRunButtonClick()}))}},{key:"resize",value:function(){}},{key:"hasFocus",value:function(){var _this$answerCodeUi,_this$testCodeUi,focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}},{key:"destroy",value:function(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}],protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),ScratchpadUi}();_exports.Constructor=ScratchpadUi}));
+define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_templates,_userinterfacewrapper,_outputdisplayarea){var obj;
+/**
+ * Implementation of the scratchpad_ui user interface plugin. For overall details
+ * of the UI plugin architecture, see userinterfacewrapper.js.
+ *
+ * This plugin replaces the usual textarea answer element with a UI is designed to
+ * allow the execution of code in the CodeRunner question in a manner similar to an IDE.
+ * It contains two editor boxes, one on top of another, allowing users to enter and
+ * edit code in both. It contains two embedded Ace UIs.
+ * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,
+ * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,
+ * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that
+ * provides information about how to use the Scratchpad.
+ * It's possible to run code 'in-browser' by clicking the Run Button,
+ * without making a submission via the Check Button:
+ * If Prefix with Answer is not checked, only the code in the Scratchpad is run --
+ * allowing for a rough working spot to quickly check the result of code.
+ * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is
+ * appended to the code in the first editor before being run.
+ * The Run Button has some limitations when using its default configuration:
+ * Does not support programs that use STDIN (by default);
+ * Only supports textual STDOUT (by default).
+ * Note: These features can be supported, see the README section on wrappers...
+ * The serialisation of this UI is a JSON object with the fields
+ * with fields:
+ * answer_code: [""] A list containing a string with answer code from the first editor;
+ * test_code: [""] A list containing a string with containing answer code from the second editor;
+ * show_hide: ["1"] when scratchpad is visible, otherwise [""];
+ * prefix_ans: ["1"] when Prefix with Answer is checked, otherwise [""].
+ *
+ * UI Parameters:
+ * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.
+ * - button_name: run button text.
+ * - prefix_name: prefix with answer check-box label text.
+ * - help_text: help text to show.
+ * - run_lang: language used to run code when the run button is clicked,
+ * this should be the language your wrapper is written in (if applicable).
+ * - wrapper_src: location of wrapper code to be used by the run button, if applicable:
+ * setting to globalextra will use text in global extra field,
+ * - prototypeextra will use the prototype extra field.
+ * - output_display_mode: control how program output is displayed on runs, there are three modes:
+ * - text: display program output as text, html escaped;
+ * - json: display program output, when it is json,
+ * - html: display program output as raw html.
+ * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...
+ * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI
+ * from student perspective.
+ * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.
+ * This can be used to swap the default state.
+ *
+ * @module qtype_coderunner/ui_scratchpad
+ * @copyright Richard Lobb, 2022, The University of Canterbury
+ * @copyright James Napier, 2022, The University of Canterbury
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Constructor=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};const invertSerial=current=>"1"===current[0]?[""]:["1"],escapeRegExp=string=>string.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),overwriteValues=(defaults,prescribed)=>{let overwritten={...defaults};if(prescribed)for(const[key,value]of Object.entries(defaults))overwritten[key]=prescribed[key]||value;return overwritten};_exports.Constructor=class{constructor(textAreaId,width,height,uiParams){const DEF_UI_PARAMS={scratchpad_name:"",button_name:"",prefix_name:"",help_text:"",params:{},run_lang:uiParams.lang,output_display_mode:"text",disable_scratchpad:!1,wrapper_src:null,open_delimiter:"{|",close_delimiter:"|}",escape:!1};this.textArea=document.getElementById(textAreaId),this.textAreaId=textAreaId,this.height=height,this.readOnly=this.textArea.readonly,this.fail=!1,this.outerDiv=null,this.outputDisplay=null,this.invertPreload=uiParams.invert_prefix,this.lang=uiParams.lang,this.numRows=this.textArea.rows,this.uiParams=overwriteValues(DEF_UI_PARAMS,uiParams),this.runWrapper=this.getRunWrapper(),this.reload()}getRunWrapper(){const wrapperSrc=this.uiParams.wrapper_src;let runWrapper=null;return wrapperSrc&&("globalextra"===wrapperSrc||"prototypeextra"===wrapperSrc?runWrapper=this.textArea.dataset[wrapperSrc]:(this.fail=!0,this.failString="scratchpad_ui_badrunwrappersrc")),runWrapper}failed(){return this.fail}failMessage(){return this.failString}sync(){if(!this.context)return;const serialisation=this.getSerialisation();this.setSerialisation(serialisation)}getSerialisation(){const prefixAns=document.getElementById(this.context.prefix_ans.id),showHide=document.getElementById(this.context.show_hide.id);let serialisation={answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:[""]};return this.answerTextarea&&(serialisation.answer_code=[this.answerTextarea.value]),this.testTextarea&&(serialisation.test_code=[this.testTextarea.value]),showHide&&!(el=>{if(!el.classList.contains("collapse")&&!el.classList.contains("collapsing"))throw Error("Element does not have collapse class");return!el.classList.contains("show")})(showHide)&&(serialisation.show_hide=["1"]),(null!=prefixAns&&prefixAns.checked||this.context.disable_scratchpad)&&(serialisation.prefix_ans=["1"]),this.invertPreload&&(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans)),serialisation}setSerialisation(serialisation){serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),Object.values(serialisation).some((val=>1===val.length&&val[0].length>0))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}getElement(){return this.outerDiv}handleRunButtonClick(){if(null===this.outputDisplay)return;this.sync();const preloadString=this.textArea.value,serial=this.readJson(preloadString),escape=code=>this.uiParams.escape?JSON.stringify(code).slice(1,-1):code,code=function(answerCode,testCode,prefixAns,template){let open=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"\\(",close=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"\\)";template||(template="".concat(open," ANSWER_CODE ").concat(close,"\n")+"".concat(open," SCRATCHPAD_CODE ").concat(close)),prefixAns||(answerCode="");const escOpen=escapeRegExp(open),escClose=escapeRegExp(close),answerRegex=new RegExp("".concat(escOpen,"\\s*ANSWER_CODE\\s*").concat(escClose),"g"),scratchpadRegex=new RegExp("".concat(escOpen,"\\s*SCRATCHPAD_CODE\\s*").concat(escClose),"g");return(template=template.replaceAll(answerRegex,answerCode)).replaceAll(scratchpadRegex,testCode)}(escape(serial.answer_code[0]),escape(serial.test_code[0]),serial.prefix_ans[0],this.runWrapper,this.uiParams.open_delimiter,this.uiParams.close_delimiter);this.outputDisplay.runCode(code,"",!0)}updateContext(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}readJson(preloadString){let serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch{serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}async reload(){const preloadString=this.textArea.value;let preload;try{preload=this.readJson(preloadString)}catch(error){return this.fail=!0,void(this.failString="scratchpad_ui_invalidserialisation")}this.updateContext(preload);try{const{html:html}=await _templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners()}catch(e){this.fail=!0,this.failString="scratchpad_ui_templateloadfail"}}drawUi(html){const wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}addAceUis(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}addEventListeners(){const runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(()=>this.handleRunButtonClick()))}resize(){}hasFocus(){var _this$answerCodeUi,_this$testCodeUi;let focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}destroy(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}}));
//# sourceMappingURL=ui_scratchpad.min.js.map
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js.map b/amd/build/ui_scratchpad.min.js.map
index 92046f842..b2958e3cd 100644
--- a/amd/build/ui_scratchpad.min.js.map
+++ b/amd/build/ui_scratchpad.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text.\n * @param {string} testCode text.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template) => {\n if (!template) {\n template = '{{ ANSWER_CODE }}\\n' +\n '{{ SCRATCHPAD_CODE }}';\n }\n if (!prefixAns) {\n answerCode = '';\n }\n template = template.replaceAll('{{ ANSWER_CODE }}', answerCode);\n template = template.replaceAll('{{ SCRATCHPAD_CODE }}', testCode);\n return template;\n};\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the defualt value.\n * Does not add keys/values to the result if that key is not in defualts.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!el.classList.contains('collapse')) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n let serialisation = {\n answer_code: [''],\n test_code: [''],\n show_hide: [''],\n prefix_ans: ['']\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const code = fillWrapper(\n serial.answer_code,\n serial.test_code,\n serial.prefix_ans[0],\n this.runWrapper\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","overwriteValues","defaults","prescribed","overwritten","Object","entries","key","value","ScratchpadUi","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","reload","wrapperSrc","dataset","failString","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","test_code","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","checked","values","some","val","length","JSON","stringify","sync","answerCode","testCode","template","preloadString","serial","readJson","code","replaceAll","runCode","preload","text","render","CSS","escape","parse","hasOwnProperty","TypeError","updateContext","Templates","renderForPromise","html","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","_this","handleRunButtonClick","focused","_this$answerCodeUi","uiInstance","hasFocus","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text from first editor.\n * @param {string} testCode text from second editor.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @param {string} open delimiter to look for, e.g. '[['\n * @param {string} close delimiter to look for, e.g. ']]'\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template, open = '\\\\(', close = '\\\\)') => {\n if (!template) {\n template = `${open} ANSWER_CODE ${close}\\n` +\n `${open} SCRATCHPAD_CODE ${close}`;\n }\n if (!prefixAns) {\n answerCode = '';\n }\n const escOpen = escapeRegExp(open);\n const escClose = escapeRegExp(close);\n const answerRegex = new RegExp(`${escOpen}\\\\s*ANSWER_CODE\\\\s*${escClose}`, 'g');\n const scratchpadRegex = new RegExp(`${escOpen}\\\\s*SCRATCHPAD_CODE\\\\s*${escClose}`, 'g');\n template = template.replaceAll(answerRegex, answerCode);\n template = template.replaceAll(scratchpadRegex, testCode);\n return template;\n};\n\n/**\n * Escapes a string for use in regex.\n * @param {string} string to escape.\n * @returns {string} RegEx escaped string\n */\nconst escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"); // $& means the whole matched string\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the default value.\n * Does not add keys/values to the result if that key is not in defaults.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!(el.classList.contains('collapse') || el.classList.contains('collapsing'))) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null,\n open_delimiter: '{|',\n close_delimiter: '|}',\n escape: false\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n let serialisation = {\n answer_code: [''],\n test_code: [''],\n show_hide: [''],\n prefix_ans: ['']\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const escape = (code) => this.uiParams.escape ? JSON.stringify(code).slice(1, -1) : code;\n const answerCode = escape(serial.answer_code[0]);\n const testCode = escape(serial.test_code[0]);\n const code = fillWrapper(\n answerCode,\n testCode,\n serial.prefix_ans[0],\n this.runWrapper,\n this.uiParams.open_delimiter,\n this.uiParams.close_delimiter\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","escapeRegExp","string","replace","overwriteValues","defaults","prescribed","overwritten","key","value","Object","entries","constructor","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","open_delimiter","close_delimiter","escape","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","reload","wrapperSrc","dataset","failString","failed","failMessage","sync","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","test_code","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","checked","values","some","val","length","JSON","stringify","getElement","handleRunButtonClick","preloadString","serial","readJson","code","slice","answerCode","testCode","template","open","close","escOpen","escClose","answerRegex","RegExp","scratchpadRegex","replaceAll","fillWrapper","runCode","updateContext","preload","text","render","CSS","parse","hasOwnProperty","TypeError","error","html","Templates","renderForPromise","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","e","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","hasFocus","focused","_this$answerCodeUi","uiInstance","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/userinterfacewrapper.min.js b/amd/build/userinterfacewrapper.min.js
index 09ae46ac3..0825142de 100644
--- a/amd/build/userinterfacewrapper.min.js
+++ b/amd/build/userinterfacewrapper.min.js
@@ -96,6 +96,6 @@
* 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)
*
*****************************************************************************/
-define("qtype_coderunner/userinterfacewrapper",["jquery"],(function($){function InterfaceWrapper(uiname,textareaId){var t=this;this.GUTTER=14,this.DEFAULT_SYNC_INTERVAL_SECS=5;this.taId=textareaId,this.loadFailId=textareaId+"_loadfailerr";var ta=document.getElementById(textareaId);this.textArea=$(ta);var params=this.textArea.attr("data-params");this.uiParams=params?JSON.parse(params):{},this.uiParams.lang=this.textArea.attr("data-lang"),this.readOnly=this.textArea.prop("readonly"),this.isLoading=!1,this.loadFailed=!1,this.retries=0;var h=parseInt(this.textArea.css("height")),content_lines=this.textArea.val().split("\n").length,rows=ta.rows;content_lines>rows&&(rows=Math.min(content_lines,50)),h=Math.max(h,19*rows,50),this.wrapperNode=$(""),this.textArea.after(this.wrapperNode),this.wrapperNode.hide(),this.wrapperNode.css({resize:"vertical",overflow:"hidden",minHeight:h,width:"100%",border:"1px solid darkgrey"}),this.textArea.data("current-ui-wrapper",this),this.uiInstance=null,this.loadUi(uiname,this.uiParams),$(document).mousemove((function(){t.checkForResize()})),$(window).resize((function(){t.checkForResize()})),this.textArea.closest("form").submit((function(){null!==t.uiInstance&&t.uiInstance.sync()})),$(document.body).on("keydown",(function(e){77===e.keyCode&&e.ctrlKey&&e.altKey&&(null!==t.uiInstance||t.loadFailed?t.stop():t.restart())}))}return InterfaceWrapper.prototype.loadUi=function(uiname,params){var t=this;function syncIntervalSecsBase(){return params.hasOwnProperty("sync_interval_secs")?parseInt(params.sync_interval_secs):t.DEFAULT_SYNC_INTERVAL_SECS}if(this.isLoading)return this.retries+=1,void(this.retries>20?(alert("Failed to load "+uiname+" UI component. If this error persists, please report it to the forum on coderunner.org.nz"),this.retries=0,this.loading=0):setTimeout((function(){t.loadUi(uiname,params)}),200));this.retries=0,this.params=params,this.stop(),this.uiname=uiname,""===this.uiname||"none"===this.uiname||sessionStorage.getItem("disableUis")?this.uiInstance=null:(this.isLoading=!0,require(["qtype_coderunner/ui_"+this.uiname],(function(ui){var langString,errorDiv,h=t.wrapperNode.innerHeight()-t.GUTTER,w=t.wrapperNode.innerWidth(),uiInstance=new ui.Constructor(t.taId,w,h,params);if(uiInstance.failed()){t.loadFailed=!0,t.wrapperNode.hide(),uiInstance.destroy(),t.uiInstance=null,t.textArea.addClass("uiloadfailed");var loadFailDiv='',jqLoadFailDiv=$(loadFailDiv);jqLoadFailDiv.insertBefore(t.textArea),langString=uiInstance.failMessage(),errorDiv=jqLoadFailDiv,require(["core/str"],(function(str){var s=str.get_string(langString,"qtype_coderunner"),fallback=str.get_string("ui_fallback","qtype_coderunner");$.when(s,fallback).done((function(s,fallback){errorDiv.html(s+" "+fallback)}))}))}else{t.hLast=0,t.wLast=0,t.textArea.hide(),t.wrapperNode.show(),t.wrapperNode.append(uiInstance.getElement()),t.uiInstance=uiInstance,t.loadFailed=!1,t.checkForResize();var uiInstancePrototype=Object.getPrototypeOf(uiInstance);uiInstancePrototype.syncIntervalSecs=uiInstancePrototype.syncIntervalSecs||syncIntervalSecsBase,t.startSyncTimer(uiInstance)}t.isLoading=!1})))},InterfaceWrapper.prototype.startSyncTimer=function(uiInstance){var timeout=uiInstance.syncIntervalSecs();this.uiInstance.timer=timeout?setInterval((function(){uiInstance.sync()}),1e3*timeout):null},InterfaceWrapper.prototype.stopSyncTimer=function(uiInstance){uiInstance.timer&&clearTimeout(uiInstance.timer)},InterfaceWrapper.prototype.stop=function(){null!==this.uiInstance&&(this.stopSyncTimer(this.uiInstance),this.textArea.show(),this.uiInstance.hasFocus()&&(this.textArea.focus(),this.textArea[0].selectionStart=this.textArea[0].value.length),this.uiInstance.destroy(),this.uiInstance=null,this.wrapperNode.hide()),this.loadFailed=!1,this.textArea.removeClass("uiloadfailed"),$(document.getElementById(this.loadFailId)).remove()},InterfaceWrapper.prototype.restart=function(){null===this.uiInstance&&this.loadUi(this.uiname,this.params)},InterfaceWrapper.prototype.checkForResize=function(){if(this.uiInstance){var h=this.wrapperNode.innerHeight(),w=this.wrapperNode.innerWidth();if(h!=this.hLast||w!=this.wLast){var xLeft=this.wrapperNode.offset().left,maxWidth=$(window).innerWidth()-xLeft-25,hAdjusted=h-this.GUTTER,wAdjusted=Math.min(maxWidth,w);this.uiInstance.resize(wAdjusted,hAdjusted),this.hLast=this.wrapperNode.innerHeight(),this.wLast=this.wrapperNode.innerWidth()}}},{newUiWrapper:function(uiname,textareaId){return uiname?new InterfaceWrapper(uiname,textareaId):null},InterfaceWrapper:InterfaceWrapper}}));
+define("qtype_coderunner/userinterfacewrapper",["jquery"],(function($){function InterfaceWrapper(uiname,textareaId){let t=this;this.GUTTER=14,this.DEFAULT_SYNC_INTERVAL_SECS=5;this.taId=textareaId,this.loadFailId=textareaId+"_loadfailerr";const ta=document.getElementById(textareaId);this.textArea=$(ta);const params=this.textArea.attr("data-params");this.uiParams=params?JSON.parse(params):{},this.uiParams.lang=this.textArea.attr("data-lang"),this.readOnly=this.textArea.prop("readonly"),this.isLoading=!1,this.loadFailed=!1,this.retries=0;let h=parseInt(this.textArea.css("height")),content_lines=this.textArea.val().split("\n").length,rows=ta.rows;content_lines>rows&&(rows=Math.min(content_lines,50)),h=Math.max(h,19*rows,50),this.wrapperNode=$(""),this.textArea.after(this.wrapperNode),this.wrapperNode.hide(),this.wrapperNode.css({resize:"vertical",overflow:"hidden",minHeight:h,width:"100%",border:"1px solid darkgrey"}),this.textArea.data("current-ui-wrapper",this),this.uiInstance=null,this.loadUi(uiname,this.uiParams),$(document).mousemove((function(){t.checkForResize()})),$(window).resize((function(){t.checkForResize()})),this.textArea.closest("form").submit((function(){null!==t.uiInstance&&t.uiInstance.sync()})),$(document.body).on("keydown",(function(e){77===e.keyCode&&e.ctrlKey&&e.altKey&&(null!==t.uiInstance||t.loadFailed?t.stop():t.restart())}))}return InterfaceWrapper.prototype.loadUi=function(uiname,params){const t=this;function syncIntervalSecsBase(){return params.hasOwnProperty("sync_interval_secs")?parseInt(params.sync_interval_secs):t.DEFAULT_SYNC_INTERVAL_SECS}if(this.isLoading)return this.retries+=1,void(this.retries>20?(alert("Failed to load "+uiname+" UI component. If this error persists, please report it to the forum on coderunner.org.nz"),this.retries=0,this.loading=0):setTimeout((function(){t.loadUi(uiname,params)}),200));this.retries=0,this.params=params,this.stop(),this.uiname=uiname,""===this.uiname||"none"===this.uiname||sessionStorage.getItem("disableUis")?this.uiInstance=null:(this.isLoading=!0,require(["qtype_coderunner/ui_"+this.uiname],(function(ui){const h=t.wrapperNode.innerHeight()-t.GUTTER,w=t.wrapperNode.innerWidth(),uiInstance=new ui.Constructor(t.taId,w,h,params);if(uiInstance.failed()){t.loadFailed=!0,t.wrapperNode.hide(),uiInstance.destroy(),t.uiInstance=null,t.textArea.addClass("uiloadfailed");const loadFailDiv='';let jqLoadFailDiv=$(loadFailDiv);jqLoadFailDiv.insertBefore(t.textArea),langString=uiInstance.failMessage(),errorDiv=jqLoadFailDiv,require(["core/str"],(function(str){const s=str.get_string(langString,"qtype_coderunner"),fallback=str.get_string("ui_fallback","qtype_coderunner");$.when(s,fallback).done((function(s,fallback){errorDiv.html(s+" "+fallback)}))}))}else{t.hLast=0,t.wLast=0,t.textArea.hide(),t.wrapperNode.show(),t.wrapperNode.append(uiInstance.getElement()),t.uiInstance=uiInstance,t.loadFailed=!1,t.checkForResize();let uiInstancePrototype=Object.getPrototypeOf(uiInstance);uiInstancePrototype.syncIntervalSecs=uiInstancePrototype.syncIntervalSecs||syncIntervalSecsBase,t.startSyncTimer(uiInstance)}var langString,errorDiv;t.isLoading=!1})))},InterfaceWrapper.prototype.startSyncTimer=function(uiInstance){const timeout=uiInstance.syncIntervalSecs();this.uiInstance.timer=timeout?setInterval((function(){uiInstance.sync()}),1e3*timeout):null},InterfaceWrapper.prototype.stopSyncTimer=function(uiInstance){uiInstance.timer&&clearTimeout(uiInstance.timer)},InterfaceWrapper.prototype.stop=function(){null!==this.uiInstance&&(this.stopSyncTimer(this.uiInstance),this.textArea.show(),this.uiInstance.hasFocus()&&(this.textArea.focus(),this.textArea[0].selectionStart=this.textArea[0].value.length),this.uiInstance.destroy(),this.uiInstance=null,this.wrapperNode.hide()),this.loadFailed=!1,this.textArea.removeClass("uiloadfailed"),$(document.getElementById(this.loadFailId)).remove()},InterfaceWrapper.prototype.restart=function(){null===this.uiInstance&&this.loadUi(this.uiname,this.params)},InterfaceWrapper.prototype.checkForResize=function(){if(this.uiInstance){const h=this.wrapperNode.innerHeight(),w=this.wrapperNode.innerWidth();if(h!=this.hLast||w!=this.wLast){const xLeft=this.wrapperNode.offset().left,maxWidth=$(window).innerWidth()-xLeft-25,hAdjusted=h-this.GUTTER,wAdjusted=Math.min(maxWidth,w);this.uiInstance.resize(wAdjusted,hAdjusted),this.hLast=this.wrapperNode.innerHeight(),this.wLast=this.wrapperNode.innerWidth()}}},{newUiWrapper:function(uiname,textareaId){return uiname?new InterfaceWrapper(uiname,textareaId):null},InterfaceWrapper:InterfaceWrapper}}));
//# sourceMappingURL=userinterfacewrapper.min.js.map
\ No newline at end of file
diff --git a/amd/build/userinterfacewrapper.min.js.map b/amd/build/userinterfacewrapper.min.js.map
index 631c835e8..f46f952fc 100644
--- a/amd/build/userinterfacewrapper.min.js.map
+++ b/amd/build/userinterfacewrapper.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"userinterfacewrapper.min.js","sources":["../src/userinterfacewrapper.js"],"sourcesContent":["/******************************************************************************\n *\n * This module provides a wrapper for user-interface modules, handling hiding\n * of the textArea that is being replaced by the UI element,\n * resizing of the UI component, and support of such usability functions as\n * ctrl-alt-M to disable/re-enable the entire user interface, including the\n * wrapper.\n *\n * @module coderunner/userinterfacewrapper\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * The InterfaceWrapper class is constructed either by Moodle PHP calls of\n * the form\n *\n * $PAGE->requires->js_call_amd($modulename, $functionname, $params)\n *\n * (e.g. from within render.php) or by JavaScript require calls, e.g. from\n * authorform.js when the question author changes UI type.\n *\n * The InterfaceWrapper provides:\n *\n * 1. A constructor InterfaceWrapper(uiname, textareaId) which\n * hides the given text area, replaces it with a wrapper div (resizable in\n * height by the user but with width resizing managed by changes in window\n * width), created an instance of nameInstance as defined in the file\n * ui_name.js (see below).\n * params is a record containing the decoded value of\n *\n * 2. A stop() method that destroys the embedded UI and hides the wrapper.\n *\n * 3. A restart() method that shows the wrapper again and re-creates the prior\n * embedded UI component within it.\n *\n * 4. A loadUi(uiname, params) method that kills any currently running UI element\n * (if there is one) and (re)loads the specified one. The params parameter\n * is a record that allows additional parameters to be passed in, such as\n * those from the question's uiParams field and, in the case of the\n * Ace UI, the 'lang' (language) that the editor is editing. This data\n * is supplied by the PHP via the data-params attribute of the answer's\n * base textarea.\n *\n * 5. Regular checking for any resizing of the wrapper, which are passed on to\n * the embedded UI element's resize() method.\n *\n * 6. Monitoring of alt-ctrl-M key presses which toggle the visibility of the\n * wrapper plus UI element and the syncronised textArea by calls to stop()\n * and restart\n *\n * =========================================================================\n *\n * The embedded user-interface module must be defined in a JavaScript file\n * of the form ui_name.js which must define a class nameInstance with\n * the following functionality:\n *\n * 1. A constructor SomeUiName(textareaId, width, height, params) that\n * builds an HTML component of the given width and height. textareaId is the\n * ID of the textArea from which the UI element should obtain its initial\n * serialisation and to which it should write the serialisation when its save\n * or destroy methods are called. params is a JavaScript object,\n * decoded from the JSON uiParams defined by the question plus any\n * additional data required, such as the 'lang' in the case of Ace.\n *\n * 2. A getElement() method that returns the HTML element that the\n * InterfaceWrapper is to insert into the document tree.\n *\n * 3. A method failed() that should return true unless the constructor\n * failed (e.g. because it was not able to de-serialise the text area's\n * contents). The wrapper will call destroy() on the object if failed()\n * returns true and abort the use of the UI element. The text area will\n * have the uiloadfailed class added, which CSS will display in some\n * error mode (e.g. a red border).\n *\n * 4. A method failMessage() that will be called only when failed() returns\n * True. It should be a defined CodeRunner language string key.\n *\n * 5. A sync() method that copies the serialised represention of the UI plugin's\n * data to the related TextArea. This is used when submit is clicked.\n *\n * 6. A destroy() method that should sync the contents to the text area then\n * destroy any HTML elements or other created content. This method is called\n * when CTRL-ALT-M is typed by the user to turn off all UI plugins\n *\n * 7. A resize(width, height) method that should resize the entire UI element\n * to the given dimensions.\n *\n * 8. A hasFocus() method that returns true if the UI element has focus.\n *\n * 9. A syncIntervalSecs() method that returns the time interval between\n * calls to the sync() method. 0 for no sync calls. The userinterfacewrapper\n * provides all instances with a generic (base-class) version that returns\n * the value of a UI parameter sync_interval_secs if given else uses the\n * UI interface wrapper default (currently 10).\n *\n * The return value from the module define is a record with a single field\n * 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)\n *\n *****************************************************************************/\n\n/**\n * 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 util.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\ndefine(['jquery'], function($) {\n /**\n * Constructor for a new user interface.\n * @param {string} uiname The name of the interface element (e.g. ace, graph, etc)\n * which should be in file ui_ace.js, ui_graph.js etc.\n * @param {string} textareaId The id of the text area that the UI is to manage.\n * The text area should have an attribute data-params, which is a\n * JSON encoded record containing whatever additional parameters might\n * be needed by the User interface. As a minimum it should contain all\n * the parameters from the uiparameters field of\n * the question so that question authors can pass in additional data\n * such as whether graph edges are bidirectional or not in the case of\n * the graph UI. Additionally the Ace editor requires a 'lang' field\n * to specify what language the editor is editing.\n * When the wrapper has been set up on a text area, the text area's\n * data attribute contains an entry for 'current-ui-wrapper' that is\n * a reference to the wrapper ('this').\n */\n function InterfaceWrapper(uiname, textareaId) {\n let t = this; // For use by embedded functions.\n\n this.GUTTER = 14; // Size of gutter at base of wrapper Node (pixels)\n this.DEFAULT_SYNC_INTERVAL_SECS = 5;\n\n const PIXELS_PER_ROW = 19; // For estimating height of textareas.\n const MAX_GROWN_ROWS = 50; // Upper limit to artifically grown textarea rows.\n const MIN_WRAPPER_HEIGHT = 50;\n\n this.taId = textareaId;\n this.loadFailId = textareaId + '_loadfailerr';\n const ta = document.getElementById(textareaId);\n this.textArea = $(ta);\n const params = this.textArea.attr('data-params');\n if (params) {\n this.uiParams = JSON.parse(params);\n } else {\n this.uiParams = {};\n }\n this.uiParams.lang = this.textArea.attr('data-lang');\n this.readOnly = this.textArea.prop('readonly');\n this.isLoading = false; // True if we're busy loading a UI element.\n this.loadFailed = false; // True if UI failed to initialise properly.\n this.retries = 0; // Number of failed attempts to load a UI component.\n\n let h = parseInt(this.textArea.css(\"height\"));\n let content_lines = this.textArea.val().split('\\n').length;\n let rows = ta.rows;\n if (content_lines > rows) {\n // Allow reloaded text areas with lots of text to grow bigger, within limits.\n rows = Math.min(content_lines, MAX_GROWN_ROWS);\n }\n h = Math.max(h, rows * PIXELS_PER_ROW, MIN_WRAPPER_HEIGHT);\n\n /**\n * Construct an empty hidden wrapper div, inserted directly after the\n * textArea, ready to contain the actual UI.\n */\n this.wrapperNode = $(\"\");\n this.textArea.after(this.wrapperNode);\n this.wrapperNode.hide();\n this.wrapperNode.css({\n resize: 'vertical',\n overflow: 'hidden',\n minHeight: h,\n width: \"100%\",\n border: \"1px solid darkgrey\"\n });\n\n /**\n * Record a reference to this wrapper in the text area's data attribute\n * for use by external javascript that needs to interact with the\n * wrapper, e.g. the multilanguage.js module.\n */\n this.textArea.data('current-ui-wrapper', this);\n\n /**\n * Load the UI into the wrapper (aysnchronous).\n */\n this.uiInstance = null; // Defined by loadUi asynchronously\n this.loadUi(uiname, this.uiParams); // Load the required UI element\n\n /**\n * Add event handlers\n */\n $(document).mousemove(function() {\n t.checkForResize();\n });\n $(window).resize(function() {\n t.checkForResize();\n });\n this.textArea.closest('form').submit(function() {\n if (t.uiInstance !== null) {\n t.uiInstance.sync();\n }\n });\n $(document.body).on('keydown', function(e) {\n const KEY_M = 77;\n if (e.keyCode === KEY_M && e.ctrlKey && e.altKey) {\n if (t.uiInstance !== null || t.loadFailed) {\n t.stop();\n } else {\n t.restart(); // Reactivate\n }\n }\n });\n }\n\n /**\n * Load the specified UI element (which in the case of Ace will need\n * to know the language, lang, as well - this must be supplied as\n * a 'lang' attribute of the record params.\n * When ui is up and running, this.uiInstance will reference it.\n * To avoid a potential race problem, if this method is already busy\n * with a load, try again in 200 msecs.\n * @param {string} uiname The name of the User Interface to be used.\n * @param {object} params The UI parameters object that passes parameters\n * to the actual UI object.\n */\n InterfaceWrapper.prototype.loadUi = function(uiname, params) {\n const t = this,\n errPart1 = 'Failed to load ',\n errPart2 = ' UI component. If this error persists, please report it to the forum on coderunner.org.nz';\n\n /**\n * Get the given language string and plug it into the given jQuery\n * div element as its html, plus a 'fallback' message on a separate line.\n * @param {string} langString The language string specifier for the error message,\n * to be loaded by AJAX.\n * @param {object} errorDiv The div object into which the error message\n * is to be inserted.\n */\n function setLoadFailMessage(langString, errorDiv) {\n require(['core/str'], function(str) {\n /**\n * Get langString text via AJAX\n */\n const\n s = str.get_string(langString, 'qtype_coderunner'),\n fallback = str.get_string('ui_fallback', 'qtype_coderunner');\n $.when(s, fallback).done(function(s, fallback) {\n errorDiv.html(s + ' ' + fallback);\n });\n });\n }\n\n /**\n * The default method for a UIs sync_interval_secs method.\n * Returns the sync_interval_secs parameter if given, else\n * DEFAULT_SYNC_INTERVAL_SECS.\n */\n function syncIntervalSecsBase() {\n if (params.hasOwnProperty('sync_interval_secs')) {\n return parseInt(params.sync_interval_secs);\n } else {\n return t.DEFAULT_SYNC_INTERVAL_SECS;\n }\n }\n\n if (this.isLoading) { // Oops, we're loading a UI element already\n this.retries += 1;\n if (this.retries > 20) {\n alert(errPart1 + uiname + errPart2);\n this.retries = 0;\n this.loading = 0;\n } else {\n setTimeout(function() {\n t.loadUi(uiname, params);\n }, 200); // Try again in 200 msecs\n }\n return;\n }\n this.retries = 0;\n this.params = params; // Save in case need to restart\n\n this.stop(); // Kill any active UI first\n this.uiname = uiname;\n\n if (this.uiname === '' || this.uiname === 'none' || sessionStorage.getItem('disableUis')) {\n this.uiInstance = null;\n } else {\n this.isLoading = true;\n require(['qtype_coderunner/ui_' + this.uiname],\n function(ui) {\n const h = t.wrapperNode.innerHeight() - t.GUTTER;\n const w = t.wrapperNode.innerWidth();\n const uiInstance = new ui.Constructor(t.taId, w, h, params);\n if (uiInstance.failed()) {\n /*\n * Constructor failed to load serialisation.\n * Set uiloadfailed class on text area.\n */\n t.loadFailed = true;\n t.wrapperNode.hide();\n uiInstance.destroy();\n t.uiInstance = null;\n t.textArea.addClass('uiloadfailed');\n const loadFailDiv = '';\n let jqLoadFailDiv = $(loadFailDiv);\n jqLoadFailDiv.insertBefore(t.textArea);\n setLoadFailMessage(uiInstance.failMessage(), jqLoadFailDiv); // Insert error by AJAX\n } else {\n t.hLast = 0; // Force resize (and hence redraw)\n t.wLast = 0; // ... on first call to checkForResize\n t.textArea.hide();\n t.wrapperNode.show();\n t.wrapperNode.append(uiInstance.getElement());\n t.uiInstance = uiInstance;\n t.loadFailed = false;\n t.checkForResize();\n\n /*\n * Set a default syncIntervalSecs method if uiInstance lacks one.\n */\n let uiInstancePrototype = Object.getPrototypeOf(uiInstance);\n uiInstancePrototype.syncIntervalSecs = uiInstancePrototype.syncIntervalSecs || syncIntervalSecsBase;\n t.startSyncTimer(uiInstance);\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Start a sync timer on the given uiInstance, unless its time interval is 0.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.startSyncTimer = function(uiInstance) {\n const timeout = uiInstance.syncIntervalSecs();\n if (timeout) {\n this.uiInstance.timer = setInterval(function () {\n uiInstance.sync();\n }, timeout * 1000);\n } else {\n this.uiInstance.timer = null;\n }\n };\n\n\n /**\n * Stop the sync timer on the given uiInstance, if running.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.stopSyncTimer = function(uiInstance) {\n if (uiInstance.timer) {\n clearTimeout(uiInstance.timer);\n }\n };\n\n\n InterfaceWrapper.prototype.stop = function() {\n /*\n * Disable (shutdown) the embedded ui component.\n * The wrapper remains active for ctrl-alt-M events, but is hidden.\n */\n if (this.uiInstance !== null) {\n this.stopSyncTimer(this.uiInstance);\n this.textArea.show();\n if (this.uiInstance.hasFocus()) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n this.uiInstance.destroy();\n this.uiInstance = null;\n this.wrapperNode.hide();\n }\n this.loadFailed = false;\n this.textArea.removeClass('uiloadfailed'); // Just in case it failed before\n $(document.getElementById(this.loadFailId)).remove();\n };\n\n /*\n * Re-enable the ui element (e.g. after alt-cntrl-M). This is\n * a full re-initialisation of the ui element.\n */\n InterfaceWrapper.prototype.restart = function() {\n if (this.uiInstance === null) {\n /**\n * Restart the UI component in the textarea\n */\n this.loadUi(this.uiname, this.params);\n }\n };\n\n\n /**\n * Check for wrapper resize - propagate to ui element.\n */\n InterfaceWrapper.prototype.checkForResize = function() {\n const SIZE_HACK = 25; // Horrible but best I can do. TODO: FIXME\n\n if (this.uiInstance) {\n const h = this.wrapperNode.innerHeight();\n const w = this.wrapperNode.innerWidth();\n if (h != this.hLast || w != this.wLast) {\n const xLeft = this.wrapperNode.offset().left;\n const maxWidth = $(window).innerWidth() - xLeft - SIZE_HACK;\n const hAdjusted = h - this.GUTTER;\n const wAdjusted = Math.min(maxWidth, w);\n this.uiInstance.resize(wAdjusted, hAdjusted);\n this.hLast = this.wrapperNode.innerHeight();\n this.wLast = this.wrapperNode.innerWidth();\n }\n }\n };\n\n /**\n * The external entry point from the PHP.\n * @param {string} uiname The name of the User Interface to use e.g. 'ace'\n * @param {string} textareaId The ID of the textarea to be wrapped.\n */\n function newUiWrapper(uiname, textareaId) {\n if (uiname) {\n return new InterfaceWrapper(uiname, textareaId);\n } else {\n return null;\n }\n }\n\n\n return {\n newUiWrapper: newUiWrapper,\n InterfaceWrapper: InterfaceWrapper\n };\n});\n"],"names":["define","$","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","taId","loadFailId","ta","document","getElementById","textArea","params","attr","uiParams","JSON","parse","lang","readOnly","prop","isLoading","loadFailed","retries","h","parseInt","css","content_lines","val","split","length","rows","Math","min","max","wrapperNode","after","hide","resize","overflow","minHeight","width","border","data","uiInstance","loadUi","mousemove","checkForResize","window","closest","submit","sync","body","on","e","keyCode","ctrlKey","altKey","stop","restart","prototype","syncIntervalSecsBase","hasOwnProperty","sync_interval_secs","alert","loading","setTimeout","sessionStorage","getItem","require","ui","langString","errorDiv","innerHeight","w","innerWidth","Constructor","failed","destroy","addClass","loadFailDiv","jqLoadFailDiv","insertBefore","failMessage","str","s","get_string","fallback","when","done","html","hLast","wLast","show","append","getElement","uiInstancePrototype","Object","getPrototypeOf","syncIntervalSecs","startSyncTimer","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","remove","xLeft","offset","left","maxWidth","hAdjusted","wAdjusted","newUiWrapper"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"userinterfacewrapper.min.js","sources":["../src/userinterfacewrapper.js"],"sourcesContent":["/******************************************************************************\n *\n * This module provides a wrapper for user-interface modules, handling hiding\n * of the textArea that is being replaced by the UI element,\n * resizing of the UI component, and support of such usability functions as\n * ctrl-alt-M to disable/re-enable the entire user interface, including the\n * wrapper.\n *\n * @module coderunner/userinterfacewrapper\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * The InterfaceWrapper class is constructed either by Moodle PHP calls of\n * the form\n *\n * $PAGE->requires->js_call_amd($modulename, $functionname, $params)\n *\n * (e.g. from within render.php) or by JavaScript require calls, e.g. from\n * authorform.js when the question author changes UI type.\n *\n * The InterfaceWrapper provides:\n *\n * 1. A constructor InterfaceWrapper(uiname, textareaId) which\n * hides the given text area, replaces it with a wrapper div (resizable in\n * height by the user but with width resizing managed by changes in window\n * width), created an instance of nameInstance as defined in the file\n * ui_name.js (see below).\n * params is a record containing the decoded value of\n *\n * 2. A stop() method that destroys the embedded UI and hides the wrapper.\n *\n * 3. A restart() method that shows the wrapper again and re-creates the prior\n * embedded UI component within it.\n *\n * 4. A loadUi(uiname, params) method that kills any currently running UI element\n * (if there is one) and (re)loads the specified one. The params parameter\n * is a record that allows additional parameters to be passed in, such as\n * those from the question's uiParams field and, in the case of the\n * Ace UI, the 'lang' (language) that the editor is editing. This data\n * is supplied by the PHP via the data-params attribute of the answer's\n * base textarea.\n *\n * 5. Regular checking for any resizing of the wrapper, which are passed on to\n * the embedded UI element's resize() method.\n *\n * 6. Monitoring of alt-ctrl-M key presses which toggle the visibility of the\n * wrapper plus UI element and the syncronised textArea by calls to stop()\n * and restart\n *\n * =========================================================================\n *\n * The embedded user-interface module must be defined in a JavaScript file\n * of the form ui_name.js which must define a class nameInstance with\n * the following functionality:\n *\n * 1. A constructor SomeUiName(textareaId, width, height, params) that\n * builds an HTML component of the given width and height. textareaId is the\n * ID of the textArea from which the UI element should obtain its initial\n * serialisation and to which it should write the serialisation when its save\n * or destroy methods are called. params is a JavaScript object,\n * decoded from the JSON uiParams defined by the question plus any\n * additional data required, such as the 'lang' in the case of Ace.\n *\n * 2. A getElement() method that returns the HTML element that the\n * InterfaceWrapper is to insert into the document tree.\n *\n * 3. A method failed() that should return true unless the constructor\n * failed (e.g. because it was not able to de-serialise the text area's\n * contents). The wrapper will call destroy() on the object if failed()\n * returns true and abort the use of the UI element. The text area will\n * have the uiloadfailed class added, which CSS will display in some\n * error mode (e.g. a red border).\n *\n * 4. A method failMessage() that will be called only when failed() returns\n * True. It should be a defined CodeRunner language string key.\n *\n * 5. A sync() method that copies the serialised represention of the UI plugin's\n * data to the related TextArea. This is used when submit is clicked.\n *\n * 6. A destroy() method that should sync the contents to the text area then\n * destroy any HTML elements or other created content. This method is called\n * when CTRL-ALT-M is typed by the user to turn off all UI plugins\n *\n * 7. A resize(width, height) method that should resize the entire UI element\n * to the given dimensions.\n *\n * 8. A hasFocus() method that returns true if the UI element has focus.\n *\n * 9. A syncIntervalSecs() method that returns the time interval between\n * calls to the sync() method. 0 for no sync calls. The userinterfacewrapper\n * provides all instances with a generic (base-class) version that returns\n * the value of a UI parameter sync_interval_secs if given else uses the\n * UI interface wrapper default (currently 10).\n *\n * The return value from the module define is a record with a single field\n * 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)\n *\n *****************************************************************************/\n\n/**\n * 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 util.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\ndefine(['jquery'], function($) {\n /**\n * Constructor for a new user interface.\n * @param {string} uiname The name of the interface element (e.g. ace, graph, etc)\n * which should be in file ui_ace.js, ui_graph.js etc.\n * @param {string} textareaId The id of the text area that the UI is to manage.\n * The text area should have an attribute data-params, which is a\n * JSON encoded record containing whatever additional parameters might\n * be needed by the User interface. As a minimum it should contain all\n * the parameters from the uiparameters field of\n * the question so that question authors can pass in additional data\n * such as whether graph edges are bidirectional or not in the case of\n * the graph UI. Additionally the Ace editor requires a 'lang' field\n * to specify what language the editor is editing.\n * When the wrapper has been set up on a text area, the text area's\n * data attribute contains an entry for 'current-ui-wrapper' that is\n * a reference to the wrapper ('this').\n */\n function InterfaceWrapper(uiname, textareaId) {\n let t = this; // For use by embedded functions.\n\n this.GUTTER = 14; // Size of gutter at base of wrapper Node (pixels)\n this.DEFAULT_SYNC_INTERVAL_SECS = 5;\n\n const PIXELS_PER_ROW = 19; // For estimating height of textareas.\n const MAX_GROWN_ROWS = 50; // Upper limit to artifically grown textarea rows.\n const MIN_WRAPPER_HEIGHT = 50;\n\n this.taId = textareaId;\n this.loadFailId = textareaId + '_loadfailerr';\n const ta = document.getElementById(textareaId);\n this.textArea = $(ta);\n const params = this.textArea.attr('data-params');\n if (params) {\n this.uiParams = JSON.parse(params);\n } else {\n this.uiParams = {};\n }\n this.uiParams.lang = this.textArea.attr('data-lang');\n this.readOnly = this.textArea.prop('readonly');\n this.isLoading = false; // True if we're busy loading a UI element.\n this.loadFailed = false; // True if UI failed to initialise properly.\n this.retries = 0; // Number of failed attempts to load a UI component.\n\n let h = parseInt(this.textArea.css(\"height\"));\n let content_lines = this.textArea.val().split('\\n').length;\n let rows = ta.rows;\n if (content_lines > rows) {\n // Allow reloaded text areas with lots of text to grow bigger, within limits.\n rows = Math.min(content_lines, MAX_GROWN_ROWS);\n }\n h = Math.max(h, rows * PIXELS_PER_ROW, MIN_WRAPPER_HEIGHT);\n\n /**\n * Construct an empty hidden wrapper div, inserted directly after the\n * textArea, ready to contain the actual UI.\n */\n this.wrapperNode = $(\"\");\n this.textArea.after(this.wrapperNode);\n this.wrapperNode.hide();\n this.wrapperNode.css({\n resize: 'vertical',\n overflow: 'hidden',\n minHeight: h,\n width: \"100%\",\n border: \"1px solid darkgrey\"\n });\n\n /**\n * Record a reference to this wrapper in the text area's data attribute\n * for use by external javascript that needs to interact with the\n * wrapper, e.g. the multilanguage.js module.\n */\n this.textArea.data('current-ui-wrapper', this);\n\n /**\n * Load the UI into the wrapper (aysnchronous).\n */\n this.uiInstance = null; // Defined by loadUi asynchronously\n this.loadUi(uiname, this.uiParams); // Load the required UI element\n\n /**\n * Add event handlers\n */\n $(document).mousemove(function() {\n t.checkForResize();\n });\n $(window).resize(function() {\n t.checkForResize();\n });\n this.textArea.closest('form').submit(function() {\n if (t.uiInstance !== null) {\n t.uiInstance.sync();\n }\n });\n $(document.body).on('keydown', function(e) {\n const KEY_M = 77;\n if (e.keyCode === KEY_M && e.ctrlKey && e.altKey) {\n if (t.uiInstance !== null || t.loadFailed) {\n t.stop();\n } else {\n t.restart(); // Reactivate\n }\n }\n });\n }\n\n /**\n * Load the specified UI element (which in the case of Ace will need\n * to know the language, lang, as well - this must be supplied as\n * a 'lang' attribute of the record params.\n * When ui is up and running, this.uiInstance will reference it.\n * To avoid a potential race problem, if this method is already busy\n * with a load, try again in 200 msecs.\n * @param {string} uiname The name of the User Interface to be used.\n * @param {object} params The UI parameters object that passes parameters\n * to the actual UI object.\n */\n InterfaceWrapper.prototype.loadUi = function(uiname, params) {\n const t = this,\n errPart1 = 'Failed to load ',\n errPart2 = ' UI component. If this error persists, please report it to the forum on coderunner.org.nz';\n\n /**\n * Get the given language string and plug it into the given jQuery\n * div element as its html, plus a 'fallback' message on a separate line.\n * @param {string} langString The language string specifier for the error message,\n * to be loaded by AJAX.\n * @param {object} errorDiv The div object into which the error message\n * is to be inserted.\n */\n function setLoadFailMessage(langString, errorDiv) {\n require(['core/str'], function(str) {\n /**\n * Get langString text via AJAX\n */\n const\n s = str.get_string(langString, 'qtype_coderunner'),\n fallback = str.get_string('ui_fallback', 'qtype_coderunner');\n $.when(s, fallback).done(function(s, fallback) {\n errorDiv.html(s + ' ' + fallback);\n });\n });\n }\n\n /**\n * The default method for a UIs sync_interval_secs method.\n * Returns the sync_interval_secs parameter if given, else\n * DEFAULT_SYNC_INTERVAL_SECS.\n */\n function syncIntervalSecsBase() {\n if (params.hasOwnProperty('sync_interval_secs')) {\n return parseInt(params.sync_interval_secs);\n } else {\n return t.DEFAULT_SYNC_INTERVAL_SECS;\n }\n }\n\n if (this.isLoading) { // Oops, we're loading a UI element already\n this.retries += 1;\n if (this.retries > 20) {\n alert(errPart1 + uiname + errPart2);\n this.retries = 0;\n this.loading = 0;\n } else {\n setTimeout(function() {\n t.loadUi(uiname, params);\n }, 200); // Try again in 200 msecs\n }\n return;\n }\n this.retries = 0;\n this.params = params; // Save in case need to restart\n\n this.stop(); // Kill any active UI first\n this.uiname = uiname;\n\n if (this.uiname === '' || this.uiname === 'none' || sessionStorage.getItem('disableUis')) {\n this.uiInstance = null;\n } else {\n this.isLoading = true;\n require(['qtype_coderunner/ui_' + this.uiname],\n function(ui) {\n const h = t.wrapperNode.innerHeight() - t.GUTTER;\n const w = t.wrapperNode.innerWidth();\n const uiInstance = new ui.Constructor(t.taId, w, h, params);\n if (uiInstance.failed()) {\n /*\n * Constructor failed to load serialisation.\n * Set uiloadfailed class on text area.\n */\n t.loadFailed = true;\n t.wrapperNode.hide();\n uiInstance.destroy();\n t.uiInstance = null;\n t.textArea.addClass('uiloadfailed');\n const loadFailDiv = '';\n let jqLoadFailDiv = $(loadFailDiv);\n jqLoadFailDiv.insertBefore(t.textArea);\n setLoadFailMessage(uiInstance.failMessage(), jqLoadFailDiv); // Insert error by AJAX\n } else {\n t.hLast = 0; // Force resize (and hence redraw)\n t.wLast = 0; // ... on first call to checkForResize\n t.textArea.hide();\n t.wrapperNode.show();\n t.wrapperNode.append(uiInstance.getElement());\n t.uiInstance = uiInstance;\n t.loadFailed = false;\n t.checkForResize();\n\n /*\n * Set a default syncIntervalSecs method if uiInstance lacks one.\n */\n let uiInstancePrototype = Object.getPrototypeOf(uiInstance);\n uiInstancePrototype.syncIntervalSecs = uiInstancePrototype.syncIntervalSecs || syncIntervalSecsBase;\n t.startSyncTimer(uiInstance);\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Start a sync timer on the given uiInstance, unless its time interval is 0.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.startSyncTimer = function(uiInstance) {\n const timeout = uiInstance.syncIntervalSecs();\n if (timeout) {\n this.uiInstance.timer = setInterval(function () {\n uiInstance.sync();\n }, timeout * 1000);\n } else {\n this.uiInstance.timer = null;\n }\n };\n\n\n /**\n * Stop the sync timer on the given uiInstance, if running.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.stopSyncTimer = function(uiInstance) {\n if (uiInstance.timer) {\n clearTimeout(uiInstance.timer);\n }\n };\n\n\n InterfaceWrapper.prototype.stop = function() {\n /*\n * Disable (shutdown) the embedded ui component.\n * The wrapper remains active for ctrl-alt-M events, but is hidden.\n */\n if (this.uiInstance !== null) {\n this.stopSyncTimer(this.uiInstance);\n this.textArea.show();\n if (this.uiInstance.hasFocus()) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n this.uiInstance.destroy();\n this.uiInstance = null;\n this.wrapperNode.hide();\n }\n this.loadFailed = false;\n this.textArea.removeClass('uiloadfailed'); // Just in case it failed before\n $(document.getElementById(this.loadFailId)).remove();\n };\n\n /*\n * Re-enable the ui element (e.g. after alt-cntrl-M). This is\n * a full re-initialisation of the ui element.\n */\n InterfaceWrapper.prototype.restart = function() {\n if (this.uiInstance === null) {\n /**\n * Restart the UI component in the textarea\n */\n this.loadUi(this.uiname, this.params);\n }\n };\n\n\n /**\n * Check for wrapper resize - propagate to ui element.\n */\n InterfaceWrapper.prototype.checkForResize = function() {\n const SIZE_HACK = 25; // Horrible but best I can do. TODO: FIXME\n\n if (this.uiInstance) {\n const h = this.wrapperNode.innerHeight();\n const w = this.wrapperNode.innerWidth();\n if (h != this.hLast || w != this.wLast) {\n const xLeft = this.wrapperNode.offset().left;\n const maxWidth = $(window).innerWidth() - xLeft - SIZE_HACK;\n const hAdjusted = h - this.GUTTER;\n const wAdjusted = Math.min(maxWidth, w);\n this.uiInstance.resize(wAdjusted, hAdjusted);\n this.hLast = this.wrapperNode.innerHeight();\n this.wLast = this.wrapperNode.innerWidth();\n }\n }\n };\n\n /**\n * The external entry point from the PHP.\n * @param {string} uiname The name of the User Interface to use e.g. 'ace'\n * @param {string} textareaId The ID of the textarea to be wrapped.\n */\n function newUiWrapper(uiname, textareaId) {\n if (uiname) {\n return new InterfaceWrapper(uiname, textareaId);\n } else {\n return null;\n }\n }\n\n\n return {\n newUiWrapper: newUiWrapper,\n InterfaceWrapper: InterfaceWrapper\n };\n});\n"],"names":["define","$","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","taId","loadFailId","ta","document","getElementById","textArea","params","attr","uiParams","JSON","parse","lang","readOnly","prop","isLoading","loadFailed","retries","h","parseInt","css","content_lines","val","split","length","rows","Math","min","max","wrapperNode","after","hide","resize","overflow","minHeight","width","border","data","uiInstance","loadUi","mousemove","checkForResize","window","closest","submit","sync","body","on","e","keyCode","ctrlKey","altKey","stop","restart","prototype","syncIntervalSecsBase","hasOwnProperty","sync_interval_secs","alert","loading","setTimeout","sessionStorage","getItem","require","ui","innerHeight","w","innerWidth","Constructor","failed","destroy","addClass","loadFailDiv","jqLoadFailDiv","insertBefore","langString","failMessage","errorDiv","str","s","get_string","fallback","when","done","html","hLast","wLast","show","append","getElement","uiInstancePrototype","Object","getPrototypeOf","syncIntervalSecs","startSyncTimer","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","remove","xLeft","offset","left","maxWidth","hAdjusted","wAdjusted","newUiWrapper"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/outputdisplayarea.js b/amd/src/outputdisplayarea.js
index c297da893..aa16a15ed 100644
--- a/amd/src/outputdisplayarea.js
+++ b/amd/src/outputdisplayarea.js
@@ -53,15 +53,23 @@ const JSON_DISPLAY_PROPS = ['returncode', 'stdout', 'stderr', 'files'];
* Get the specified language string using
* AJAX and plug it into the given textarea
* @param {string} langStringName The language string name.
- * @param {DOMnode} display area into which the error message
+ * @param {DOMnode} node area into which the error message
* should be plugged.
+ * @param {function} callback Callback function, with two arguments: node, message.
* @param {string} additionalText Extra text to follow the result code.
*/
-const setLangString = async(langStringName, display, additionalText) => {
- const message = await getLangString(langStringName, 'qtype_coderunner');
- display.innerText = "*** " + message + " ***\n";
+const setLangString = async(langStringName, node, callback, additionalText = '') => {
+ let message = await getLangString(langStringName, 'qtype_coderunner');
+ if (langStringName.includes('error')) {
+ message = "*** " + message + " ***\n";
+ }
if (additionalText) {
- display.innerText += additionalText;
+ message += additionalText;
+ }
+ if (callback instanceof Function) {
+ callback(node, message);
+ } else {
+ node.innerText = message;
}
};
@@ -191,17 +199,19 @@ class OutputDisplayArea {
*/
displayJson(response) {
const result = this.validateJson(response.output);
+
let text = result.stdout;
if (result.returncode !== INPUT_INTERRUPT) {
text += result.stderr;
}
if (result.returncode == 13) { // Timeout
- text += "\n*** Timeout error ***\n";
- } // TODO: Langstring
+ setLangString('error_timeout', this.textDisplay, (node, msg) => {
+ node.innerText += msg;
+ });
+ }
const numImages = this.displayImages(result.files);
-
if (text.trim() === '' && result.returncode !== INPUT_INTERRUPT) {
if (numImages == 0) {
this.displayNoOutput(null);
@@ -209,7 +219,6 @@ class OutputDisplayArea {
} else {
this.textDisplay.innerText = text;
}
-
if (result.returncode === INPUT_INTERRUPT) {
this.addInput();
}
@@ -226,20 +235,20 @@ class OutputDisplayArea {
result = JSON.parse(jsonString);
} catch (e) {
window.alert(
- `Error parsing display JSON output:
- ${jsonString}
- JSON Parsing error Msg:
- ${e.message}
- The question author must fix this!`
+ `Error parsing display JSON output: \n` +
+ `'${jsonString}\n'` +
+ `Error Msg: \n` +
+ ` ${e.message} \n` +
+ `The question author must fix this!`
);
}
const missing = missingProperties(result, JSON_DISPLAY_PROPS);
if (missing.length > 0) {
window.alert(
- `Display JSON (in response.result) is missing the following fields:
- ${missing.join()}
- The question author must fix this!`
+ `Display JSON (in response.result) is missing the following fields: \n` +
+ `${missing.join()} \n` +
+ `The question author must fix this!`
);
}
return result;
@@ -255,7 +264,7 @@ class OutputDisplayArea {
if (isNoOutput || response === null) {
const span = document.createElement('span');
span.style.color = 'red';
- span.innerText = '< No output! >';
+ setLangString('nooutput', span);
this.clearDisplay();
this.textDisplay.append(span);
}
@@ -327,6 +336,9 @@ class OutputDisplayArea {
const inputId = `${this.displayAreaId}-input-field`;
this.textDisplay.innerHTML += ``;
const inputEl = document.getElementById(inputId);
+ setLangString('enter_to_submit', inputEl, (node, msg) => {
+ node.placeholder += msg;
+ });
this.addInputEvents(inputEl);
}
@@ -369,7 +381,7 @@ class OutputDisplayArea {
this.imageDisplay.append(image);
numImages += 1;
} else {
- window.alert(`Could not read filename correctly: ${fname}`);
+ window.alert(`Could not read filename correctly: "${fname}"`);
}
}
return numImages;
diff --git a/amd/src/ui_scratchpad.js b/amd/src/ui_scratchpad.js
index a9c76a3bf..9abb59582 100644
--- a/amd/src/ui_scratchpad.js
+++ b/amd/src/ui_scratchpad.js
@@ -86,29 +86,42 @@ const invertSerial = (current) => current[0] === '1' ? [''] : ['1'];
* Insert the answer code and test code into the wrapper. This may
* be defined by the user, in UI Params or globalextra. If prefixAns is
* false: do not include answerCode in final wrapper.
- * @param {string} answerCode text.
- * @param {string} testCode text.
+ * @param {string} answerCode text from first editor.
+ * @param {string} testCode text from second editor.
* @param {string} prefixAns '1' for true, '' for false.
* @param {string} template provided in UI Params or globalextra.
+ * @param {string} open delimiter to look for, e.g. '[['
+ * @param {string} close delimiter to look for, e.g. ']]'
* @returns {string} filled template.
*/
-const fillWrapper = (answerCode, testCode, prefixAns, template) => {
+const fillWrapper = (answerCode, testCode, prefixAns, template, open = '\\(', close = '\\)') => {
if (!template) {
- template = '{{ ANSWER_CODE }}\n' +
- '{{ SCRATCHPAD_CODE }}';
+ template = `${open} ANSWER_CODE ${close}\n` +
+ `${open} SCRATCHPAD_CODE ${close}`;
}
if (!prefixAns) {
answerCode = '';
}
- template = template.replaceAll('{{ ANSWER_CODE }}', answerCode);
- template = template.replaceAll('{{ SCRATCHPAD_CODE }}', testCode);
+ const escOpen = escapeRegExp(open);
+ const escClose = escapeRegExp(close);
+ const answerRegex = new RegExp(`${escOpen}\\s*ANSWER_CODE\\s*${escClose}`, 'g');
+ const scratchpadRegex = new RegExp(`${escOpen}\\s*SCRATCHPAD_CODE\\s*${escClose}`, 'g');
+ template = template.replaceAll(answerRegex, answerCode);
+ template = template.replaceAll(scratchpadRegex, testCode);
return template;
};
+/**
+ * Escapes a string for use in regex.
+ * @param {string} string to escape.
+ * @returns {string} RegEx escaped string
+ */
+const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
+
/**
* Returns a new object contain default values. If a matching key exists in
- * prescribed, the corresponding value from prescribed will replace the defualt value.
- * Does not add keys/values to the result if that key is not in defualts.
+ * prescribed, the corresponding value from prescribed will replace the default value.
+ * Does not add keys/values to the result if that key is not in defaults.
* @param {object} defaults object with values to be overwritten.
* @param {object} prescribed settings, typically set by a user.
* @returns {object} filled with default values, overwritten by their prescribed value (iff included).
@@ -129,7 +142,7 @@ const overwriteValues = (defaults, prescribed) => {
* @returns {boolean} true if el is collapsed.
*/
const isCollapsed = (el) => {
- if (!el.classList.contains('collapse')) {
+ if (!(el.classList.contains('collapse') || el.classList.contains('collapsing'))) {
throw Error('Element does not have collapse class');
}
return !el.classList.contains('show');
@@ -154,7 +167,10 @@ class ScratchpadUi {
run_lang: uiParams.lang, // Use answer's ace language if not specified.
output_display_mode: 'text',
disable_scratchpad: false,
- wrapper_src: null
+ wrapper_src: null,
+ open_delimiter: '{|',
+ close_delimiter: '|}',
+ escape: false
};
this.textArea = document.getElementById(textAreaId);
this.textAreaId = textAreaId;
@@ -249,11 +265,16 @@ class ScratchpadUi {
this.sync(); // Use up-to-date serialization.
const preloadString = this.textArea.value;
const serial = this.readJson(preloadString);
+ const escape = (code) => this.uiParams.escape ? JSON.stringify(code).slice(1, -1) : code;
+ const answerCode = escape(serial.answer_code[0]);
+ const testCode = escape(serial.test_code[0]);
const code = fillWrapper(
- serial.answer_code,
- serial.test_code,
- serial.prefix_ans[0],
- this.runWrapper
+ answerCode,
+ testCode,
+ serial.prefix_ans[0],
+ this.runWrapper,
+ this.uiParams.open_delimiter,
+ this.uiParams.close_delimiter
);
this.outputDisplay.runCode(code, '', true); // Call with no stdin.
}
diff --git a/amd/src/ui_scratchpad.json b/amd/src/ui_scratchpad.json
index 568e9677a..050b7a30a 100644
--- a/amd/src/ui_scratchpad.json
+++ b/amd/src/ui_scratchpad.json
@@ -29,6 +29,14 @@
"type": "string",
"default": "text"
},
+ "open_delimiter": {
+ "type": "string",
+ "default": "{|"
+ },
+ "close_delimiter": {
+ "type": "string",
+ "default": "|}"
+ },
"params": {
"type": "object",
"default": null
@@ -40,6 +48,10 @@
"invert_prefix": {
"type": "boolean",
"default": false
+ },
+ "escape": {
+ "type": "boolean",
+ "default": false
}
}
}
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index b13a6e1b1..67a6dd147 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -151,6 +151,7 @@
$string['enable_diff_check'] = 'Enable \'Show differences\' button';
$string['enable_diff_check_desc'] = 'Present students with a \'Show differences\' button if their answer is wrong and an exact-match validator is being used';
$string['enable_sandbox_desc'] = 'Permit use of the specified sandbox for running student submissions';
+$string['enter_to_submit'] = 'Enter to submit...';
$string['equalitygrader'] = 'Exact match';
$string['error_loading_prototype'] = 'Error loading prototype. Network problems or server down, perhaps?';
$string['error_loading_ui_descr'] = 'Error loading UI description. Network problems or server down, perhaps?';
@@ -169,6 +170,18 @@
$string['errorstring-submissionfailed'] = 'Submission to sandbox failed';
$string['errorstring-unknown'] = 'Unexpected error while executing your code. The sandbox server may be down or overloaded. Perhaps try again shortly?';
+# Webservice errors for output display area.
+$string['error_access_denied'] = 'Sandbox server access denied';
+$string['error_excessive_output'] = 'Excessive output';
+$string['error_json_params'] = 'Params set are not in correct JSON format';
+$string['error_jobe_unknown'] = 'Unknown error from Jobe server';
+$string['error_memory_limit'] = 'Memory limit exceeded';
+$string['error_sandbox_server_overload'] = 'Jobe server overload';
+$string['error_submission_limit_reached'] = 'Jobe sandbox submission limit reached';
+$string['error_timeout'] = 'Time limit exceeded';
+$string['error_unknown_language'] = 'Unknown language requested';
+$string['error_unknown_runtime'] = 'Unknown runtime error';
+
$string['event_sandboxwebserviceexec'] = 'CR sandbox exec';
$string['event_sandboxwebserviceexec_desc'] = 'A job was executed via the CodeRunner sandbox web service.';
@@ -440,6 +453,7 @@
$string['noerrorsallowed'] = 'Your code must pass all tests to earn any marks. Try again.';
$string['nonnumericmark'] = 'Non-numeric mark';
$string['nosampleanswer'] = 'No sample answer';
+$string['nooutput'] = '< No output! >';
$string['negativeorzeromark'] = 'Mark must be greater than zero';
$string['options'] = 'Options';
@@ -993,6 +1007,50 @@ class definition is followed by a public __Tester__ class that
$string['supportscripts'] = 'Support scripts';
$string['syntax_errors'] = 'Syntax Error(s)';
+# SCRATCHPAD UI Default text
+$string['scratchpadui_def_button_name'] = 'Run';
+$string['scratchpadui_def_scratchpad_name'] = 'Scratchpad';
+$string['scratchpadui_def_prefix_name'] = 'Prefix with Answer';
+$string['scratchpadui_def_help_text'] = '
You can enter code into this panel and click \'Run\' to execute it.
+
By default, the code in this panel is prefixed with the contents of the answer box, giving you an easy way to test your answer.
+
You can uncheck the \'Prefix with answer\' checkbox to run the code in this panel standalone, e.g. to explore how small code fragments behave.
';
+# SCRATCHPAD UI Parameter descriptions
+$string['scratchpadui_scratchpad_name_descr'] = 'Display name of the scratchpad, used to hide/un-hide the scratchpad.';
+$string['scratchpadui_button_name_descr'] = 'Run button text.';
+$string['scratchpadui_prefix_name_descr'] = 'Prefix with answer check-box label text.';
+$string['scratchpadui_run_lang_descr'] = 'Language used to run code when the run button is clicked, this should be the language your wrapper is written in (if applicable).';
+$string['scratchpadui_params_descr'] = 'Parameters for the sandbox webservice.';
+$string['scratchpadui_output_display_mode_descr'] = 'Control how program output is displayed on runs, there are three modes:
+
+
text: Display the output as text, html escaped. (default)
+
json: Display programs that output JSON, useful for capturing stdin and displaying images. (recommended)
+
+
Accepts JSON in run output with the fields:
+
+
returncode: Exit code from program run.
+
stdout: Stdout text from program run.
+
stderr: Error text from program run.
+
files: An object containing filenames mapped to base64 encoded images. These will be displayed below any stdout text.
+
+
When the returncode is set to 42, an HTML input field will be added after the last stdout received. When the enter key is pressed inside the input, the input\'s value is added to stdin and the program is run again with this updated stdin. This is repeated until returncode is not set to 42.
+
html: Display program output as raw html inside the output area. (advanced)
+
+
This can be used to show images and insert other HTML.
+
Giving an <input> element the class coderunner-run-input will add an event: when the enter key is pressed inside the input, the input\'s value is added to stdin and the program is run again with this updated stdin.
+
+
';
+$string['scratchpadui_open_delimiter_descr'] = 'The opening delimiter to use when inserting answer or Scratchpad code into the wrapper. It will replace the default value \'{|\'.';
+$string['scratchpadui_close_delimiter_descr'] = 'The closing delimiter to use when inserting answer or Scratchpad code into the wrapper. It will replace the default value \'|}\'.';
+$string['scratchpadui_help_text_descr'] = 'The help text to show.';
+$string['scratchpadui_wrapper_src_descr'] = 'The location of wrapper to be used by the run button: setting to \'globalextra\' will use text in global extra field, \'prototypeextra\' will use the prototype extra field.';
+$string['scratchpadui_disable_scratchpad_descr'] = 'Disable the scratchpad, effectively revert back to Ace UI from student perspective.';
+$string['scratchpadui_invert_prefix_descr'] = 'Inverts meaning of prefix_ans serialisation: \'1\' means un-ticked -- and vice versa. This can be used to swap the default state.';
+$string['scratchpadui_escape_descr'] = 'Escape (JSON with " removed from start and end) ANSWER_CODE and SCRATCHPAD_CODE before insertion into wrapper. Useful when inserting code into a string. NOTE: single quotes \' are NOT escaped.';
+# SCRATCHPAD UI Errors
+$string['scratchpad_ui_badrunwrappersrc'] = 'Invalid run wrapper source given, please contact question author.';
+$string['scratchpad_ui_invalidserialisation'] = 'Invalid JSON serialisation provided, must include \"answer_code\" field.';
+$string['scratchpad_ui_templateloadfail'] = 'Scratchpad UI template failed to load, please refresh the page. If this persists please report.';
+
$string['tableui_num_rows_descr'] = 'The (initial) number of rows in the table, excluding the top header row (if headers are given). Required.';
$string['tableui_num_columns_descr'] = 'The number of columns in the table, excluding the left-most label column (if labels are given). Required.';
$string['tableui_column_headers_descr'] = 'A list of strings for the column headers.';
@@ -1254,36 +1312,3 @@ function should be applied, e.g. {{STUDENT_ANSWER | e(\'py\')}} is
$string['xmlcoderunnerformaterror'] = 'XML format error in coderunner question';
-
-$string['scratchpadui_scratchpad_name_descr'] = 'The display name of the scratchpad, used to hide/un-hide the scratchpad.';
-$string['scratchpadui_button_name_descr'] = 'The run button text.';
-$string['scratchpadui_prefix_name_descr'] = 'The prefix with answer check-box label text.';
-$string['scratchpadui_run_lang_descr'] = 'The language used to run code when the run button is clicked, this should be the language your wrapper is written in (if applicable).';
-$string['scratchpadui_params_descr'] = 'The prefix with answer check-box label text.';
-$string['scratchpadui_html_output_descr'] = 'Display the output from run as raw HTML instead of text.';
-$string['scratchpadui_help_text_descr'] = 'The help text to show.';
-$string['scratchpadui_wrapper_src_descr'] = 'The location of wrapper to be used by the run button: setting to \'globalextra\' will use text in global extra field, \'prototypeextra\' will use the prototype extra field.';
-$string['scratchpadui_disable_scratchpad_descr'] = 'Disable the scratchpad, effectively revert back to Ace UI from student perspective.';
-$string['scratchpadui_invert_prefix_descr'] = 'Inverts meaning of prefix_ans serialisation -- \'1\' means un-ticked, vice versa. This can be used to swap the default state.';
-$string['scratchpadui_def_button_name'] = 'Run';
-$string['scratchpadui_def_scratchpad_name'] = 'Scratchpad';
-$string['scratchpadui_def_prefix_name'] = 'Prefix with Answer';
-
-$string['scratchpadui_def_help_text'] = 'You can enter code into this panel and click \'Run\' to execute it.
-By default, the code in this panel is prefixed with the contents of the answer box, giving you an easy way to test your answer.
-You can uncheck the \'Prefix with answer\' checkbox to run the code in this panel standalone, e.g. to explore how small code fragments behave.';
-
-$string['scratchpad_ui_invalidserialisation'] = 'Invalid JSON serialisation provided, must include \"answer_code\" field.';
-$string['scratchpad_ui_templateloadfail'] = 'Scratchpad UI template failed to load, please refresh the page.';
-$string['scratchpad_ui_badrunwrappersrc'] = 'Invalid run wrapper source given, please contact question author.';
-
-$string['error_access_denied'] = 'Sandbox server access denied';
-$string['error_excessive_output'] = 'Excessive output';
-$string['error_json_params'] = 'Params set are not in correct JSON format';
-$string['error_jobe_unknown'] = 'Unknown error from Jobe server';
-$string['error_memory_limit'] = 'Memory limit exceeded';
-$string['error_sandbox_server_overload'] = 'Jobe server overload';
-$string['error_submission_limit_reached'] = 'Jobe sandbox submission limit reached';
-$string['error_timeout'] = 'Time limit exceeded';
-$string['error_unknown_language'] = 'Unknown language requested';
-$string['error_unknown_runtime'] = 'Unknown runtime error';
diff --git a/tests/behat/ace_scratchpad_compatibility.feature b/tests/behat/ace_scratchpad_compatibility.feature
new file mode 100644
index 000000000..4819c6983
--- /dev/null
+++ b/tests/behat/ace_scratchpad_compatibility.feature
@@ -0,0 +1,74 @@
+@qtype @javascript @qtype_coderunner @scratchpad
+Feature: Ace UI convert to Scratchpad UI questions with one click
+ In order to use the Scratchpad UI
+ As a teacher
+ I should be able to change a question from using Ace to Scratchpad in one click
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@asd.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And the following "question categories" exist:
+ | contextlevel | reference | name |
+ | Course | C1 | Test questions |
+ And the following "questions" exist:
+ | questioncategory | qtype | name |
+ | Test questions | coderunner | Square function |
+ And the CodeRunner sandbox is enabled
+
+ When I am on the "Square function" "core_question > edit" page logged in as teacher1
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_answer | def sqr(n): return n * n |
+
+ Scenario: Edit a CodeRunner Ace UI question, changing it to be a Scratchpad UI question.
+ When I set the field "id_uiplugin" to "Scratchpad"
+ Then I should see "def sqr(n): return n * n"
+ And I click on "Scratchpad" "button"
+ And I should see "Run"
+ And I should see "Prefix with Answer"
+
+ When I press "id_submitbutton"
+ Then I should see "Square function"
+
+ When I choose "Preview" action for "Square function" in the question bank
+ And I set the ace field "answer_code" to "def sqr(n): return n * n"
+ And I press "Check"
+ Then the following should exist in the "coderunner-test-results" table:
+ | Test |
+ | sqr(-7) |
+ | sqr(-3) |
+ | sqr(11) |
+ And I should see "Passed all tests!"
+ And I should not see "Show differences"
+ And I should see "Correct"
+
+ When I press "Close preview"
+ Then I should see "Square function"
+
+ When I choose "Edit question" action for "Square function" in the question bank
+ And I set the field "id_uiplugin" to "Ace"
+ And I should see "def sqr(n): return n * n"
+
+ When I press "id_submitbutton"
+ Then I should see "Square function"
+
+ When I choose "Preview" action for "Square function" in the question bank
+ Then I should not see "Scratchpad"
+
+ When I set the ace field "_answer" to "def sqr(n): return n * n"
+ And I press "Check"
+ Then the following should exist in the "coderunner-test-results" table:
+ | Test |
+ | sqr(-7) |
+ | sqr(-3) |
+ | sqr(11) |
+ And I should see "Passed all tests!"
+ And I should not see "Show differences"
+ And I should see "Correct"
diff --git a/tests/behat/behat_coderunner.php b/tests/behat/behat_coderunner.php
index 1cdb10ced..9e3ec5565 100644
--- a/tests/behat/behat_coderunner.php
+++ b/tests/behat/behat_coderunner.php
@@ -99,14 +99,15 @@ public function i_should_see_in_answer_pystring(Behat\Gherkin\Node\PyStringNode
/**
* Sets the ace editor content to provided string, using name of associated textarea.
* NOTE: this assumes the existence of a text area next to a
- * UI wrapper div containing the Ace div!
+ * UI wrapper div containing the Ace div! Also works on partial matches,
+ * i.e. value as _answer will work for Ace UI
* Intended as a replacement for I set field to , for ace fields.
* @Then /^I set the ace field "(?P(?:[^"]|\\")*)" to "(?P(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @param string $expected The string that we expect to find
*/
public function i_set_ace_field($elname, $value) {
- $xpath = "//textarea[@name='$elname']/../div/div";
+ $xpath = "//textarea[@name='$elname' or (contains(@name, '$elname') and contains(@class, 'edit_code'))]/../div/div ";
$driver = $this->getSession()->getDriver();
// Does the div managed by Ace exist?
if (!$driver->find($xpath)) {
@@ -281,4 +282,46 @@ public function i_set_the_field_and_dismiss_the_alert($field, $value) {
throw new ExpectationException("No alert was triggered appropriately", $this->getSession());
}
}
+
+
+
+ /**
+ * Presses a named button. Checks if there is a specified error text displayed.
+ *
+ * @Then I should see an alert of :error when I press :button
+ * @param string $errortext The expected error message when alerted
+ * @param string $button The name of the alert button.
+ */
+ public function there_is_an_alert_when_i_click($errortext, $button) {
+ // Gets the item of the button.
+ $xpath = "//button[@type='button' and contains(text(), '$button')]";
+ $session = $this->getSession();
+ $item = $session->getSelectorsHandler()->selectorToXpath('xpath', $xpath);
+ $element = $session->getPage()->find('xpath', $item);
+
+ // Makes sure there is an element before continuing.
+ if ($element) {
+ $element->click();
+ } else {
+ throw new ExpectationException("No button '{$button}'", $this->getSession());
+ }
+ try {
+ // Gets you to wait for the pending JS alert by sleeping.
+ sleep(1);
+ // Gets the alert and its text.
+ $alert = $this->getSession()->getDriver()->getWebDriver()->switchTo()->alert();
+ $alerttext = $alert->getText();
+ } catch (NoSuchAlertException $ex) {
+ throw new ExpectationException("No alert was triggered appropriately", $this->getSession());
+ }
+
+ // Throws an error if expected error text doesn't match alert.
+ if (!str_contains($alerttext, $errortext)) {
+ throw new ExpectationException("Wrong alert; alert given: {$alerttext}", $this->getSession());
+ } else {
+ // To stop the Behat tests from throwing their own errors.
+ $alert->accept();
+ }
+ }
}
+
diff --git a/tests/behat/scratchpad_ui.feature b/tests/behat/scratchpad_ui.feature
index e0ee42135..9397521a5 100644
--- a/tests/behat/scratchpad_ui.feature
+++ b/tests/behat/scratchpad_ui.feature
@@ -130,7 +130,7 @@ Feature: Test the Scratchpad UI
| id_uiplugin | Scratchpad |
| id_validateonsave | 0 |
| id_expected_0 | "" |
- And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I set the field "id_template" to "print('''{| STUDENT_ANSWER |}''')"
And I press "id_submitbutton"
Then I should see "Print answer"
@@ -150,7 +150,7 @@ Feature: Test the Scratchpad UI
| id_uiplugin | Scratchpad |
| id_validateonsave | 0 |
| id_expected_0 | '' |
- And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I set the field "id_template" to "print('''{| STUDENT_ANSWER |}''')"
And I press "id_submitbutton"
Then I should see "Print answer"
@@ -171,7 +171,7 @@ Feature: Test the Scratchpad UI
| id_uiplugin | Scratchpad |
| id_validateonsave | 0 |
| id_expected_0 | '' |
- And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I set the field "id_template" to "print('''{| STUDENT_ANSWER |}''')"
And I press "id_submitbutton"
Then I should see "Print answer"
@@ -253,7 +253,7 @@ Feature: Test the Scratchpad UI
| id_uiplugin | Scratchpad |
| id_validateonsave | 0 |
| id_expected_0 | '' |
- And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I set the field "id_template" to "print('''{| STUDENT_ANSWER |}''')"
And I press "id_submitbutton"
Then I should see "Print answer"
@@ -278,7 +278,7 @@ Feature: Test the Scratchpad UI
| id_uiplugin | Scratchpad |
| id_validateonsave | 0 |
| id_expected_0 | '' |
- And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I set the field "id_template" to "print('''{| STUDENT_ANSWER |}''')"
And I press "id_submitbutton"
Then I should see "Print answer"
@@ -306,7 +306,7 @@ Feature: Test the Scratchpad UI
| id_uiplugin | Scratchpad |
| id_validateonsave | 0 |
| id_expected_0 | '' |
- And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I set the field "id_template" to "print('''{| STUDENT_ANSWER |}''')"
And I press "id_submitbutton"
Then I should see "Print answer"
@@ -334,7 +334,7 @@ Feature: Test the Scratchpad UI
| id_uiplugin | Scratchpad |
| id_validateonsave | 0 |
| id_expected_0 | '' |
- And I set the field "id_template" to "print('''{{ STUDENT_ANSWER }}''')"
+ And I set the field "id_template" to "print('''{| STUDENT_ANSWER |}''')"
And I press "id_submitbutton"
Then I should see "Print answer"
diff --git a/tests/behat/scratchpad_ui_params.feature b/tests/behat/scratchpad_ui_params.feature
index c3dbd6748..5b4d7faa8 100644
--- a/tests/behat/scratchpad_ui_params.feature
+++ b/tests/behat/scratchpad_ui_params.feature
@@ -134,8 +134,8 @@ Feature: Test the Scratchpad UI, UI Params
And I set the field "globalextra" to:
"""
print('Hello Wrapper', end=' ')
- {{ ANSWER_CODE }}
- {{ SCRATCHPAD_CODE }}
+ {| ANSWER_CODE |}
+ {| SCRATCHPAD_CODE |}
"""
And I press "id_submitbutton"
@@ -158,8 +158,8 @@ Feature: Test the Scratchpad UI, UI Params
And I set the field "globalextra" to:
"""
print('Hello Wrapper', end=' ')
- {{ ANSWER_CODE }}
- {{ SCRATCHPAD_CODE }}
+ {| ANSWER_CODE |}
+ {| SCRATCHPAD_CODE |}
"""
And I press "id_submitbutton"
@@ -185,14 +185,14 @@ Feature: Test the Scratchpad UI, UI Params
And I set the field "globalextra" to:
"""
print('Hello GlobalExtra', end=' ')
- {{ ANSWER_CODE }}
- {{ SCRATCHPAD_CODE }}
+ {| ANSWER_CODE |}
+ {| SCRATCHPAD_CODE |}
"""
And I set the field "prototypeextra" to:
"""
print('Hello PrototypeExtra', end=' ')
- {{ ANSWER_CODE }}
- {{ SCRATCHPAD_CODE }}
+ {| ANSWER_CODE |}
+ {| SCRATCHPAD_CODE |}
"""
And I press "id_submitbutton"
Then I should see "Print answer"
@@ -216,8 +216,8 @@ Feature: Test the Scratchpad UI, UI Params
And I set the field "globalextra" to:
"""
print('Hello Wrapper', end=' ')
- {{ ANSWER_CODE }}
- {{ SCRATCHPAD_CODE }}
+ {| ANSWER_CODE |}
+ {| SCRATCHPAD_CODE |}
"""
And I press "id_submitbutton"
Then I should see "Print answer"
@@ -239,8 +239,8 @@ Feature: Test the Scratchpad UI, UI Params
And I set the field "globalextra" to:
"""
print('Hello Wrapper', end=' ')
- {{ ANSWER_CODE }}
- {{ SCRATCHPAD_CODE }}
+ {| ANSWER_CODE |}
+ {| SCRATCHPAD_CODE |}
"""
And I press "id_submitbutton"
@@ -446,8 +446,53 @@ Feature: Test the Scratchpad UI, UI Params
print(json.dumps(output))
"""
Then I press "Run"
+ And I set the ace field "test_code" to ""
And I should see "
Hi
"
+ Scenario: Set output_display_mode to json, 'print' a hidden div to output area
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"output_display_mode": "json"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "test_code" to:
+ """
+ output = {'returncode': 0,
+ 'stdout' : '
Hi
',
+ 'stderr' : '',
+ 'files' : ''
+ """
+ Then I should see an alert of "Error parsing display JSON output:" when I press "Run"
+
+ Scenario: Set output_display_mode to json, 'print' a hidden div to output area
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"output_display_mode": "json"} |
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "test_code" to:
+ """
+ import json
+ output = {'returncode': 0,
+ 'stdout' : '
Hi
',
+ 'files' : ''
+ }
+ print(json.dumps(output))
+ """
+ Then I should see an alert of "stderr" when I press "Run"
+
Scenario: Set output_display_mode to json, get no output.
And I set the field "id_answer" to ""
And I set the following fields to these values:
@@ -552,3 +597,27 @@ Feature: Test the Scratchpad UI, UI Params
When I set the ace field "test_code" to "'d'd'd'"
And I press "Run"
Then I should see "SyntaxError: invalid syntax"
+
+ Scenario: Create a wrapper with student code in single double-quote string (python) and enter program containing many quotes.
+ And I set the field "id_answer" to ""
+ And I set the following fields to these values:
+ | id_customise | 1 |
+ | id_uiplugin | Scratchpad |
+ | id_uiparameters | {"escape": "true", "wrapper_src": "globalextra"} |
+ And I set the field "globalextra" to "exec(\"{| ANSWER_CODE |}\n{| SCRATCHPAD_CODE |}\")"
+
+ And I press "id_submitbutton"
+ Then I should see "Print answer"
+
+ When I choose "Preview" action for "Print answer" in the question bank
+ And I click on "Scratchpad" "button"
+ And I set the ace field "answer_code" to "print(\"\"\"h\"\"\", end='')"
+ And I set the ace field "test_code" to:
+ """
+ print("i", end='')
+ print('1', end='')
+ print('''2''', end='')
+ print('3', end='')
+ """
+ And I press "Run"
+ Then I should see "hi123"
diff --git a/tests/fixtures/input_mpl_wrapper_json.py b/tests/fixtures/input_mpl_wrapper_json.py
index 05398c149..435d2ffe0 100644
--- a/tests/fixtures/input_mpl_wrapper_json.py
+++ b/tests/fixtures/input_mpl_wrapper_json.py
@@ -2,8 +2,8 @@
MAX_OUTPUT_CHARS = 30000
student_code = """
-{{ ANSWER_CODE }}
-{{ SCRATCHPAD_CODE }}
+\( ANSWER_CODE \)
+\( SCRATCHPAD_CODE \)
"""
uses_matplotlib = 'matplotlib' in student_code
@@ -112,7 +112,7 @@ def check_syntax():
'returncode': returncode,
'stdout' : truncated(stdout),
'stderr' : truncated(stderr),
- 'files' : files,
+ 'files' : files
}
print(json.dumps(output))
\ No newline at end of file
diff --git a/tests/fixtures/input_wrapper_json.py b/tests/fixtures/input_wrapper_json.py
new file mode 100644
index 000000000..fa3a907cc
--- /dev/null
+++ b/tests/fixtures/input_wrapper_json.py
@@ -0,0 +1,83 @@
+import sys, io, subprocess, ast, traceback, json
+MAX_OUTPUT_CHARS = 30000
+
+student_code = """
+{{ ANSWER_CODE }}
+{{ SCRATCHPAD_CODE }}
+"""
+
+subproc_code = """
+import sys
+MAX_OUTPUT_CHARS = 30000
+__saved_input__ = input
+
+def input(prompt=''):
+ try:
+ line = __saved_input__()
+ except EOFError:
+ print(prompt, end = '')
+ sys.stderr.flush()
+ sys.stdout.flush()
+ sys.exit(42)
+ print(prompt, end='')
+ print(line)
+ return line
+
+__saved_print__ = print
+__output_chars__ = 0
+
+def print(*params, **keyparams):
+ global __output_chars__
+ for param in params:
+ try:
+ __output_chars__ += len(str(param))
+ except:
+ pass
+ if __output_chars__ > 2 * MAX_OUTPUT_CHARS:
+ __saved_print__("\\\\n*** Excessive output. Job aborted ***", file=sys.stderr)
+ sys.exit(1)
+ else:
+ __saved_print__(*params, **keyparams)
+"""
+
+subproc_code += student_code
+
+def truncated(s):
+ return s if len(s) < MAX_OUTPUT_CHARS else s[:MAX_OUTPUT_CHARS] + ' ... (truncated)'
+
+def check_syntax():
+ try:
+ ast.parse(student_code)
+ return ''
+ except SyntaxError:
+ catcher = io.StringIO()
+ traceback.print_exc(limit=0, file=catcher)
+ return catcher.getvalue()
+
+stdout = ''
+stderr = check_syntax()
+if stderr == '': # No syntax errors
+ program_code = subproc_code
+ with open('prog.py', 'w') as outfile:
+ outfile.write(program_code)
+ proc = subprocess.Popen([sys.executable, 'prog.py'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+ try:
+ stdout, stderr = proc.communicate(timeout=3)
+ returncode = proc.returncode
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ stdout, stderr = proc.communicate()
+ returncode = 13
+
+
+else:
+ returncode = 1 # Syntax errors
+
+output = {
+ 'returncode': returncode,
+ 'stdout' : truncated(stdout),
+ 'stderr' : truncated(stderr),
+ 'files' : [],
+}
+
+print(json.dumps(output))
\ No newline at end of file
From 860b189f19084ef04397f462e831425be5471a64 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Fri, 17 Feb 2023 16:05:06 +1300
Subject: [PATCH 064/188] Remove from the Ace Gapfiller UI the ability to use
one of Ace's dark themes when the OS is in Dark Mode. [Looks horrible].
---
amd/build/authorform.min.js | 2 +-
amd/build/authorform.min.js.map | 2 +-
amd/build/graphutil.min.js | 2 +-
amd/build/graphutil.min.js.map | 2 +-
amd/build/outputdisplayarea.min.js | 27 +----------
amd/build/outputdisplayarea.min.js.map | 2 +-
amd/build/textareas.min.js | 2 +-
amd/build/textareas.min.js.map | 2 +-
amd/build/ui_ace.min.js | 2 +-
amd/build/ui_ace.min.js.map | 2 +-
amd/build/ui_ace_gapfiller.min.js | 2 +-
amd/build/ui_ace_gapfiller.min.js.map | 2 +-
amd/build/ui_scratchpad.min.js | 56 +----------------------
amd/build/ui_scratchpad.min.js.map | 2 +-
amd/build/userinterfacewrapper.min.js | 2 +-
amd/build/userinterfacewrapper.min.js.map | 2 +-
amd/src/ui_ace_gapfiller.js | 8 +---
17 files changed, 18 insertions(+), 101 deletions(-)
diff --git a/amd/build/authorform.min.js b/amd/build/authorform.min.js
index 20d22f825..1d7c6c937 100644
--- a/amd/build/authorform.min.js
+++ b/amd/build/authorform.min.js
@@ -5,6 +5,6 @@
* @copyright Richard Lobb, 2015, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/authorform",["jquery","qtype_coderunner/userinterfacewrapper","core/str"],(function($,ui,str){let currentQtype="";var JSON_TO_FORM_MAP={template:["#id_template","value",""],iscombinatortemplate:["#id_iscombinatortemplate","checked","",function(value){return"1"===value}],cputimelimitsecs:["#id_cputimelimitsecs","value",""],memlimitmb:["#id_memlimitmb","value",""],sandbox:["#id_sandbox","value","DEFAULT"],sandboxparams:["#id_sandboxparams","value",""],testsplitterre:["#id_testsplitterre","value","",function(splitter){return splitter.replace("\n","\\n")}],allowmultiplestdins:["#id_allowmultiplestdins","checked","",function(value){return"1"===value}],grader:["#id_grader","value","EqualityGrader"],resultcolumns:["#id_resultcolumns","value",""],language:["#id_language","value",""],acelang:["#id_acelang","value",""],uiplugin:["#id_uiplugin","value","ace"]};return{initEditForm:function(){var typeCombo=$("#id_coderunnertype"),prototypeDisplay=$("#id_isprototype"),template=$("#id_template"),evaluatePerStudent=$("#id_templateparamsevalpertry"),globalextra=$("#id_globalextra"),prototypeextra=$("#id_prototypeextra"),useace=$("#id_useace"),language=$("#id_language"),acelang=$("#id_acelang"),customise=$("#id_customise"),isCombinator=$("#id_iscombinatortemplate"),testSplitterRe=$("#id_testsplitterre"),allowMultipleStdins=$("#id_allowmultiplestdins"),customisationFieldSet=$("#id_customisationheader"),advancedCustomisation=$("#id_advancedcustomisationheader"),isCustomised=customise.prop("checked"),prototypeType=$("#id_prototypetype"),preloadHdr=$("#id_answerpreloadhdr"),courseId=$('input[name="courseid"]').prop("value"),questiontypeHelpDiv=$("#qtype-help"),precheck=$("select#id_precheck"),testtypedivs=$("div.testtype"),brokenQuestion=$("#id_broken_question"),badQuestionLoad=$("#id_bad_question_load"),uiplugin=$("#id_uiplugin"),uiparameters=$("#id_uiparameters");function setUi(taId,uiname){var lang,uiWrapper,ta=$(document.getElementById(taId)),currentLang=ta.attr("data-lang"),paramsJson=ta.attr("data-params"),params={};ta.attr("data-prototypeextra",prototypeextra.val()),ta.attr("data-globalextra",globalextra.val()),ta.attr("data-test0",$("#id_testcode_0").val());try{params=JSON.parse(paramsJson)}catch(err){}"none"===(uiname=uiname.toLowerCase())&&(uiname=""),"id_templateparams"==taId||"id_uiparameters"==taId?lang="":(lang=language.prop("value"),"id_template"!==taId&&acelang.prop("value")&&(lang=function(acelang){var langs,i;if(acelang.indexOf(",")<0)return acelang;for(langs=acelang.split(","),i=0;i0?langs[0]:""}(acelang.prop("value")))),(uiWrapper=ta.data("current-ui-wrapper"))&&uiWrapper.uiname===uiname&¤tLang==lang||(ta.attr("data-lang",lang),uiWrapper?(params.lang=lang,uiWrapper.loadUi(uiname,params)):uiWrapper=new ui.InterfaceWrapper(uiname,taId))}function setUis(){let uiname=uiplugin.val(),answer=$("#id_answer"),enableUi=!0;if("html"===uiname&&""!==answer.attr("data-params"))try{!1===JSON.parse(answer.attr("data-params")).enable_in_editor&&(enableUi=!1)}catch(error){alert("Invalid UI parameters.")}enableUi&&(setUi("id_answer",uiname),setUi("id_answerpreload",uiname))}function setCustomisationVisibility(isVisible){var display=isVisible?"block":"none";customisationFieldSet.css("display",display),advancedCustomisation.css("display",display),isVisible&&useace.prop("checked")&&setUi("id_template","ace")}function copyFieldsFromQuestionType(newType,response){var formspecifier,attrval,isCombinatorEnabled;for(var key in function(stateOn){var uiWrapper,taIds=["id_template","id_uiparameters"];if(useace.prop("checked"))for(var i=0;i3&&(attrval=(0,formspecifier[3])(attrval)),$(formspecifier[0]).prop(formspecifier[1],attrval);customise.prop("checked",!1),str.get_string("coderunner_question_type","qtype_coderunner").then((function(s){var title,coderunner_descr,html,resultHtml;questiontypeHelpDiv.html((title=newType,coderunner_descr=s,html=response.questiontext,resultHtml='
")}))):loadCustomisationFields(),set_testtype_visibilities(),useace.prop("checked")&&(setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")),loadUiParametersDescription(),customise.on("change",(function(){customise.prop("checked")?setCustomisationVisibility(!0):str.get_string("confirm_proceed","qtype_coderunner").then((function(s){window.confirm(s)?setCustomisationVisibility(!1):customise.prop("checked",!0)}))})),acelang.on("change",check_ace_lang),language.on("change",(function(){useace.prop("checked")&&setUi("id_template","ace"),check_ace_lang()})),typeCombo.on("change",(function(){customise.prop("checked")?str.get_string("question_type_changed","qtype_coderunner").then((function(s){window.confirm(s)&&loadCustomisationFields()})):loadCustomisationFields()})),useace.on("change",(function(){useace.prop("checked")?(setUi("id_template","ace"),setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")):(setUi("id_template",""),setUi("id_templateparams",""),setUi("id_uiparameters",""))})),evaluatePerStudent.on("change",(function(){evaluatePerStudent.is(":checked")&&langStringAlert("templateparamsusingsandbox")})),uiplugin.on("change",(function(){setUis(),loadUiParametersDescription()})),precheck.on("change",set_testtype_visibilities),prototypeType.on("change",(function(){"0"==prototypeType.prop("value")?prototypeDisplay.attr("hidden","1"):prototypeDisplay.removeAttr("hidden")})),new MutationObserver((function(){setUis()})).observe(preloadHdr.get(0),{attributes:!0}),$("button.replaceexpectedwithgot").click((function(){var gotPre=$(this).prev('pre[id^="id_got_"]'),testCaseId=gotPre.attr("id").replace("id_got_","");$("#id_expected_"+testCaseId).val(gotPre.text()),$("#id_fail_expected_"+testCaseId).html(gotPre.text()),$(".failrow_"+testCaseId).addClass("fixed"),$(this).prop("disabled",!0)})),$(".btn-primary").click((function(){typeCombo.prop("disabled",!1)}))}}}));
//# sourceMappingURL=authorform.min.js.map
\ No newline at end of file
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
index 02b9de9e9..adb230c7d 100644
--- a/amd/build/authorform.min.js.map
+++ b/amd/build/authorform.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * The sample answer and answer preload have the data-params attribute which contains\n * the UI params in a JSON from the current question merged with the prototype.\n * Both of them are identical and are changed simultaneously; only checking\n * answer as state is identical.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n *\n */\n function setUis() {\n let uiname = uiplugin.val();\n let answer = $('#id_answer');\n let enableUi = true;\n if (uiname === 'html' && answer.attr('data-params') !== '') {\n try {\n let answerparams = JSON.parse(answer.attr('data-params'));\n if (answerparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","answer","enableUi","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","trim","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * The sample answer and answer preload have the data-params attribute which contains\n * the UI params in a JSON from the current question merged with the prototype.\n * Both of them are identical and are changed simultaneously; only checking\n * answer as state is identical.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n *\n */\n function setUis() {\n let uiname = uiplugin.val();\n let answer = $('#id_answer');\n let enableUi = true;\n if (uiname === 'html' && answer.attr('data-params') !== '') {\n try {\n let answerparams = JSON.parse(answer.attr('data-params'));\n if (answerparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","messagePara","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","answer","enableUi","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","trim","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/graphutil.min.js b/amd/build/graphutil.min.js
index 93b742731..feb02fc20 100644
--- a/amd/build/graphutil.min.js
+++ b/amd/build/graphutil.min.js
@@ -1,3 +1,3 @@
-define("qtype_coderunner/graphutil",(function(){function Util(){this.greekLetterNames=["Alpha","Beta","Gamma","Delta","Epsilon","Zeta","Eta","Theta","Iota","Kappa","Lambda","Mu","Nu","Xi","Omicron","Pi","Rho","Sigma","Tau","Upsilon","Phi","Chi","Psi","Omega"]}return Util.prototype.convertLatexShortcuts=function(text){for(var i=0;i16)))).replace(new RegExp("\\\\"+name.toLowerCase(),"g"),String.fromCharCode(945+i+(i>16)))}for(i=0;i<10;i++)text=text.replace(new RegExp("_"+i,"g"),String.fromCharCode(8320+i));return text=text.replace(new RegExp("_a","g"),String.fromCharCode(8336))},Util.prototype.drawArrow=function(c,x,y,angle){var dx=Math.cos(angle),dy=Math.sin(angle);c.beginPath(),c.moveTo(x,y),c.lineTo(x-8*dx+5*dy,y-8*dy-5*dx),c.lineTo(x-8*dx-5*dy,y-8*dy+5*dx),c.fill()},Util.prototype.det=function(a,b,c,d,e,f,g,h,i){return a*e*i+b*f*g+c*d*h-a*f*h-b*d*i-c*e*g},Util.prototype.vectorMagnitude=function(v){return Math.sqrt(v.x*v.x+v.y*v.y)},Util.prototype.scalarProjection=function(a,b){return(a.x*b.x+a.y*b.y)/this.vectorMagnitude(b)},Util.prototype.isCCW=function(a,b){return a.x*b.y-b.x*a.y>0},Util.prototype.circleFromThreePoints=function(x1,y1,x2,y2,x3,y3){var a=this.det(x1,y1,1,x2,y2,1,x3,y3,1),bx=-this.det(x1*x1+y1*y1,y1,1,x2*x2+y2*y2,y2,1,x3*x3+y3*y3,y3,1),by=this.det(x1*x1+y1*y1,x1,1,x2*x2+y2*y2,x2,1,x3*x3+y3*y3,x3,1),c=-this.det(x1*x1+y1*y1,x1,y1,x2*x2+y2*y2,x2,y2,x3*x3+y3*y3,x3,y3);return{x:-bx/(2*a),y:-by/(2*a),radius:Math.sqrt(bx*bx+by*by-4*a*c)/(2*Math.abs(a))}},Util.prototype.isInside=function(pos,rect){return pos.x>rect.x&&pos.xrect.y},Util.prototype.crossBrowserKey=function(e){return(e=e||window.event).which||e.keyCode},Util.prototype.crossBrowserRelativeMousePos=function(e){const rect=e.target.getBoundingClientRect();return{x:e.clientX-rect.left,y:e.clientY-rect.top}},new Util}));
+define("qtype_coderunner/graphutil",(function(){function Util(){this.greekLetterNames=["Alpha","Beta","Gamma","Delta","Epsilon","Zeta","Eta","Theta","Iota","Kappa","Lambda","Mu","Nu","Xi","Omicron","Pi","Rho","Sigma","Tau","Upsilon","Phi","Chi","Psi","Omega"]}return Util.prototype.convertLatexShortcuts=function(text){for(var i=0;i16)))).replace(new RegExp("\\\\"+name.toLowerCase(),"g"),String.fromCharCode(945+i+(i>16)))}for(i=0;i<10;i++)text=text.replace(new RegExp("_"+i,"g"),String.fromCharCode(8320+i));return text=text.replace(new RegExp("_a","g"),String.fromCharCode(8336))},Util.prototype.drawArrow=function(c,x,y,angle){var dx=Math.cos(angle),dy=Math.sin(angle);c.beginPath(),c.moveTo(x,y),c.lineTo(x-8*dx+5*dy,y-8*dy-5*dx),c.lineTo(x-8*dx-5*dy,y-8*dy+5*dx),c.fill()},Util.prototype.det=function(a,b,c,d,e,f,g,h,i){return a*e*i+b*f*g+c*d*h-a*f*h-b*d*i-c*e*g},Util.prototype.vectorMagnitude=function(v){return Math.sqrt(v.x*v.x+v.y*v.y)},Util.prototype.scalarProjection=function(a,b){return(a.x*b.x+a.y*b.y)/this.vectorMagnitude(b)},Util.prototype.isCCW=function(a,b){return a.x*b.y-b.x*a.y>0},Util.prototype.circleFromThreePoints=function(x1,y1,x2,y2,x3,y3){var a=this.det(x1,y1,1,x2,y2,1,x3,y3,1),bx=-this.det(x1*x1+y1*y1,y1,1,x2*x2+y2*y2,y2,1,x3*x3+y3*y3,y3,1),by=this.det(x1*x1+y1*y1,x1,1,x2*x2+y2*y2,x2,1,x3*x3+y3*y3,x3,1),c=-this.det(x1*x1+y1*y1,x1,y1,x2*x2+y2*y2,x2,y2,x3*x3+y3*y3,x3,y3);return{x:-bx/(2*a),y:-by/(2*a),radius:Math.sqrt(bx*bx+by*by-4*a*c)/(2*Math.abs(a))}},Util.prototype.isInside=function(pos,rect){return pos.x>rect.x&&pos.xrect.y},Util.prototype.crossBrowserKey=function(e){return(e=e||window.event).which||e.keyCode},Util.prototype.crossBrowserRelativeMousePos=function(e){var rect=e.target.getBoundingClientRect();return{x:e.clientX-rect.left,y:e.clientY-rect.top}},new Util}));
//# sourceMappingURL=graphutil.min.js.map
\ No newline at end of file
diff --git a/amd/build/graphutil.min.js.map b/amd/build/graphutil.min.js.map
index 23ac65002..5f11864b8 100644
--- a/amd/build/graphutil.min.js.map
+++ b/amd/build/graphutil.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"graphutil.min.js","sources":["../src/graphutil.js"],"sourcesContent":["/***********************************************************************\n *\n * Utility functions/data/constants for the ui_graph module.\n *\n ***********************************************************************/\n// Most of this code is taken from Finite State Machine Designer:\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(function() {\n /**\n * Contstructor for the Util class.\n */\n function Util() {\n\n this.greekLetterNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',\n 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda',\n 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',\n 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega' ];\n }\n\n Util.prototype.convertLatexShortcuts = function(text) {\n // Html greek characters.\n for(var i = 0; i < this.greekLetterNames.length; i++) {\n var name = this.greekLetterNames[i];\n text = text.replace(new RegExp('\\\\\\\\' + name, 'g'), String.fromCharCode(913 + i + (i > 16)));\n text = text.replace(new RegExp('\\\\\\\\' + name.toLowerCase(), 'g'), String.fromCharCode(945 + i + (i > 16)));\n }\n\n // Subscripts.\n for(var i = 0; i < 10; i++) {\n text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));\n }\n text = text.replace(new RegExp('_a', 'g'), String.fromCharCode(8336));\n return text;\n };\n\n Util.prototype.drawArrow = function(c, x, y, angle) {\n // Draw an arrow head on the graphics context c at (x, y) with given angle.\n\n var dx = Math.cos(angle);\n var dy = Math.sin(angle);\n c.beginPath();\n c.moveTo(x, y);\n c.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);\n c.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);\n c.fill();\n };\n\n Util.prototype.det = function(a, b, c, d, e, f, g, h, i) {\n // Determinant of given matrix elements.\n return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g;\n };\n\n Util.prototype.vectorMagnitude = function(v){\n // Returns magnitude (length) of a vector v\n return Math.sqrt(v.x * v.x + v.y * v.y);\n };\n\n Util.prototype.scalarProjection = function(a, b) {\n // Returns scalar projection of vector a onto vector b\n return (a.x * b.x + a.y * b.y) / this.vectorMagnitude(b);\n };\n\n Util.prototype.isCCW = function(a, b) {\n // Returns true iff vector b is in a counter-clockwise orientation relative to a\n return (a.x * b.y) - (b.x * a.y) > 0;\n };\n\n Util.prototype.circleFromThreePoints = function(x1, y1, x2, y2, x3, y3) {\n // Return {x, y, radius} of circle through (x1, y1), (x2, y2), (x3, y3).\n var a = this.det(x1, y1, 1, x2, y2, 1, x3, y3, 1);\n var bx = -this.det(x1 * x1 + y1 * y1, y1, 1, x2 * x2 + y2 * y2, y2, 1, x3 * x3 + y3 * y3, y3, 1);\n var by = this.det(x1 * x1 + y1 * y1, x1, 1, x2 * x2 + y2 * y2, x2, 1, x3 * x3 + y3 * y3, x3, 1);\n var c = -this.det(x1 * x1 + y1 * y1, x1, y1, x2 * x2 + y2 * y2, x2, y2, x3 * x3 + y3 * y3, x3, y3);\n return {\n 'x': -bx / (2 * a),\n 'y': -by / (2 * a),\n 'radius': Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))\n };\n };\n\n Util.prototype.isInside = function(pos, rect) {\n // True iff given point pos is inside rectangle.\n return pos.x > rect.x && pos.x < rect.x + rect.width && pos.y < rect.y + rect.height && pos.y > rect.y;\n };\n\n Util.prototype.crossBrowserKey = function(e) {\n // Return which key was pressed, given the event, in a browser-independent way.\n e = e || window.event;\n return e.which || e.keyCode;\n };\n\n\n Util.prototype.crossBrowserRelativeMousePos = function(e) {\n // Earlier complex version was breaking in Moodle 4, so replaced\n // with this much simpler version that should work with all modern\n // browsers.\n const rect = e.target.getBoundingClientRect();\n const x = e.clientX - rect.left; //x position within the element.\n const y = e.clientY - rect.top; //y position within the element.\n return {'x': x, 'y': y};\n };\n\n return new Util();\n});\n"],"names":["define","Util","greekLetterNames","prototype","convertLatexShortcuts","text","i","this","length","name","replace","RegExp","String","fromCharCode","toLowerCase","drawArrow","c","x","y","angle","dx","Math","cos","dy","sin","beginPath","moveTo","lineTo","fill","det","a","b","d","e","f","g","h","vectorMagnitude","v","sqrt","scalarProjection","isCCW","circleFromThreePoints","x1","y1","x2","y2","x3","y3","bx","by","abs","isInside","pos","rect","width","height","crossBrowserKey","window","event","which","keyCode","crossBrowserRelativeMousePos","target","getBoundingClientRect","clientX","left","clientY","top"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"graphutil.min.js","sources":["../src/graphutil.js"],"sourcesContent":["/***********************************************************************\n *\n * Utility functions/data/constants for the ui_graph module.\n *\n ***********************************************************************/\n// Most of this code is taken from Finite State Machine Designer:\n/*\n Finite State Machine Designer (http://madebyevan.com/fsm/)\n License: MIT License (see below)\n Copyright (c) 2010 Evan Wallace\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n// 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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n\ndefine(function() {\n /**\n * Contstructor for the Util class.\n */\n function Util() {\n\n this.greekLetterNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',\n 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda',\n 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',\n 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega' ];\n }\n\n Util.prototype.convertLatexShortcuts = function(text) {\n // Html greek characters.\n for(var i = 0; i < this.greekLetterNames.length; i++) {\n var name = this.greekLetterNames[i];\n text = text.replace(new RegExp('\\\\\\\\' + name, 'g'), String.fromCharCode(913 + i + (i > 16)));\n text = text.replace(new RegExp('\\\\\\\\' + name.toLowerCase(), 'g'), String.fromCharCode(945 + i + (i > 16)));\n }\n\n // Subscripts.\n for(var i = 0; i < 10; i++) {\n text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));\n }\n text = text.replace(new RegExp('_a', 'g'), String.fromCharCode(8336));\n return text;\n };\n\n Util.prototype.drawArrow = function(c, x, y, angle) {\n // Draw an arrow head on the graphics context c at (x, y) with given angle.\n\n var dx = Math.cos(angle);\n var dy = Math.sin(angle);\n c.beginPath();\n c.moveTo(x, y);\n c.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);\n c.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);\n c.fill();\n };\n\n Util.prototype.det = function(a, b, c, d, e, f, g, h, i) {\n // Determinant of given matrix elements.\n return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g;\n };\n\n Util.prototype.vectorMagnitude = function(v){\n // Returns magnitude (length) of a vector v\n return Math.sqrt(v.x * v.x + v.y * v.y);\n };\n\n Util.prototype.scalarProjection = function(a, b) {\n // Returns scalar projection of vector a onto vector b\n return (a.x * b.x + a.y * b.y) / this.vectorMagnitude(b);\n };\n\n Util.prototype.isCCW = function(a, b) {\n // Returns true iff vector b is in a counter-clockwise orientation relative to a\n return (a.x * b.y) - (b.x * a.y) > 0;\n };\n\n Util.prototype.circleFromThreePoints = function(x1, y1, x2, y2, x3, y3) {\n // Return {x, y, radius} of circle through (x1, y1), (x2, y2), (x3, y3).\n var a = this.det(x1, y1, 1, x2, y2, 1, x3, y3, 1);\n var bx = -this.det(x1 * x1 + y1 * y1, y1, 1, x2 * x2 + y2 * y2, y2, 1, x3 * x3 + y3 * y3, y3, 1);\n var by = this.det(x1 * x1 + y1 * y1, x1, 1, x2 * x2 + y2 * y2, x2, 1, x3 * x3 + y3 * y3, x3, 1);\n var c = -this.det(x1 * x1 + y1 * y1, x1, y1, x2 * x2 + y2 * y2, x2, y2, x3 * x3 + y3 * y3, x3, y3);\n return {\n 'x': -bx / (2 * a),\n 'y': -by / (2 * a),\n 'radius': Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))\n };\n };\n\n Util.prototype.isInside = function(pos, rect) {\n // True iff given point pos is inside rectangle.\n return pos.x > rect.x && pos.x < rect.x + rect.width && pos.y < rect.y + rect.height && pos.y > rect.y;\n };\n\n Util.prototype.crossBrowserKey = function(e) {\n // Return which key was pressed, given the event, in a browser-independent way.\n e = e || window.event;\n return e.which || e.keyCode;\n };\n\n\n Util.prototype.crossBrowserRelativeMousePos = function(e) {\n // Earlier complex version was breaking in Moodle 4, so replaced\n // with this much simpler version that should work with all modern\n // browsers.\n const rect = e.target.getBoundingClientRect();\n const x = e.clientX - rect.left; //x position within the element.\n const y = e.clientY - rect.top; //y position within the element.\n return {'x': x, 'y': y};\n };\n\n return new Util();\n});\n"],"names":["define","Util","greekLetterNames","prototype","convertLatexShortcuts","text","i","this","length","name","replace","RegExp","String","fromCharCode","toLowerCase","drawArrow","c","x","y","angle","dx","Math","cos","dy","sin","beginPath","moveTo","lineTo","fill","det","a","b","d","e","f","g","h","vectorMagnitude","v","sqrt","scalarProjection","isCCW","circleFromThreePoints","x1","y1","x2","y2","x3","y3","bx","by","abs","isInside","pos","rect","width","height","crossBrowserKey","window","event","which","keyCode","crossBrowserRelativeMousePos","target","getBoundingClientRect","clientX","left","clientY","top"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/outputdisplayarea.min.js b/amd/build/outputdisplayarea.min.js
index cbda09b8b..bc947d8a2 100644
--- a/amd/build/outputdisplayarea.min.js
+++ b/amd/build/outputdisplayarea.min.js
@@ -1,28 +1,3 @@
-define("qtype_coderunner/outputdisplayarea",["exports","core/ajax","core/str"],(function(_exports,_ajax,_str){var obj;
-/**
- * A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally
- * developed for use in the Scratchpad UI. It has three modes of operation:
- * - 'text': Just display the output as text, html escaped.
- * - 'json': The recommended way to display programs that use stdin or output images (or both).
- * - Accepts JSON in the CRWS response output with fields:
- * - "returncode": Error/return code from running program.
- * - "stdout": Stdout text from running program.
- * - "stderr": Error text from running program.
- * - "files": An object containing filenames mapped to base64 encoded images.
- * These will be displayed below any stdout text.
- * - When input from stdin is required the returncode 42 should be returned, raise this
- * any time the program asks for input. An (html) input will be added after the last stdout received.
- * When enter is pressed, runCode is called with value of the input added to the stdin string.
- * This repeats until returncode is no longer 42.
- * - 'html': Display program output as raw html inside the output area.
- * - This can be used to show images and insert other HTML tags (and beyond).
- * - Giving an tag the class 'coderunner-run-input' will add an event that
- * on pressing enter will call the runCode method again with the value of that input field added to stdin.
- * This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.
- *
- * @module qtype_coderunner/outputdisplayarea
- * @copyright James Napier, 2023, The University of Canterbury
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.OutputDisplayArea=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};const JSON_DISPLAY_PROPS=["returncode","stdout","stderr","files"],setLangString=async function(langStringName,node,callback){let additionalText=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"",message=await(0,_str.get_string)(langStringName,"qtype_coderunner");langStringName.includes("error")&&(message="*** "+message+" ***\n"),additionalText&&(message+=additionalText),callback instanceof Function?callback(node,message):node.innerText=message},combinedOutput=response=>response.cmpinfo+response.output+response.stderr,getImage=function(base64){let type=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"png";const image=document.createElement("img");return image.src="data:image/".concat(type,";base64,").concat(base64),image};_exports.OutputDisplayArea=class{constructor(displayAreaId,outputMode,lang,sandboxParams){this.displayAreaId=displayAreaId,this.lang=lang,this.mode=outputMode,this.sandboxParams=sandboxParams,this.textDisplay=document.getElementById(displayAreaId+"-text"),this.imageDisplay=document.getElementById(displayAreaId+"-images"),this.prevRunSettings=null}clearDisplay(){this.textDisplay.innerHTML="",this.imageDisplay.innerHTML=""}displayText(response){this.textDisplay.innerText=combinedOutput(response)}displayHtml(response){this.textDisplay.innerHTML=combinedOutput(response);const inputEl=this.textDisplay.querySelector(".coderunner-run-input");inputEl&&this.addInputEvents(inputEl)}displayJson(response){const result=this.validateJson(response.output);let text=result.stdout;42!==result.returncode&&(text+=result.stderr),13==result.returncode&&setLangString("error_timeout",this.textDisplay,((node,msg)=>{node.innerText+=msg}));const numImages=this.displayImages(result.files);""===text.trim()&&42!==result.returncode?0==numImages&&this.displayNoOutput(null):this.textDisplay.innerText=text,42===result.returncode&&this.addInput()}validateJson(jsonString){let result=null;try{result=JSON.parse(jsonString)}catch(e){window.alert("Error parsing display JSON output: \n"+"'".concat(jsonString,"\n'")+"Error Msg: \n"+" ".concat(e.message," \n")+"The question author must fix this!")}const missing=((obj,props)=>props.filter((prop=>!obj.hasOwnProperty(prop))))(result,JSON_DISPLAY_PROPS);return missing.length>0&&window.alert("Display JSON (in response.result) is missing the following fields: \n"+"".concat(missing.join()," \n")+"The question author must fix this!"),result}displayNoOutput(response){const isNoOutput=!response||0===combinedOutput(response).length;if(isNoOutput||null===response){const span=document.createElement("span");span.style.color="red",setLangString("nooutput",span),this.clearDisplay(),this.textDisplay.append(span)}return isNoOutput}display(response){const error=(response=>{const ERROR_RESPONSES=[[1,0,"error_access_denied"],[2,0,"error_unknown_language"],[3,0,"error_access_denied"],[4,0,"error_submission_limit_reached"],[5,0,"error_sandbox_server_overload"],[0,11,""],[0,12,""],[0,13,"error_timeout"],[0,15,""],[0,17,"error_memory_limit"],[0,21,"error_sandbox_server_overload"],[0,30,"error_excessive_output"]];for(let i=0;i2&&void 0!==arguments[2]&&arguments[2];this.prevRunSettings=[code,stdin],shouldClearDisplay&&this.clearDisplay(),_ajax.default.call([{methodname:"qtype_coderunner_run_in_sandbox",args:{contextid:M.cfg.contextid,sourcecode:code,language:this.lang,stdin:stdin,params:JSON.stringify(this.sandboxParams)},done:responseJson=>{const response=JSON.parse(responseJson);this.display(response)},fail:error=>{alert(error.message)}}])}addInput(){const inputId="".concat(this.displayAreaId,"-input-field");this.textDisplay.innerHTML+='');const inputEl=document.getElementById(inputId);setLangString("enter_to_submit",inputEl,((node,msg)=>{node.placeholder+=msg})),this.addInputEvents(inputEl)}addInputEvents(inputEl){inputEl.focus(),inputEl.addEventListener("keydown",(e=>{13===e.keyCode&&e.preventDefault()})),inputEl.addEventListener("keyup",(e=>{if(13===e.keyCode){const line=inputEl.value;inputEl.remove(),this.textDisplay.innterHTML+=line,this.prevRunSettings[1]+=line+"\n",this.runCode(...this.prevRunSettings,!1)}}))}displayImages(files){let numImages=0;for(const[fname,fcontents]of Object.entries(files)){const fileType=fname.split(".")[1];if(fileType){const image=getImage(fcontents,fileType);this.imageDisplay.append(image),numImages+=1}else window.alert('Could not read filename correctly: "'.concat(fname,'"'))}return numImages}}}));
+define("qtype_coderunner/outputdisplayarea",["exports","core/ajax","core/str"],(function(_exports,_ajax,_str){var obj;function _slicedToArray(arr,i){return function(arr){if(Array.isArray(arr))return arr}(arr)||function(arr,i){var _i=null==arr?null:"undefined"!=typeof Symbol&&arr[Symbol.iterator]||arr["@@iterator"];if(null==_i)return;var _s,_e,_arr=[],_n=!0,_d=!1;try{for(_i=_i.call(arr);!(_n=(_s=_i.next()).done)&&(_arr.push(_s.value),!i||_arr.length!==i);_n=!0);}catch(err){_d=!0,_e=err}finally{try{_n||null==_i.return||_i.return()}finally{if(_d)throw _e}}return _arr}(arr,i)||_unsupportedIterableToArray(arr,i)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function _toConsumableArray(arr){return function(arr){if(Array.isArray(arr))return _arrayLikeToArray(arr)}(arr)||function(iter){if("undefined"!=typeof Symbol&&null!=iter[Symbol.iterator]||null!=iter["@@iterator"])return Array.from(iter)}(arr)||_unsupportedIterableToArray(arr)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function _unsupportedIterableToArray(o,minLen){if(o){if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);return"Object"===n&&o.constructor&&(n=o.constructor.name),"Map"===n||"Set"===n?Array.from(o):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?_arrayLikeToArray(o,minLen):void 0}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i3&&void 0!==_args[3]?_args[3]:"",_context.next=3,(0,_str.get_string)(langStringName,"qtype_coderunner");case 3:message=_context.sent,langStringName.includes("error")&&(message="*** "+message+" ***\n"),additionalText&&(message+=additionalText),callback instanceof Function?callback(node,message):node.innerText=message;case 7:case"end":return _context.stop()}}),_callee)})),_ref=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(_x,_x2,_x3){return _ref.apply(this,arguments)}),combinedOutput=function(response){return response.cmpinfo+response.output+response.stderr},getImage=function(base64){var type=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"png",image=document.createElement("img");return image.src="data:image/".concat(type,";base64,").concat(base64),image},OutputDisplayArea=function(){function OutputDisplayArea(displayAreaId,outputMode,lang,sandboxParams){!function(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}(this,OutputDisplayArea),this.displayAreaId=displayAreaId,this.lang=lang,this.mode=outputMode,this.sandboxParams=sandboxParams,this.textDisplay=document.getElementById(displayAreaId+"-text"),this.imageDisplay=document.getElementById(displayAreaId+"-images"),this.prevRunSettings=null}var Constructor,protoProps,staticProps;return Constructor=OutputDisplayArea,protoProps=[{key:"clearDisplay",value:function(){this.textDisplay.innerHTML="",this.imageDisplay.innerHTML=""}},{key:"displayText",value:function(response){this.textDisplay.innerText=combinedOutput(response)}},{key:"displayHtml",value:function(response){this.textDisplay.innerHTML=combinedOutput(response);var inputEl=this.textDisplay.querySelector(".coderunner-run-input");inputEl&&this.addInputEvents(inputEl)}},{key:"displayJson",value:function(response){var result=this.validateJson(response.output),text=result.stdout;42!==result.returncode&&(text+=result.stderr),13==result.returncode&&setLangString("error_timeout",this.textDisplay,(function(node,msg){node.innerText+=msg}));var numImages=this.displayImages(result.files);""===text.trim()&&42!==result.returncode?0==numImages&&this.displayNoOutput(null):this.textDisplay.innerText=text,42===result.returncode&&this.addInput()}},{key:"validateJson",value:function(jsonString){var result=null;try{result=JSON.parse(jsonString)}catch(e){window.alert("Error parsing display JSON output: \n"+"'".concat(jsonString,"\n'")+"Error Msg: \n"+" ".concat(e.message," \n")+"The question author must fix this!")}var missing=function(obj,props){return props.filter((function(prop){return!obj.hasOwnProperty(prop)}))}(result,JSON_DISPLAY_PROPS);return missing.length>0&&window.alert("Display JSON (in response.result) is missing the following fields: \n"+"".concat(missing.join()," \n")+"The question author must fix this!"),result}},{key:"displayNoOutput",value:function(response){var isNoOutput=!response||0===combinedOutput(response).length;if(isNoOutput||null===response){var span=document.createElement("span");span.style.color="red",setLangString("nooutput",span),this.clearDisplay(),this.textDisplay.append(span)}return isNoOutput}},{key:"display",value:function(response){var error=function(response){for(var ERROR_RESPONSES=[[1,0,"error_access_denied"],[2,0,"error_unknown_language"],[3,0,"error_access_denied"],[4,0,"error_submission_limit_reached"],[5,0,"error_sandbox_server_overload"],[0,11,""],[0,12,""],[0,13,"error_timeout"],[0,15,""],[0,17,"error_memory_limit"],[0,21,"error_sandbox_server_overload"],[0,30,"error_excessive_output"]],i=0;i2&&void 0!==arguments[2]&&arguments[2];this.prevRunSettings=[code,stdin],shouldClearDisplay&&this.clearDisplay(),_ajax.default.call([{methodname:"qtype_coderunner_run_in_sandbox",args:{contextid:M.cfg.contextid,sourcecode:code,language:this.lang,stdin:stdin,params:JSON.stringify(this.sandboxParams)},done:function(responseJson){var response=JSON.parse(responseJson);_this.display(response)},fail:function(error){alert(error.message)}}])}},{key:"addInput",value:function(){var inputId="".concat(this.displayAreaId,"-input-field");this.textDisplay.innerHTML+='');var inputEl=document.getElementById(inputId);setLangString("enter_to_submit",inputEl,(function(node,msg){node.placeholder+=msg})),this.addInputEvents(inputEl)}},{key:"addInputEvents",value:function(inputEl){var _this2=this;inputEl.focus(),inputEl.addEventListener("keydown",(function(e){13===e.keyCode&&e.preventDefault()})),inputEl.addEventListener("keyup",(function(e){if(13===e.keyCode){var line=inputEl.value;inputEl.remove(),_this2.textDisplay.innterHTML+=line,_this2.prevRunSettings[1]+=line+"\n",_this2.runCode.apply(_this2,_toConsumableArray(_this2.prevRunSettings).concat([!1]))}}))}},{key:"displayImages",value:function(files){for(var numImages=0,_i=0,_Object$entries=Object.entries(files);_i<_Object$entries.length;_i++){var _Object$entries$_i=_slicedToArray(_Object$entries[_i],2),fname=_Object$entries$_i[0],fcontents=_Object$entries$_i[1],fileType=fname.split(".")[1];if(fileType){var image=getImage(fcontents,fileType);this.imageDisplay.append(image),numImages+=1}else window.alert('Could not read filename correctly: "'.concat(fname,'"'))}return numImages}}],protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),OutputDisplayArea}();_exports.OutputDisplayArea=OutputDisplayArea}));
//# sourceMappingURL=outputdisplayarea.min.js.map
\ No newline at end of file
diff --git a/amd/build/outputdisplayarea.min.js.map b/amd/build/outputdisplayarea.min.js.map
index 97c596809..48cda1c39 100644
--- a/amd/build/outputdisplayarea.min.js.map
+++ b/amd/build/outputdisplayarea.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"outputdisplayarea.min.js","sources":["../src/outputdisplayarea.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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/**\n * A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally\n * developed for use in the Scratchpad UI. It has three modes of operation:\n * - 'text': Just display the output as text, html escaped.\n * - 'json': The recommended way to display programs that use stdin or output images (or both).\n * - Accepts JSON in the CRWS response output with fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * - When input from stdin is required the returncode 42 should be returned, raise this\n * any time the program asks for input. An (html) input will be added after the last stdout received.\n * When enter is pressed, runCode is called with value of the input added to the stdin string.\n * This repeats until returncode is no longer 42.\n * - 'html': Display program output as raw html inside the output area.\n * - This can be used to show images and insert other HTML tags (and beyond).\n * - Giving an tag the class 'coderunner-run-input' will add an event that\n * on pressing enter will call the runCode method again with the value of that input field added to stdin.\n * This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.\n *\n * @module qtype_coderunner/outputdisplayarea\n * @copyright James Napier, 2023, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\nimport {get_string as getLangString} from 'core/str';\n\n\nconst ENTER_KEY = 13;\nconst INPUT_INTERRUPT = 42;\nconst RESULT_SUCCESS = 15;\nconst INPUT_CLASS = 'coderunner-run-input';\nconst JSON_DISPLAY_PROPS = ['returncode', 'stdout', 'stderr', 'files'];\n\n\n/**\n * Get the specified language string using\n * AJAX and plug it into the given textarea\n * @param {string} langStringName The language string name.\n * @param {DOMnode} node area into which the error message\n * should be plugged.\n * @param {function} callback Callback function, with two arguments: node, message.\n * @param {string} additionalText Extra text to follow the result code.\n */\nconst setLangString = async(langStringName, node, callback, additionalText = '') => {\n let message = await getLangString(langStringName, 'qtype_coderunner');\n if (langStringName.includes('error')) {\n message = \"*** \" + message + \" ***\\n\";\n }\n if (additionalText) {\n message += additionalText;\n }\n if (callback instanceof Function) {\n callback(node, message);\n } else {\n node.innerText = message;\n }\n};\n\nconst diagnoseWebserviceResponse = (response) => {\n // Table of error conditions.\n // Each row is response.error, response.result, langstring\n // response.result is ignored if response.error is non-zero.\n // Any condition not in the table is deemed an \"Unknown runtime error\".\n const ERROR_RESPONSES = [\n [1, 0, 'error_access_denied'], // Sandbox AUTH_ERROR\n [2, 0, 'error_unknown_language'], // Sandbox WRONG_LANG_ID\n [3, 0, 'error_access_denied'], // Sandbox ACCESS_DENIED\n [4, 0, 'error_submission_limit_reached'], // Sandbox SUBMISSION_LIMIT_EXCEEDED\n [5, 0, 'error_sandbox_server_overload'], // Sandbox SERVER_OVERLOAD\n [0, 11, ''], // RESULT_COMPILATION_ERROR\n [0, 12, ''], // RESULT_RUNTIME_ERROR\n [0, 13, 'error_timeout'], // RESULT TIME_LIMIT\n [0, RESULT_SUCCESS, ''], // RESULT_SUCCESS\n [0, 17, 'error_memory_limit'], // RESULT_MEMORY_LIMIT\n [0, 21, 'error_sandbox_server_overload'], // RESULT_SERVER_OVERLOAD\n [0, 30, 'error_excessive_output'] // RESULT OUTPUT_LIMIT\n ];\n for (let i = 0; i < ERROR_RESPONSES.length; i++) {\n let row = ERROR_RESPONSES[i];\n if (row[0] == response.error && (response.error != 0 || response.result == row[1])) {\n return row[2];\n }\n }\n return 'error_unknown_runtime'; // We're dead, Fred.\n};\n\n/**\n * Concatenates the cmpinfo, stdout and stderr fields of the sandbox\n * response, truncating both stdout and stderr to a given maximum length\n * if necessary (in which case '... (truncated)' is appended.\n * @param {object} response Sandbox response object\n */\nconst combinedOutput = (response) => {\n return response.cmpinfo + response.output + response.stderr;\n};\n\n/**\n * Check whether obj has the properties in props, returns missing properties.\n * @param {object} obj to check properties of\n * @param {array} props to check for.\n * @returns {array} of missing properties.\n */\nconst missingProperties = (obj, props) => {\n return props.filter(prop => !obj.hasOwnProperty(prop));\n};\n\n/**\n * Insert a base64 encoded string into HTML image.\n * @param {string} base64 encoded string.\n * @param {string} type of encoded image file.\n * @returns {*|jQuery|HTMLElement} image tag containing encoded image from string.\n */\nconst getImage = (base64, type = 'png') => {\n const image = document.createElement('img');\n image.src = `data:image/${type};base64,${base64}`;\n return image;\n};\n\n/**\n * Constructor for OutputDisplayArea object. For use with the output_displayarea template.\n * @param {string} displayAreaId The id of the display area div, this should match the 'id'\n * from the template.\n * @param {string} outputMode The mode being used for output, must be text, html or json.\n * @param {string} lang The language to run code with.\n * @param {string} sandboxParams The sandbox params to run code with.\n */\nclass OutputDisplayArea {\n constructor(displayAreaId, outputMode, lang, sandboxParams) {\n this.displayAreaId = displayAreaId;\n this.lang = lang;\n this.mode = outputMode;\n this.sandboxParams = sandboxParams;\n\n this.textDisplay = document.getElementById(displayAreaId + '-text');\n this.imageDisplay = document.getElementById(displayAreaId + '-images');\n\n this.prevRunSettings = null;\n }\n\n /**\n * Clear the display of any images and text.\n */\n clearDisplay() {\n this.textDisplay.innerHTML = \"\";\n this.imageDisplay.innerHTML = \"\";\n }\n\n /**\n * Display text from a CRWS response to the display (escaped).\n * @param {object} response Coderunner webservice response JSON.\n */\n displayText(response) {\n this.textDisplay.innerText = combinedOutput(response);\n }\n\n /**\n * Display HTML from a CRWS response to the display (un-escaped).\n * Find the first HTML input element with the input class and\n * add event listeners to handle reading stdin.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing HTML.\n */\n displayHtml(response) {\n this.textDisplay.innerHTML = combinedOutput(response);\n const inputEl = this.textDisplay.querySelector('.' + INPUT_CLASS);\n if (inputEl) {\n this.addInputEvents(inputEl);\n }\n }\n\n /**\n * Display JSON from a CRWS response to the display.\n * Assumes response.output will be a JSON with the fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * NOTE: See file header/readme for more info.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing JSON string.\n */\n displayJson(response) {\n const result = this.validateJson(response.output);\n\n let text = result.stdout;\n\n if (result.returncode !== INPUT_INTERRUPT) {\n text += result.stderr;\n }\n if (result.returncode == 13) { // Timeout\n setLangString('error_timeout', this.textDisplay, (node, msg) => {\n node.innerText += msg;\n });\n }\n\n const numImages = this.displayImages(result.files);\n if (text.trim() === '' && result.returncode !== INPUT_INTERRUPT) {\n if (numImages == 0) {\n this.displayNoOutput(null);\n }\n } else {\n this.textDisplay.innerText = text;\n }\n if (result.returncode === INPUT_INTERRUPT) {\n this.addInput();\n }\n }\n\n /**\n * Validate JSON to display, make sure it is valid json and has required fields.\n * @param {string} jsonString string of JSON to be displayed.\n * @returns {object} JSON as object\n */\n validateJson(jsonString) {\n let result = null;\n try {\n result = JSON.parse(jsonString);\n } catch (e) {\n window.alert(\n `Error parsing display JSON output: \\n` +\n `'${jsonString}\\n'` +\n `Error Msg: \\n` +\n ` ${e.message} \\n` +\n `The question author must fix this!`\n );\n }\n\n const missing = missingProperties(result, JSON_DISPLAY_PROPS);\n if (missing.length > 0) {\n window.alert(\n `Display JSON (in response.result) is missing the following fields: \\n` +\n `${missing.join()} \\n` +\n `The question author must fix this!`\n );\n }\n return result;\n }\n\n /**\n * Display no output message if no output to display or response is null.\n * @param {object} response Coderunner webservice response JSON, set to null to force\n * display of no output message.\n */\n displayNoOutput(response) {\n const isNoOutput = response ? combinedOutput(response).length === 0 : true;\n if (isNoOutput || response === null) {\n const span = document.createElement('span');\n span.style.color = 'red';\n setLangString('nooutput', span);\n this.clearDisplay();\n this.textDisplay.append(span);\n }\n return isNoOutput;\n }\n /**\n * Display response using the current display mode.\n * @param {object} response Coderunner webservice response JSON.\n */\n display(response) {\n const error = diagnoseWebserviceResponse(response);\n if (error !== '') {\n setLangString(error, this.textDisplay);\n return;\n }\n if (this.displayNoOutput(response)) {\n return;\n }\n\n if (this.mode === 'json') {\n this.displayJson(response);\n } else if (this.mode === 'html') {\n this.displayHtml(response);\n } else if (this.mode === 'text') {\n this.displayText(response);\n } else {\n throw Error(`Invalid outputMode given: \"${this.mode}\"`);\n }\n }\n\n /**\n * Run code using the Coderunner webservice and then display the output\n * using the selected mode. This function uses AJAX to asynchronously run and\n * display code.\n * @param {string} code to be run.\n * @param {string} stdin to be fed into the program.\n * @param {boolean} shouldClearDisplay will reset the display before displaying.\n * Use false when doing stdin runs.\n */\n runCode(code, stdin, shouldClearDisplay = false) {\n this.prevRunSettings = [code, stdin];\n if (shouldClearDisplay) {\n this.clearDisplay();\n }\n ajax.call([{\n methodname: 'qtype_coderunner_run_in_sandbox',\n args: {\n contextid: M.cfg.contextid, // Moodle context ID\n sourcecode: code,\n language: this.lang,\n stdin: stdin,\n params: JSON.stringify(this.sandboxParams) // Sandbox params\n },\n done: (responseJson) => {\n const response = JSON.parse(responseJson);\n this.display(response);\n },\n fail: (error) => {\n alert(error.message);\n }\n }]);\n }\n\n /**\n * Add an input field with event listeners to support running again\n * with new stdin entered by user.\n */\n addInput() {\n const inputId = `${this.displayAreaId}-input-field`;\n this.textDisplay.innerHTML += ``;\n const inputEl = document.getElementById(inputId);\n setLangString('enter_to_submit', inputEl, (node, msg) => {\n node.placeholder += msg;\n });\n this.addInputEvents(inputEl);\n }\n\n /**\n * Add event listeners to inputEl overriding enter key to:\n * - Prevent form-submit.\n * - Call runCode again, adding value in inputEl to stdin.\n * @param {node} inputEl to add event listeners to.\n */\n addInputEvents(inputEl) {\n inputEl.focus();\n\n inputEl.addEventListener('keydown', (e) => {\n if (e.keyCode === ENTER_KEY) {\n e.preventDefault(); // Do NOT form submit.\n }\n });\n inputEl.addEventListener('keyup', (e) => {\n if (e.keyCode === ENTER_KEY) {\n const line = inputEl.value;\n inputEl.remove();\n this.textDisplay.innterHTML += line; // Perhaps this should be sanitized.\n this.prevRunSettings[1] += line + '\\n';\n this.runCode(...this.prevRunSettings, false);\n }\n });\n }\n\n /**\n * Take the files from a JSON response and display them.\n * @param {object} files from response, in filename: filecontents pairs.\n * @returns {number} number of images displayed.\n */\n displayImages(files) {\n let numImages = 0;\n for (const [fname, fcontents] of Object.entries(files)) {\n const fileType = fname.split('.')[1];\n if (fileType) {\n const image = getImage(fcontents, fileType);\n this.imageDisplay.append(image);\n numImages += 1;\n } else {\n window.alert(`Could not read filename correctly: \"${fname}\"`);\n }\n }\n return numImages;\n }\n}\n\n\nexport {\n OutputDisplayArea\n};\n"],"names":["JSON_DISPLAY_PROPS","setLangString","async","langStringName","node","callback","additionalText","message","includes","Function","innerText","combinedOutput","response","cmpinfo","output","stderr","getImage","base64","type","image","document","createElement","src","constructor","displayAreaId","outputMode","lang","sandboxParams","mode","textDisplay","getElementById","imageDisplay","prevRunSettings","clearDisplay","innerHTML","displayText","displayHtml","inputEl","this","querySelector","addInputEvents","displayJson","result","validateJson","text","stdout","returncode","msg","numImages","displayImages","files","trim","displayNoOutput","addInput","jsonString","JSON","parse","e","window","alert","missing","obj","props","filter","prop","hasOwnProperty","missingProperties","length","join","isNoOutput","span","style","color","append","display","error","ERROR_RESPONSES","i","row","diagnoseWebserviceResponse","Error","runCode","code","stdin","shouldClearDisplay","call","methodname","args","contextid","M","cfg","sourcecode","language","params","stringify","done","responseJson","fail","inputId","placeholder","focus","addEventListener","keyCode","preventDefault","line","value","remove","innterHTML","fname","fcontents","Object","entries","fileType","split"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"outputdisplayarea.min.js","sources":["../src/outputdisplayarea.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 util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/**\n * A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally\n * developed for use in the Scratchpad UI. It has three modes of operation:\n * - 'text': Just display the output as text, html escaped.\n * - 'json': The recommended way to display programs that use stdin or output images (or both).\n * - Accepts JSON in the CRWS response output with fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * - When input from stdin is required the returncode 42 should be returned, raise this\n * any time the program asks for input. An (html) input will be added after the last stdout received.\n * When enter is pressed, runCode is called with value of the input added to the stdin string.\n * This repeats until returncode is no longer 42.\n * - 'html': Display program output as raw html inside the output area.\n * - This can be used to show images and insert other HTML tags (and beyond).\n * - Giving an tag the class 'coderunner-run-input' will add an event that\n * on pressing enter will call the runCode method again with the value of that input field added to stdin.\n * This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.\n *\n * @module qtype_coderunner/outputdisplayarea\n * @copyright James Napier, 2023, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\nimport {get_string as getLangString} from 'core/str';\n\n\nconst ENTER_KEY = 13;\nconst INPUT_INTERRUPT = 42;\nconst RESULT_SUCCESS = 15;\nconst INPUT_CLASS = 'coderunner-run-input';\nconst JSON_DISPLAY_PROPS = ['returncode', 'stdout', 'stderr', 'files'];\n\n\n/**\n * Get the specified language string using\n * AJAX and plug it into the given textarea\n * @param {string} langStringName The language string name.\n * @param {DOMnode} node area into which the error message\n * should be plugged.\n * @param {function} callback Callback function, with two arguments: node, message.\n * @param {string} additionalText Extra text to follow the result code.\n */\nconst setLangString = async(langStringName, node, callback, additionalText = '') => {\n let message = await getLangString(langStringName, 'qtype_coderunner');\n if (langStringName.includes('error')) {\n message = \"*** \" + message + \" ***\\n\";\n }\n if (additionalText) {\n message += additionalText;\n }\n if (callback instanceof Function) {\n callback(node, message);\n } else {\n node.innerText = message;\n }\n};\n\nconst diagnoseWebserviceResponse = (response) => {\n // Table of error conditions.\n // Each row is response.error, response.result, langstring\n // response.result is ignored if response.error is non-zero.\n // Any condition not in the table is deemed an \"Unknown runtime error\".\n const ERROR_RESPONSES = [\n [1, 0, 'error_access_denied'], // Sandbox AUTH_ERROR\n [2, 0, 'error_unknown_language'], // Sandbox WRONG_LANG_ID\n [3, 0, 'error_access_denied'], // Sandbox ACCESS_DENIED\n [4, 0, 'error_submission_limit_reached'], // Sandbox SUBMISSION_LIMIT_EXCEEDED\n [5, 0, 'error_sandbox_server_overload'], // Sandbox SERVER_OVERLOAD\n [0, 11, ''], // RESULT_COMPILATION_ERROR\n [0, 12, ''], // RESULT_RUNTIME_ERROR\n [0, 13, 'error_timeout'], // RESULT TIME_LIMIT\n [0, RESULT_SUCCESS, ''], // RESULT_SUCCESS\n [0, 17, 'error_memory_limit'], // RESULT_MEMORY_LIMIT\n [0, 21, 'error_sandbox_server_overload'], // RESULT_SERVER_OVERLOAD\n [0, 30, 'error_excessive_output'] // RESULT OUTPUT_LIMIT\n ];\n for (let i = 0; i < ERROR_RESPONSES.length; i++) {\n let row = ERROR_RESPONSES[i];\n if (row[0] == response.error && (response.error != 0 || response.result == row[1])) {\n return row[2];\n }\n }\n return 'error_unknown_runtime'; // We're dead, Fred.\n};\n\n/**\n * Concatenates the cmpinfo, stdout and stderr fields of the sandbox\n * response, truncating both stdout and stderr to a given maximum length\n * if necessary (in which case '... (truncated)' is appended.\n * @param {object} response Sandbox response object\n */\nconst combinedOutput = (response) => {\n return response.cmpinfo + response.output + response.stderr;\n};\n\n/**\n * Check whether obj has the properties in props, returns missing properties.\n * @param {object} obj to check properties of\n * @param {array} props to check for.\n * @returns {array} of missing properties.\n */\nconst missingProperties = (obj, props) => {\n return props.filter(prop => !obj.hasOwnProperty(prop));\n};\n\n/**\n * Insert a base64 encoded string into HTML image.\n * @param {string} base64 encoded string.\n * @param {string} type of encoded image file.\n * @returns {*|jQuery|HTMLElement} image tag containing encoded image from string.\n */\nconst getImage = (base64, type = 'png') => {\n const image = document.createElement('img');\n image.src = `data:image/${type};base64,${base64}`;\n return image;\n};\n\n/**\n * Constructor for OutputDisplayArea object. For use with the output_displayarea template.\n * @param {string} displayAreaId The id of the display area div, this should match the 'id'\n * from the template.\n * @param {string} outputMode The mode being used for output, must be text, html or json.\n * @param {string} lang The language to run code with.\n * @param {string} sandboxParams The sandbox params to run code with.\n */\nclass OutputDisplayArea {\n constructor(displayAreaId, outputMode, lang, sandboxParams) {\n this.displayAreaId = displayAreaId;\n this.lang = lang;\n this.mode = outputMode;\n this.sandboxParams = sandboxParams;\n\n this.textDisplay = document.getElementById(displayAreaId + '-text');\n this.imageDisplay = document.getElementById(displayAreaId + '-images');\n\n this.prevRunSettings = null;\n }\n\n /**\n * Clear the display of any images and text.\n */\n clearDisplay() {\n this.textDisplay.innerHTML = \"\";\n this.imageDisplay.innerHTML = \"\";\n }\n\n /**\n * Display text from a CRWS response to the display (escaped).\n * @param {object} response Coderunner webservice response JSON.\n */\n displayText(response) {\n this.textDisplay.innerText = combinedOutput(response);\n }\n\n /**\n * Display HTML from a CRWS response to the display (un-escaped).\n * Find the first HTML input element with the input class and\n * add event listeners to handle reading stdin.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing HTML.\n */\n displayHtml(response) {\n this.textDisplay.innerHTML = combinedOutput(response);\n const inputEl = this.textDisplay.querySelector('.' + INPUT_CLASS);\n if (inputEl) {\n this.addInputEvents(inputEl);\n }\n }\n\n /**\n * Display JSON from a CRWS response to the display.\n * Assumes response.output will be a JSON with the fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * NOTE: See file header/readme for more info.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing JSON string.\n */\n displayJson(response) {\n const result = this.validateJson(response.output);\n\n let text = result.stdout;\n\n if (result.returncode !== INPUT_INTERRUPT) {\n text += result.stderr;\n }\n if (result.returncode == 13) { // Timeout\n setLangString('error_timeout', this.textDisplay, (node, msg) => {\n node.innerText += msg;\n });\n }\n\n const numImages = this.displayImages(result.files);\n if (text.trim() === '' && result.returncode !== INPUT_INTERRUPT) {\n if (numImages == 0) {\n this.displayNoOutput(null);\n }\n } else {\n this.textDisplay.innerText = text;\n }\n if (result.returncode === INPUT_INTERRUPT) {\n this.addInput();\n }\n }\n\n /**\n * Validate JSON to display, make sure it is valid json and has required fields.\n * @param {string} jsonString string of JSON to be displayed.\n * @returns {object} JSON as object\n */\n validateJson(jsonString) {\n let result = null;\n try {\n result = JSON.parse(jsonString);\n } catch (e) {\n window.alert(\n `Error parsing display JSON output: \\n` +\n `'${jsonString}\\n'` +\n `Error Msg: \\n` +\n ` ${e.message} \\n` +\n `The question author must fix this!`\n );\n }\n\n const missing = missingProperties(result, JSON_DISPLAY_PROPS);\n if (missing.length > 0) {\n window.alert(\n `Display JSON (in response.result) is missing the following fields: \\n` +\n `${missing.join()} \\n` +\n `The question author must fix this!`\n );\n }\n return result;\n }\n\n /**\n * Display no output message if no output to display or response is null.\n * @param {object} response Coderunner webservice response JSON, set to null to force\n * display of no output message.\n */\n displayNoOutput(response) {\n const isNoOutput = response ? combinedOutput(response).length === 0 : true;\n if (isNoOutput || response === null) {\n const span = document.createElement('span');\n span.style.color = 'red';\n setLangString('nooutput', span);\n this.clearDisplay();\n this.textDisplay.append(span);\n }\n return isNoOutput;\n }\n /**\n * Display response using the current display mode.\n * @param {object} response Coderunner webservice response JSON.\n */\n display(response) {\n const error = diagnoseWebserviceResponse(response);\n if (error !== '') {\n setLangString(error, this.textDisplay);\n return;\n }\n if (this.displayNoOutput(response)) {\n return;\n }\n\n if (this.mode === 'json') {\n this.displayJson(response);\n } else if (this.mode === 'html') {\n this.displayHtml(response);\n } else if (this.mode === 'text') {\n this.displayText(response);\n } else {\n throw Error(`Invalid outputMode given: \"${this.mode}\"`);\n }\n }\n\n /**\n * Run code using the Coderunner webservice and then display the output\n * using the selected mode. This function uses AJAX to asynchronously run and\n * display code.\n * @param {string} code to be run.\n * @param {string} stdin to be fed into the program.\n * @param {boolean} shouldClearDisplay will reset the display before displaying.\n * Use false when doing stdin runs.\n */\n runCode(code, stdin, shouldClearDisplay = false) {\n this.prevRunSettings = [code, stdin];\n if (shouldClearDisplay) {\n this.clearDisplay();\n }\n ajax.call([{\n methodname: 'qtype_coderunner_run_in_sandbox',\n args: {\n contextid: M.cfg.contextid, // Moodle context ID\n sourcecode: code,\n language: this.lang,\n stdin: stdin,\n params: JSON.stringify(this.sandboxParams) // Sandbox params\n },\n done: (responseJson) => {\n const response = JSON.parse(responseJson);\n this.display(response);\n },\n fail: (error) => {\n alert(error.message);\n }\n }]);\n }\n\n /**\n * Add an input field with event listeners to support running again\n * with new stdin entered by user.\n */\n addInput() {\n const inputId = `${this.displayAreaId}-input-field`;\n this.textDisplay.innerHTML += ``;\n const inputEl = document.getElementById(inputId);\n setLangString('enter_to_submit', inputEl, (node, msg) => {\n node.placeholder += msg;\n });\n this.addInputEvents(inputEl);\n }\n\n /**\n * Add event listeners to inputEl overriding enter key to:\n * - Prevent form-submit.\n * - Call runCode again, adding value in inputEl to stdin.\n * @param {node} inputEl to add event listeners to.\n */\n addInputEvents(inputEl) {\n inputEl.focus();\n\n inputEl.addEventListener('keydown', (e) => {\n if (e.keyCode === ENTER_KEY) {\n e.preventDefault(); // Do NOT form submit.\n }\n });\n inputEl.addEventListener('keyup', (e) => {\n if (e.keyCode === ENTER_KEY) {\n const line = inputEl.value;\n inputEl.remove();\n this.textDisplay.innterHTML += line; // Perhaps this should be sanitized.\n this.prevRunSettings[1] += line + '\\n';\n this.runCode(...this.prevRunSettings, false);\n }\n });\n }\n\n /**\n * Take the files from a JSON response and display them.\n * @param {object} files from response, in filename: filecontents pairs.\n * @returns {number} number of images displayed.\n */\n displayImages(files) {\n let numImages = 0;\n for (const [fname, fcontents] of Object.entries(files)) {\n const fileType = fname.split('.')[1];\n if (fileType) {\n const image = getImage(fcontents, fileType);\n this.imageDisplay.append(image);\n numImages += 1;\n } else {\n window.alert(`Could not read filename correctly: \"${fname}\"`);\n }\n }\n return numImages;\n }\n}\n\n\nexport {\n OutputDisplayArea\n};\n"],"names":["JSON_DISPLAY_PROPS","setLangString","langStringName","node","callback","additionalText","message","includes","Function","innerText","combinedOutput","response","cmpinfo","output","stderr","getImage","base64","type","image","document","createElement","src","OutputDisplayArea","displayAreaId","outputMode","lang","sandboxParams","mode","textDisplay","getElementById","imageDisplay","prevRunSettings","innerHTML","inputEl","this","querySelector","addInputEvents","result","validateJson","text","stdout","returncode","msg","numImages","displayImages","files","trim","displayNoOutput","addInput","jsonString","JSON","parse","e","window","alert","missing","obj","props","filter","prop","hasOwnProperty","missingProperties","length","join","isNoOutput","span","style","color","clearDisplay","append","error","ERROR_RESPONSES","i","row","diagnoseWebserviceResponse","displayJson","displayHtml","Error","displayText","code","stdin","shouldClearDisplay","call","methodname","args","contextid","M","cfg","sourcecode","language","params","stringify","done","responseJson","_this","display","fail","inputId","placeholder","focus","addEventListener","keyCode","preventDefault","line","value","remove","_this2","innterHTML","runCode","Object","entries","fname","fcontents","fileType","split"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/textareas.min.js b/amd/build/textareas.min.js
index f7c2df89a..3826a6463 100644
--- a/amd/build/textareas.min.js
+++ b/amd/build/textareas.min.js
@@ -6,6 +6,6 @@
* @copyright Richard Lobb, 2015, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/textareas",["jquery"],(function($){function initTextArea(){$(this).data("clickInProgress",!1),$(this).data("capturingTab",!0),$(this).on("mousedown",(function(){$(this).data("clickInProgress",!0)})),$(this).on("focusin",(function(){$(this).data("capturingTab",$(this).data("clickInProgress"))})),$(this).on("click",(function(){$(this).data("clickInProgress",!1)})),$(this).on("keydown",(function(e){if(!(window.hasOwnProperty("behattesting")&&window.behattesting||void 0!==e.which&&0===e.which))if(9==e.keyCode&&$(this).data("capturingTab"))(e.shiftKey||insertString(this," "))&&e.preventDefault();else if(13===e.keyCode&&void 0!==this.selectionStart){for(var before=this.value.substring(0,this.selectionStart),eol=before.lastIndexOf("\n"),line=before.substring(eol+1),indent="",i=0;i{div.is(":visible")?(div.hide(),hdr.innerText=hdrText.replace("▾","▸")):(div.show(),div.trigger("mousemove"),hdr.innerText=hdrText.replace("▸","▾"))}))}}}));
+define("qtype_coderunner/textareas",["jquery"],(function($){function initTextArea(){$(this).data("clickInProgress",!1),$(this).data("capturingTab",!0),$(this).on("mousedown",(function(){$(this).data("clickInProgress",!0)})),$(this).on("focusin",(function(){$(this).data("capturingTab",$(this).data("clickInProgress"))})),$(this).on("click",(function(){$(this).data("clickInProgress",!1)})),$(this).on("keydown",(function(e){if(!(window.hasOwnProperty("behattesting")&&window.behattesting||void 0!==e.which&&0===e.which))if(9==e.keyCode&&$(this).data("capturingTab"))(e.shiftKey||insertString(this," "))&&e.preventDefault();else if(13===e.keyCode&&void 0!==this.selectionStart){for(var before=this.value.substring(0,this.selectionStart),eol=before.lastIndexOf("\n"),line=before.substring(eol+1),indent="",i=0;i.\n */\n\n/**\n * JavaScript for handling textareas and form actions in CodeRunner question\n * editing forms and student question answering forms.\n *\n * @module qtype_coderunner/textareas\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Function to initialise all code-input text-areas in a page.\n * Used by the form editor but can't be used for question text areas as\n * renderer.php is called once for each question in a quiz, and there is\n * no communication between the questions.\n */\n function setupAllTAs() {\n $('textarea.edit_code').each(initTextArea);\n }\n\n /**\n * Initialise a particular text area (TA), given its ID (which may contain\n * colons, so can't use jQuery selector).\n * @param {string} taId The ID of the textarea html element.\n */\n function initQuestionTA(taId) {\n $(document.getElementById(taId)).each(initTextArea);\n }\n\n\n /**\n * Set up to expand or collapse the question author's answer when the user clicks\n * on the show/hide answer button.\n * @param {string} linkId The ID of the link that the user clicks.\n * @param {sting} divId The ID of the div to be shown or hidden.\n */\n function setupShowHideAnswer(linkId, divId) {\n let link = $(document.getElementById(linkId)),\n div = $(document.getElementById(divId)),\n hdr = link.children()[0], // The
element.\n hdrText = hdr.innerText; // Its body.\n link.click(() => {\n if (div.is(\":visible\")) {\n div.hide();\n hdr.innerText = hdrText.replace(\"\\u25BE\", \"\\u25B8\");\n } else {\n div.show();\n div.trigger('mousemove'); // So user sees contents.\n hdr.innerText = hdrText.replace(\"\\u25B8\", \"\\u25BE\");\n }\n });\n }\n\n /**\n * Set up the JavaScript to handle a text area 'this'.\n * It just does rudimentary autoindent on return and replaces tabs with\n * 4 spaces always.\n * For info on key handling browser inconsistencies see\n * http://unixpapa.com/js/key.html\n * If the AceEditor is handling a text area, this code is unused as\n * the actual textarea is hidden by Ace.\n */\n function initTextArea() {\n var TAB = 9,\n ENTER = 13,\n ESC = 27,\n KEY_M = 77;\n\n $(this).data('clickInProgress', false);\n $(this).data('capturingTab', true);\n\n $(this).on('mousedown', function() {\n /*\n * Event order seems to be (\\ is where the mouse button is pressed, / released):\n * Chrome: \\ mousedown, mouseup, focusin / click.\n * Firefox/IE: \\ mousedown, focusin / mouseup, click.\n */\n $(this).data('clickInProgress', true);\n });\n\n $(this).on('focusin', function() {\n /*\n * At first, pressing TAB moves focus.\n */\n $(this).data('capturingTab', $(this).data('clickInProgress'));\n });\n\n $(this).on('click', function() {\n $(this).data('clickInProgress', false);\n });\n\n $(this).on('keydown', function(e) {\n /*\n * Don't autoindent when behat testing in progress.\n */\n if (window.hasOwnProperty('behattesting') && window.behattesting) { return; }\n\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode == TAB && $(this).data('capturingTab')) {\n /*\n * Ignore SHIFT/TAB. Insert 4 spaces on TAB.\n */\n if (e.shiftKey || insertString(this, \" \")) {\n e.preventDefault();\n }\n }\n else if (e.keyCode === ENTER && this.selectionStart !== undefined) {\n /*\n * Handle autoindent only on non-IE.\n */\n var before = this.value.substring(0, this.selectionStart);\n var eol = before.lastIndexOf(\"\\n\");\n var line = before.substring(eol + 1); // Take from eol to end.\n var indent = \"\";\n for (var i = 0; i < line.length && line.charAt(i) === ' '; i++) {\n indent = indent + \" \";\n }\n if (insertString(this, \"\\n\" + indent)) {\n e.preventDefault();\n }\n /*\n * Once the user has started typing, TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n else if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n /*\n * CTRL + M toggles TAB capturing mode.\n * This is the short-cut recommended by\n * https:www.w3.org/TR/wai-aria-practices/#richtext.\n */\n $(this).data('capturingTab', !$(this).data('capturingTab'));\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n /*\n * ESC always stops capturing TAB.\n */\n $(this).data('capturingTab', false);\n }\n else if (!(e.ctrlKey || e.altKey)) {\n /*\n * Once the user has started typing (not modifier keys) TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n }\n });\n }\n\n /**\n * Insert into the given textarea ta the given string sToInsert.\n * @param {html_element} ta The textarea to be updated.\n * @param {string} sToInsert The string to be inserted at the current selection\n * point.\n */\n function insertString(ta, sToInsert) {\n if (ta.selectionStart !== undefined) { // Firefox etc.\n var before = ta.value.substring(0, ta.selectionStart);\n var selSave = ta.selectionEnd;\n var after = ta.value.substring(ta.selectionEnd, ta.value.length);\n\n /**\n * Update the text field.\n */\n var tmp = ta.scrollTop; // Inhibit annoying auto-scroll.\n ta.value = before + sToInsert + after;\n var pos = selSave + sToInsert.length;\n ta.selectionStart = pos;\n ta.selectionEnd = pos;\n ta.scrollTop = tmp;\n return true;\n\n }\n else if (document.selection && document.selection.createRange) { // IE.\n /*\n * TODO: check if this still works. OLD CODE.\n */\n var r = document.selection.createRange();\n var dr = r.duplicate();\n dr.moveToElementText(ta);\n dr.setEndPoint(\"EndToEnd\", r);\n r.text = sToInsert;\n return true;\n }\n /*\n * Other browsers we can't handle.\n */\n else {\n return false;\n }\n }\n\n return {\n setupAllTAs: setupAllTAs,\n initQuestionTA: initQuestionTA,\n setupShowHideAnswer: setupShowHideAnswer,\n };\n});\n"],"names":["define","$","initTextArea","this","data","on","e","window","hasOwnProperty","behattesting","undefined","which","keyCode","shiftKey","insertString","preventDefault","selectionStart","before","value","substring","eol","lastIndexOf","line","indent","i","length","charAt","ctrlKey","altKey","ta","sToInsert","selSave","selectionEnd","after","tmp","scrollTop","pos","document","selection","createRange","r","dr","duplicate","moveToElementText","setEndPoint","text","setupAllTAs","each","initQuestionTA","taId","getElementById","setupShowHideAnswer","linkId","divId","link","div","hdr","children","hdrText","innerText","click","is","hide","replace","show","trigger"],"mappings":";;;;;;;;AA2BAA,oCAAO,CAAC,WAAW,SAASC,YAsDfC,eAMLD,EAAEE,MAAMC,KAAK,mBAAmB,GAChCH,EAAEE,MAAMC,KAAK,gBAAgB,GAE7BH,EAAEE,MAAME,GAAG,aAAa,WAMpBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,WAIlBJ,EAAEE,MAAMC,KAAK,eAAgBH,EAAEE,MAAMC,KAAK,uBAG9CH,EAAEE,MAAME,GAAG,SAAS,WAChBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,SAASC,QAIvBC,OAAOC,eAAe,iBAAmBD,OAAOE,mBAEpCC,IAAZJ,EAAEK,OAAmC,IAAZL,EAAEK,UAlCzB,GAmCEL,EAAEM,SAAkBX,EAAEE,MAAMC,KAAK,iBAI7BE,EAAEO,UAAYC,aAAaX,KAAM,UACjCG,EAAES,sBAGL,GA1CD,KA0CKT,EAAEM,cAA6CF,IAAxBP,KAAKa,eAA8B,SAI3DC,OAASd,KAAKe,MAAMC,UAAU,EAAGhB,KAAKa,gBACtCI,IAAMH,OAAOI,YAAY,MACzBC,KAAOL,OAAOE,UAAUC,IAAM,GAC9BG,OAAS,GACJC,EAAI,EAAGA,EAAIF,KAAKG,QAA6B,MAAnBH,KAAKI,OAAOF,GAAYA,IACvDD,QAAkB,IAElBT,aAAaX,KAAM,KAAOoB,SAC1BjB,EAAES,iBAKNd,EAAEE,MAAMC,KAAK,gBAAgB,QAzD7B,KA2DKE,EAAEM,SAAqBN,EAAEqB,UAAYrB,EAAEsB,QAM5C3B,EAAEE,MAAMC,KAAK,gBAAiBH,EAAEE,MAAMC,KAAK,iBAC3CE,EAAES,kBAnEJ,KAqEOT,EAAEM,QAIPX,EAAEE,MAAMC,KAAK,gBAAgB,GAEtBE,EAAEqB,SAAWrB,EAAEsB,QAItB3B,EAAEE,MAAMC,KAAK,gBAAgB,eAYpCU,aAAae,GAAIC,mBACIpB,IAAtBmB,GAAGb,eAA8B,KAC7BC,OAASY,GAAGX,MAAMC,UAAU,EAAGU,GAAGb,gBAClCe,QAAUF,GAAGG,aACbC,MAAQJ,GAAGX,MAAMC,UAAUU,GAAGG,aAAcH,GAAGX,MAAMO,QAKrDS,IAAML,GAAGM,UACbN,GAAGX,MAAQD,OAASa,UAAYG,UAC5BG,IAAML,QAAUD,UAAUL,cAC9BI,GAAGb,eAAiBoB,IACpBP,GAAGG,aAAeI,IAClBP,GAAGM,UAAYD,KACR,EAGN,GAAIG,SAASC,WAAaD,SAASC,UAAUC,YAAa,KAIvDC,EAAIH,SAASC,UAAUC,cACvBE,GAAKD,EAAEE,mBACXD,GAAGE,kBAAkBd,IACrBY,GAAGG,YAAY,WAAYJ,GAC3BA,EAAEK,KAAOf,WACF,SAMA,QAIR,CACHgB,uBAjLA7C,EAAE,sBAAsB8C,KAAK7C,eAkL7B8C,wBA1KoBC,MACpBhD,EAAEoC,SAASa,eAAeD,OAAOF,KAAK7C,eA0KtCiD,6BAhKyBC,OAAQC,WAC7BC,KAAOrD,EAAEoC,SAASa,eAAeE,SACjCG,IAAMtD,EAAEoC,SAASa,eAAeG,QAChCG,IAAMF,KAAKG,WAAW,GACtBC,QAAUF,IAAIG,UAClBL,KAAKM,OAAM,KACHL,IAAIM,GAAG,aACPN,IAAIO,OACJN,IAAIG,UAAYD,QAAQK,QAAQ,IAAU,OAE1CR,IAAIS,OACJT,IAAIU,QAAQ,aACZT,IAAIG,UAAYD,QAAQK,QAAQ,IAAU"}
\ No newline at end of file
+{"version":3,"file":"textareas.min.js","sources":["../src/textareas.js"],"sourcesContent":["/**\n * 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/**\n * JavaScript for handling textareas and form actions in CodeRunner question\n * editing forms and student question answering forms.\n *\n * @module qtype_coderunner/textareas\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\ndefine(['jquery'], function($) {\n\n /**\n * Function to initialise all code-input text-areas in a page.\n * Used by the form editor but can't be used for question text areas as\n * renderer.php is called once for each question in a quiz, and there is\n * no communication between the questions.\n */\n function setupAllTAs() {\n $('textarea.edit_code').each(initTextArea);\n }\n\n /**\n * Initialise a particular text area (TA), given its ID (which may contain\n * colons, so can't use jQuery selector).\n * @param {string} taId The ID of the textarea html element.\n */\n function initQuestionTA(taId) {\n $(document.getElementById(taId)).each(initTextArea);\n }\n\n\n /**\n * Set up to expand or collapse the question author's answer when the user clicks\n * on the show/hide answer button.\n * @param {string} linkId The ID of the link that the user clicks.\n * @param {sting} divId The ID of the div to be shown or hidden.\n */\n function setupShowHideAnswer(linkId, divId) {\n let link = $(document.getElementById(linkId)),\n div = $(document.getElementById(divId)),\n hdr = link.children()[0], // The
element.\n hdrText = hdr.innerText; // Its body.\n link.click(() => {\n if (div.is(\":visible\")) {\n div.hide();\n hdr.innerText = hdrText.replace(\"\\u25BE\", \"\\u25B8\");\n } else {\n div.show();\n div.trigger('mousemove'); // So user sees contents.\n hdr.innerText = hdrText.replace(\"\\u25B8\", \"\\u25BE\");\n }\n });\n }\n\n /**\n * Set up the JavaScript to handle a text area 'this'.\n * It just does rudimentary autoindent on return and replaces tabs with\n * 4 spaces always.\n * For info on key handling browser inconsistencies see\n * http://unixpapa.com/js/key.html\n * If the AceEditor is handling a text area, this code is unused as\n * the actual textarea is hidden by Ace.\n */\n function initTextArea() {\n var TAB = 9,\n ENTER = 13,\n ESC = 27,\n KEY_M = 77;\n\n $(this).data('clickInProgress', false);\n $(this).data('capturingTab', true);\n\n $(this).on('mousedown', function() {\n /*\n * Event order seems to be (\\ is where the mouse button is pressed, / released):\n * Chrome: \\ mousedown, mouseup, focusin / click.\n * Firefox/IE: \\ mousedown, focusin / mouseup, click.\n */\n $(this).data('clickInProgress', true);\n });\n\n $(this).on('focusin', function() {\n /*\n * At first, pressing TAB moves focus.\n */\n $(this).data('capturingTab', $(this).data('clickInProgress'));\n });\n\n $(this).on('click', function() {\n $(this).data('clickInProgress', false);\n });\n\n $(this).on('keydown', function(e) {\n /*\n * Don't autoindent when behat testing in progress.\n */\n if (window.hasOwnProperty('behattesting') && window.behattesting) { return; }\n\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode == TAB && $(this).data('capturingTab')) {\n /*\n * Ignore SHIFT/TAB. Insert 4 spaces on TAB.\n */\n if (e.shiftKey || insertString(this, \" \")) {\n e.preventDefault();\n }\n }\n else if (e.keyCode === ENTER && this.selectionStart !== undefined) {\n /*\n * Handle autoindent only on non-IE.\n */\n var before = this.value.substring(0, this.selectionStart);\n var eol = before.lastIndexOf(\"\\n\");\n var line = before.substring(eol + 1); // Take from eol to end.\n var indent = \"\";\n for (var i = 0; i < line.length && line.charAt(i) === ' '; i++) {\n indent = indent + \" \";\n }\n if (insertString(this, \"\\n\" + indent)) {\n e.preventDefault();\n }\n /*\n * Once the user has started typing, TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n else if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n /*\n * CTRL + M toggles TAB capturing mode.\n * This is the short-cut recommended by\n * https:www.w3.org/TR/wai-aria-practices/#richtext.\n */\n $(this).data('capturingTab', !$(this).data('capturingTab'));\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n /*\n * ESC always stops capturing TAB.\n */\n $(this).data('capturingTab', false);\n }\n else if (!(e.ctrlKey || e.altKey)) {\n /*\n * Once the user has started typing (not modifier keys) TAB indents.\n */\n $(this).data('capturingTab', true);\n }\n }\n });\n }\n\n /**\n * Insert into the given textarea ta the given string sToInsert.\n * @param {html_element} ta The textarea to be updated.\n * @param {string} sToInsert The string to be inserted at the current selection\n * point.\n */\n function insertString(ta, sToInsert) {\n if (ta.selectionStart !== undefined) { // Firefox etc.\n var before = ta.value.substring(0, ta.selectionStart);\n var selSave = ta.selectionEnd;\n var after = ta.value.substring(ta.selectionEnd, ta.value.length);\n\n /**\n * Update the text field.\n */\n var tmp = ta.scrollTop; // Inhibit annoying auto-scroll.\n ta.value = before + sToInsert + after;\n var pos = selSave + sToInsert.length;\n ta.selectionStart = pos;\n ta.selectionEnd = pos;\n ta.scrollTop = tmp;\n return true;\n\n }\n else if (document.selection && document.selection.createRange) { // IE.\n /*\n * TODO: check if this still works. OLD CODE.\n */\n var r = document.selection.createRange();\n var dr = r.duplicate();\n dr.moveToElementText(ta);\n dr.setEndPoint(\"EndToEnd\", r);\n r.text = sToInsert;\n return true;\n }\n /*\n * Other browsers we can't handle.\n */\n else {\n return false;\n }\n }\n\n return {\n setupAllTAs: setupAllTAs,\n initQuestionTA: initQuestionTA,\n setupShowHideAnswer: setupShowHideAnswer,\n };\n});\n"],"names":["define","$","initTextArea","this","data","on","e","window","hasOwnProperty","behattesting","undefined","which","keyCode","shiftKey","insertString","preventDefault","selectionStart","before","value","substring","eol","lastIndexOf","line","indent","i","length","charAt","ctrlKey","altKey","ta","sToInsert","selSave","selectionEnd","after","tmp","scrollTop","pos","document","selection","createRange","r","dr","duplicate","moveToElementText","setEndPoint","text","setupAllTAs","each","initQuestionTA","taId","getElementById","setupShowHideAnswer","linkId","divId","link","div","hdr","children","hdrText","innerText","click","is","hide","replace","show","trigger"],"mappings":";;;;;;;;AA2BAA,oCAAO,CAAC,WAAW,SAASC,YAsDfC,eAMLD,EAAEE,MAAMC,KAAK,mBAAmB,GAChCH,EAAEE,MAAMC,KAAK,gBAAgB,GAE7BH,EAAEE,MAAME,GAAG,aAAa,WAMpBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,WAIlBJ,EAAEE,MAAMC,KAAK,eAAgBH,EAAEE,MAAMC,KAAK,uBAG9CH,EAAEE,MAAME,GAAG,SAAS,WAChBJ,EAAEE,MAAMC,KAAK,mBAAmB,MAGpCH,EAAEE,MAAME,GAAG,WAAW,SAASC,QAIvBC,OAAOC,eAAe,iBAAmBD,OAAOE,mBAEpCC,IAAZJ,EAAEK,OAAmC,IAAZL,EAAEK,UAlCzB,GAmCEL,EAAEM,SAAkBX,EAAEE,MAAMC,KAAK,iBAI7BE,EAAEO,UAAYC,aAAaX,KAAM,UACjCG,EAAES,sBAGL,GA1CD,KA0CKT,EAAEM,cAA6CF,IAAxBP,KAAKa,eAA8B,SAI3DC,OAASd,KAAKe,MAAMC,UAAU,EAAGhB,KAAKa,gBACtCI,IAAMH,OAAOI,YAAY,MACzBC,KAAOL,OAAOE,UAAUC,IAAM,GAC9BG,OAAS,GACJC,EAAI,EAAGA,EAAIF,KAAKG,QAA6B,MAAnBH,KAAKI,OAAOF,GAAYA,IACvDD,QAAkB,IAElBT,aAAaX,KAAM,KAAOoB,SAC1BjB,EAAES,iBAKNd,EAAEE,MAAMC,KAAK,gBAAgB,QAzD7B,KA2DKE,EAAEM,SAAqBN,EAAEqB,UAAYrB,EAAEsB,QAM5C3B,EAAEE,MAAMC,KAAK,gBAAiBH,EAAEE,MAAMC,KAAK,iBAC3CE,EAAES,kBAnEJ,KAqEOT,EAAEM,QAIPX,EAAEE,MAAMC,KAAK,gBAAgB,GAEtBE,EAAEqB,SAAWrB,EAAEsB,QAItB3B,EAAEE,MAAMC,KAAK,gBAAgB,eAYpCU,aAAae,GAAIC,mBACIpB,IAAtBmB,GAAGb,eAA8B,KAC7BC,OAASY,GAAGX,MAAMC,UAAU,EAAGU,GAAGb,gBAClCe,QAAUF,GAAGG,aACbC,MAAQJ,GAAGX,MAAMC,UAAUU,GAAGG,aAAcH,GAAGX,MAAMO,QAKrDS,IAAML,GAAGM,UACbN,GAAGX,MAAQD,OAASa,UAAYG,UAC5BG,IAAML,QAAUD,UAAUL,cAC9BI,GAAGb,eAAiBoB,IACpBP,GAAGG,aAAeI,IAClBP,GAAGM,UAAYD,KACR,EAGN,GAAIG,SAASC,WAAaD,SAASC,UAAUC,YAAa,KAIvDC,EAAIH,SAASC,UAAUC,cACvBE,GAAKD,EAAEE,mBACXD,GAAGE,kBAAkBd,IACrBY,GAAGG,YAAY,WAAYJ,GAC3BA,EAAEK,KAAOf,WACF,SAMA,QAIR,CACHgB,uBAjLA7C,EAAE,sBAAsB8C,KAAK7C,eAkL7B8C,wBA1KoBC,MACpBhD,EAAEoC,SAASa,eAAeD,OAAOF,KAAK7C,eA0KtCiD,6BAhKyBC,OAAQC,WAC7BC,KAAOrD,EAAEoC,SAASa,eAAeE,SACjCG,IAAMtD,EAAEoC,SAASa,eAAeG,QAChCG,IAAMF,KAAKG,WAAW,GACtBC,QAAUF,IAAIG,UAClBL,KAAKM,OAAM,WACHL,IAAIM,GAAG,aACPN,IAAIO,OACJN,IAAIG,UAAYD,QAAQK,QAAQ,IAAU,OAE1CR,IAAIS,OACJT,IAAIU,QAAQ,aACZT,IAAIG,UAAYD,QAAQK,QAAQ,IAAU"}
\ No newline at end of file
diff --git a/amd/build/ui_ace.min.js b/amd/build/ui_ace.min.js
index db72c4da7..b2ca7b514 100644
--- a/amd/build/ui_ace.min.js
+++ b/amd/build/ui_ace.min.js
@@ -14,6 +14,6 @@
* @copyright Richard Lobb, 2015, 2017, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_ace",["jquery"],(function($){function AceWrapper(textareaId,w,h,params){var session,code,textarea=$(document.getElementById(textareaId)),wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=textarea[0]===document.activeElement,lang=params.lang,t=this;try{window.ace.require("ace/ext/language_tools"),this.modelist=window.ace.require("ace/ext/modelist"),this.textareaId=textareaId,this.textarea=textarea,this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),textarea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({enableBasicAutocompletion:!0,enableLiveAutocompletion:params.live_autocompletion,fontSize:params.font_size?params.font_size:"14px",newLineMode:"unix"}),this.editor.$blockScrolling=1/0,session=this.editor.getSession(),code=this.textarea.val(),(void 0===params.import_from_scratchpad||params.import_from_scratchpad)&&(code=this.extract_from_json_maybe(code)),session.setValue(code);const userTheme=window.localStorage.getItem("qtype_coderunner.ace.theme"),consider_prefers=params.auto_switch_light_dark&&window.matchMedia;null!==userTheme?this.editor.setTheme(userTheme):consider_prefers&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):consider_prefers&&window.matchMedia("(prefers-color-scheme: light)").matches?this.editor.setTheme("ace/theme/textmate"):params.theme?this.editor.setTheme("ace/theme/"+params.theme):this.editor.setTheme("ace/theme/textmate"),this.fixSlowLoad(),this.setLanguage(lang),this.setEventHandlers(textarea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.fail=!1}catch(err){this.fail=!0}}return AceWrapper.prototype.extract_from_json_maybe=function(code){try{code=JSON.parse(code).answer_code[0]}catch(err){}return code},AceWrapper.prototype.failed=function(){return this.fail},AceWrapper.prototype.failMessage=function(){return"ace_ui_notready"},AceWrapper.prototype.sync=function(){const thisThemeNow=this.editor.getOption("theme"),thisEditorKey="qtype_coderunner.".concat(this.textareaId,".theme"),lastTheme=window.sessionStorage.getItem(thisEditorKey),globalTheme=window.localStorage.getItem("qtype_coderunner.ace.theme");null!==lastTheme&&lastTheme!=thisThemeNow?window.localStorage.setItem("qtype_coderunner.ace.theme",thisThemeNow):globalTheme&&globalTheme!==thisThemeNow&&(this.editor.setOption("theme",globalTheme),thisThemeNow=globalTheme),window.sessionStorage.setItem(thisEditorKey,thisThemeNow)},AceWrapper.prototype.syncIntervalSecs=function(){return 2},AceWrapper.prototype.setLanguage=function(language){var session=this.editor.getSession(),mode=this.findMode(language);mode&&session.setMode(mode.mode)},AceWrapper.prototype.getElement=function(){return this.editNode},AceWrapper.prototype.captureTab=function(){this.capturingTab=!0,this.editor.commands.bindKeys({Tab:"indent","Shift-Tab":"outdent"})},AceWrapper.prototype.releaseTab=function(){this.capturingTab=!1,this.editor.commands.bindKeys({Tab:null,"Shift-Tab":null})},AceWrapper.prototype.fixSlowLoad=function(){const observer=new IntersectionObserver((()=>{$(document).trigger("mousemove")})),editNode=this.editNode.get(0);observer.observe(editNode)},AceWrapper.prototype.setEventHandlers=function(){var t=this;this.editor.getSession().on("change",(function(){t.textarea.val(t.editor.getSession().getValue()),t.contents_changed=!0})),this.editor.on("blur",(function(){t.contents_changed&&t.textarea.trigger("change")})),this.editor.on("mousedown",(function(){t.clickInProgress=!0})),this.editor.on("focus",(function(){t.clickInProgress?t.captureTab():t.releaseTab()})),this.editor.on("click",(function(){t.clickInProgress=!1})),this.editor.container.addEventListener("keydown",(function(e){void 0!==e.which&&0===e.which||(77===e.keyCode&&e.ctrlKey&&!e.altKey?(t.capturingTab?t.releaseTab():t.captureTab(),e.preventDefault()):27===e.keyCode?t.releaseTab():e.shiftKey||e.ctrlKey||e.altKey||9==e.keyCode||t.captureTab())}),!0)},AceWrapper.prototype.destroy=function(){var focused;this.fail||(focused=this.editor.isFocused(),this.textarea.val(this.editor.getSession().getValue()),this.editor.destroy(),$(this.editNode).remove(),focused&&(this.textarea.focus(),this.textarea[0].selectionStart=this.textarea[0].value.length))},AceWrapper.prototype.hasFocus=function(){return this.editor.isFocused()},AceWrapper.prototype.findMode=function(language){var candidate,filename,result,candidates,nameMap={octave:"matlab",nodejs:"javascript","c#":"cs"};if("string"==typeof language){language.toLowerCase()in nameMap&&(language=nameMap[language.toLowerCase()]),candidates=[language,language.replace(/\d+$/,"")];for(var i=0;i
"),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),textarea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({enableBasicAutocompletion:!0,enableLiveAutocompletion:params.live_autocompletion,fontSize:params.font_size?params.font_size:"14px",newLineMode:"unix"}),this.editor.$blockScrolling=1/0,session=this.editor.getSession(),code=this.textarea.val(),(void 0===params.import_from_scratchpad||params.import_from_scratchpad)&&(code=this.extract_from_json_maybe(code)),session.setValue(code);var userTheme=window.localStorage.getItem("qtype_coderunner.ace.theme"),consider_prefers=params.auto_switch_light_dark&&window.matchMedia;null!==userTheme?this.editor.setTheme(userTheme):consider_prefers&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):consider_prefers&&window.matchMedia("(prefers-color-scheme: light)").matches?this.editor.setTheme("ace/theme/textmate"):params.theme?this.editor.setTheme("ace/theme/"+params.theme):this.editor.setTheme("ace/theme/textmate"),this.fixSlowLoad(),this.setLanguage(lang),this.setEventHandlers(textarea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.fail=!1}catch(err){this.fail=!0}}return AceWrapper.prototype.extract_from_json_maybe=function(code){try{code=JSON.parse(code).answer_code[0]}catch(err){}return code},AceWrapper.prototype.failed=function(){return this.fail},AceWrapper.prototype.failMessage=function(){return"ace_ui_notready"},AceWrapper.prototype.sync=function(){var thisThemeNow=this.editor.getOption("theme"),thisEditorKey="qtype_coderunner.".concat(this.textareaId,".theme"),lastTheme=window.sessionStorage.getItem(thisEditorKey),globalTheme=window.localStorage.getItem("qtype_coderunner.ace.theme");null!==lastTheme&&lastTheme!=thisThemeNow?window.localStorage.setItem("qtype_coderunner.ace.theme",thisThemeNow):globalTheme&&globalTheme!==thisThemeNow&&(this.editor.setOption("theme",globalTheme),function(name){throw new TypeError('"'+name+'" is read-only')}("thisThemeNow")),window.sessionStorage.setItem(thisEditorKey,thisThemeNow)},AceWrapper.prototype.syncIntervalSecs=function(){return 2},AceWrapper.prototype.setLanguage=function(language){var session=this.editor.getSession(),mode=this.findMode(language);mode&&session.setMode(mode.mode)},AceWrapper.prototype.getElement=function(){return this.editNode},AceWrapper.prototype.captureTab=function(){this.capturingTab=!0,this.editor.commands.bindKeys({Tab:"indent","Shift-Tab":"outdent"})},AceWrapper.prototype.releaseTab=function(){this.capturingTab=!1,this.editor.commands.bindKeys({Tab:null,"Shift-Tab":null})},AceWrapper.prototype.fixSlowLoad=function(){var observer=new IntersectionObserver((function(){$(document).trigger("mousemove")})),editNode=this.editNode.get(0);observer.observe(editNode)},AceWrapper.prototype.setEventHandlers=function(){var t=this;this.editor.getSession().on("change",(function(){t.textarea.val(t.editor.getSession().getValue()),t.contents_changed=!0})),this.editor.on("blur",(function(){t.contents_changed&&t.textarea.trigger("change")})),this.editor.on("mousedown",(function(){t.clickInProgress=!0})),this.editor.on("focus",(function(){t.clickInProgress?t.captureTab():t.releaseTab()})),this.editor.on("click",(function(){t.clickInProgress=!1})),this.editor.container.addEventListener("keydown",(function(e){void 0!==e.which&&0===e.which||(77===e.keyCode&&e.ctrlKey&&!e.altKey?(t.capturingTab?t.releaseTab():t.captureTab(),e.preventDefault()):27===e.keyCode?t.releaseTab():e.shiftKey||e.ctrlKey||e.altKey||9==e.keyCode||t.captureTab())}),!0)},AceWrapper.prototype.destroy=function(){var focused;this.fail||(focused=this.editor.isFocused(),this.textarea.val(this.editor.getSession().getValue()),this.editor.destroy(),$(this.editNode).remove(),focused&&(this.textarea.focus(),this.textarea[0].selectionStart=this.textarea[0].value.length))},AceWrapper.prototype.hasFocus=function(){return this.editor.isFocused()},AceWrapper.prototype.findMode=function(language){var candidate,filename,result,candidates,nameMap={octave:"matlab",nodejs:"javascript","c#":"cs"};if("string"==typeof language){language.toLowerCase()in nameMap&&(language=nameMap[language.toLowerCase()]),candidates=[language,language.replace(/\d+$/,"")];for(var i=0;i.\n\n/**\n * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n code,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n this.textareaId = textareaId;\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n enableLiveAutocompletion: params.live_autocompletion,\n fontSize: params.font_size ? params.font_size : \"14px\",\n newLineMode: \"unix\",\n });\n\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n code = this.textarea.val();\n if (params.import_from_scratchpad === undefined || params.import_from_scratchpad) {\n code = this.extract_from_json_maybe(code);\n }\n session.setValue(code);\n\n // If there's a user-defined theme in local storage, use that.\n // Otherwise use the 'prefers-color-scheme' option if given or\n // the question/system defaults if not.\n const userTheme = window.localStorage.getItem('qtype_coderunner.ace.theme');\n const consider_prefers = params.auto_switch_light_dark && window.matchMedia;\n if (userTheme !== null) {\n this.editor.setTheme(userTheme);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: light)').matches) {\n this.editor.setTheme(ACE_LIGHT_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.fixSlowLoad();\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n AceWrapper.prototype.extract_from_json_maybe = function(code) {\n // If the given code looks like JSON from the Scratchpad UI,\n // extract and return the answer_code attribute.\n try {\n const jsonObj = JSON.parse(code);\n code = jsonObj.answer_code[0];\n } catch(err) {}\n\n return code;\n };\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // The data is always sync'd to the text area. But here we use sync to\n // poll the value of the current theme and record in browser local\n // storage if the value for this particular Ace instance has changed\n // (as recorded in session storage). The theme to use for all Ace windows\n // from now on in this browser is recorded in browser local storage.\n const thisThemeNow = this.editor.getOption('theme');\n const thisEditorKey = `qtype_coderunner.${this.textareaId}.theme`;\n const lastTheme = window.sessionStorage.getItem(thisEditorKey);\n const globalKey = 'qtype_coderunner.ace.theme';\n const globalTheme = window.localStorage.getItem(globalKey);\n\n if (lastTheme !== null && lastTheme != thisThemeNow) {\n // The theme in this Ace window has been changed by the user.\n window.localStorage.setItem(globalKey, thisThemeNow);\n } else if (globalTheme && globalTheme !== thisThemeNow) {\n // A theme has been defined by another Ace window since we loaded.\n // Switch to that theme.\n this.editor.setOption('theme', globalTheme);\n thisThemeNow = globalTheme;\n }\n window.sessionStorage.setItem(thisEditorKey, thisThemeNow);\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 2;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n // Sometimes Ace editors do not load until the mouse is moved. To fix this,\n // 'move' the mouse using JQuery when the editor div enters the viewport.\n AceWrapper.prototype.fixSlowLoad = function () {\n const observer = new IntersectionObserver( () => {\n $(document).trigger('mousemove');\n });\n const editNode = this.editNode.get(0); // Non-JQuerry node.\n observer.observe(editNode);\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","session","code","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","enableLiveAutocompletion","live_autocompletion","fontSize","font_size","newLineMode","$blockScrolling","Infinity","getSession","val","undefined","import_from_scratchpad","extract_from_json_maybe","setValue","userTheme","localStorage","getItem","consider_prefers","auto_switch_light_dark","matchMedia","setTheme","matches","theme","fixSlowLoad","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","JSON","parse","answer_code","failed","failMessage","sync","thisThemeNow","getOption","thisEditorKey","lastTheme","sessionStorage","globalTheme","setItem","setOption","syncIntervalSecs","language","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","observer","IntersectionObserver","trigger","observe","getValue","container","addEventListener","e","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_ace.min.js","sources":["../src/ui_ace.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 * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n code,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n this.textareaId = textareaId;\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n enableLiveAutocompletion: params.live_autocompletion,\n fontSize: params.font_size ? params.font_size : \"14px\",\n newLineMode: \"unix\",\n });\n\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n code = this.textarea.val();\n if (params.import_from_scratchpad === undefined || params.import_from_scratchpad) {\n code = this.extract_from_json_maybe(code);\n }\n session.setValue(code);\n\n // If there's a user-defined theme in local storage, use that.\n // Otherwise use the 'prefers-color-scheme' option if given or\n // the question/system defaults if not.\n const userTheme = window.localStorage.getItem('qtype_coderunner.ace.theme');\n const consider_prefers = params.auto_switch_light_dark && window.matchMedia;\n if (userTheme !== null) {\n this.editor.setTheme(userTheme);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: light)').matches) {\n this.editor.setTheme(ACE_LIGHT_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.fixSlowLoad();\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n AceWrapper.prototype.extract_from_json_maybe = function(code) {\n // If the given code looks like JSON from the Scratchpad UI,\n // extract and return the answer_code attribute.\n try {\n const jsonObj = JSON.parse(code);\n code = jsonObj.answer_code[0];\n } catch(err) {}\n\n return code;\n };\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // The data is always sync'd to the text area. But here we use sync to\n // poll the value of the current theme and record in browser local\n // storage if the value for this particular Ace instance has changed\n // (as recorded in session storage). The theme to use for all Ace windows\n // from now on in this browser is recorded in browser local storage.\n const thisThemeNow = this.editor.getOption('theme');\n const thisEditorKey = `qtype_coderunner.${this.textareaId}.theme`;\n const lastTheme = window.sessionStorage.getItem(thisEditorKey);\n const globalKey = 'qtype_coderunner.ace.theme';\n const globalTheme = window.localStorage.getItem(globalKey);\n\n if (lastTheme !== null && lastTheme != thisThemeNow) {\n // The theme in this Ace window has been changed by the user.\n window.localStorage.setItem(globalKey, thisThemeNow);\n } else if (globalTheme && globalTheme !== thisThemeNow) {\n // A theme has been defined by another Ace window since we loaded.\n // Switch to that theme.\n this.editor.setOption('theme', globalTheme);\n thisThemeNow = globalTheme;\n }\n window.sessionStorage.setItem(thisEditorKey, thisThemeNow);\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 2;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n // Sometimes Ace editors do not load until the mouse is moved. To fix this,\n // 'move' the mouse using JQuery when the editor div enters the viewport.\n AceWrapper.prototype.fixSlowLoad = function () {\n const observer = new IntersectionObserver( () => {\n $(document).trigger('mousemove');\n });\n const editNode = this.editNode.get(0); // Non-JQuerry node.\n observer.observe(editNode);\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","session","code","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","enableLiveAutocompletion","live_autocompletion","fontSize","font_size","newLineMode","$blockScrolling","Infinity","getSession","val","undefined","import_from_scratchpad","extract_from_json_maybe","setValue","userTheme","localStorage","getItem","consider_prefers","auto_switch_light_dark","matchMedia","setTheme","matches","theme","fixSlowLoad","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","JSON","parse","answer_code","failed","failMessage","sync","thisThemeNow","getOption","thisEditorKey","lastTheme","sessionStorage","globalTheme","setItem","setOption","syncIntervalSecs","language","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","observer","IntersectionObserver","trigger","observe","getValue","container","addEventListener","e","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_ace_gapfiller.min.js b/amd/build/ui_ace_gapfiller.min.js
index 3cf7ed422..4bf3d7434 100644
--- a/amd/build/ui_ace_gapfiller.min.js
+++ b/amd/build/ui_ace_gapfiller.min.js
@@ -38,6 +38,6 @@
* @copyright Matthew Toohey, 2021, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_ace_gapfiller",["jquery"],(function($){var Range;const validChars=/[ !"#$%&'()*+,`\-./0-9:;<=>?@A-Z\[\]\\^_a-z{}|~]/;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this;let code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){let cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){let nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,selectionRange.start,char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){let maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+1=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(let i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(let i=start;i?@A-Z\[\]\\^_a-z{}|~]/;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this,code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){var cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){var nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){var char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){var _char=e.args;validChars.test(_char)&&gap.insertChar(t.gaps,selectionRange.start,_char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){var maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+1=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(var i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(var i=start;i.\n\n/**\n * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Set theme to dark if user-prefers-color-scheme is dark,\n // else use the uiParams theme if provided else use light.\n if (window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","matchMedia","matches","setTheme","theme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_ace_gapfiller.min.js","sources":["../src/ui_ace_gapfiller.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 util.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 * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Use the uiParams theme if provided else use light.\n if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","theme","setTheme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js b/amd/build/ui_scratchpad.min.js
index 7567f02a2..6a3e92a04 100644
--- a/amd/build/ui_scratchpad.min.js
+++ b/amd/build/ui_scratchpad.min.js
@@ -1,57 +1,3 @@
-define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_templates,_userinterfacewrapper,_outputdisplayarea){var obj;
-/**
- * Implementation of the scratchpad_ui user interface plugin. For overall details
- * of the UI plugin architecture, see userinterfacewrapper.js.
- *
- * This plugin replaces the usual textarea answer element with a UI is designed to
- * allow the execution of code in the CodeRunner question in a manner similar to an IDE.
- * It contains two editor boxes, one on top of another, allowing users to enter and
- * edit code in both. It contains two embedded Ace UIs.
- * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,
- * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,
- * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that
- * provides information about how to use the Scratchpad.
- * It's possible to run code 'in-browser' by clicking the Run Button,
- * without making a submission via the Check Button:
- * If Prefix with Answer is not checked, only the code in the Scratchpad is run --
- * allowing for a rough working spot to quickly check the result of code.
- * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is
- * appended to the code in the first editor before being run.
- * The Run Button has some limitations when using its default configuration:
- * Does not support programs that use STDIN (by default);
- * Only supports textual STDOUT (by default).
- * Note: These features can be supported, see the README section on wrappers...
- * The serialisation of this UI is a JSON object with the fields
- * with fields:
- * answer_code: [""] A list containing a string with answer code from the first editor;
- * test_code: [""] A list containing a string with containing answer code from the second editor;
- * show_hide: ["1"] when scratchpad is visible, otherwise [""];
- * prefix_ans: ["1"] when Prefix with Answer is checked, otherwise [""].
- *
- * UI Parameters:
- * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.
- * - button_name: run button text.
- * - prefix_name: prefix with answer check-box label text.
- * - help_text: help text to show.
- * - run_lang: language used to run code when the run button is clicked,
- * this should be the language your wrapper is written in (if applicable).
- * - wrapper_src: location of wrapper code to be used by the run button, if applicable:
- * setting to globalextra will use text in global extra field,
- * - prototypeextra will use the prototype extra field.
- * - output_display_mode: control how program output is displayed on runs, there are three modes:
- * - text: display program output as text, html escaped;
- * - json: display program output, when it is json,
- * - html: display program output as raw html.
- * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...
- * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI
- * from student perspective.
- * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.
- * This can be used to swap the default state.
- *
- * @module qtype_coderunner/ui_scratchpad
- * @copyright Richard Lobb, 2022, The University of Canterbury
- * @copyright James Napier, 2022, The University of Canterbury
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Constructor=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};const invertSerial=current=>"1"===current[0]?[""]:["1"],escapeRegExp=string=>string.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),overwriteValues=(defaults,prescribed)=>{let overwritten={...defaults};if(prescribed)for(const[key,value]of Object.entries(defaults))overwritten[key]=prescribed[key]||value;return overwritten};_exports.Constructor=class{constructor(textAreaId,width,height,uiParams){const DEF_UI_PARAMS={scratchpad_name:"",button_name:"",prefix_name:"",help_text:"",params:{},run_lang:uiParams.lang,output_display_mode:"text",disable_scratchpad:!1,wrapper_src:null,open_delimiter:"{|",close_delimiter:"|}",escape:!1};this.textArea=document.getElementById(textAreaId),this.textAreaId=textAreaId,this.height=height,this.readOnly=this.textArea.readonly,this.fail=!1,this.outerDiv=null,this.outputDisplay=null,this.invertPreload=uiParams.invert_prefix,this.lang=uiParams.lang,this.numRows=this.textArea.rows,this.uiParams=overwriteValues(DEF_UI_PARAMS,uiParams),this.runWrapper=this.getRunWrapper(),this.reload()}getRunWrapper(){const wrapperSrc=this.uiParams.wrapper_src;let runWrapper=null;return wrapperSrc&&("globalextra"===wrapperSrc||"prototypeextra"===wrapperSrc?runWrapper=this.textArea.dataset[wrapperSrc]:(this.fail=!0,this.failString="scratchpad_ui_badrunwrappersrc")),runWrapper}failed(){return this.fail}failMessage(){return this.failString}sync(){if(!this.context)return;const serialisation=this.getSerialisation();this.setSerialisation(serialisation)}getSerialisation(){const prefixAns=document.getElementById(this.context.prefix_ans.id),showHide=document.getElementById(this.context.show_hide.id);let serialisation={answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:[""]};return this.answerTextarea&&(serialisation.answer_code=[this.answerTextarea.value]),this.testTextarea&&(serialisation.test_code=[this.testTextarea.value]),showHide&&!(el=>{if(!el.classList.contains("collapse")&&!el.classList.contains("collapsing"))throw Error("Element does not have collapse class");return!el.classList.contains("show")})(showHide)&&(serialisation.show_hide=["1"]),(null!=prefixAns&&prefixAns.checked||this.context.disable_scratchpad)&&(serialisation.prefix_ans=["1"]),this.invertPreload&&(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans)),serialisation}setSerialisation(serialisation){serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),Object.values(serialisation).some((val=>1===val.length&&val[0].length>0))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}getElement(){return this.outerDiv}handleRunButtonClick(){if(null===this.outputDisplay)return;this.sync();const preloadString=this.textArea.value,serial=this.readJson(preloadString),escape=code=>this.uiParams.escape?JSON.stringify(code).slice(1,-1):code,code=function(answerCode,testCode,prefixAns,template){let open=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"\\(",close=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"\\)";template||(template="".concat(open," ANSWER_CODE ").concat(close,"\n")+"".concat(open," SCRATCHPAD_CODE ").concat(close)),prefixAns||(answerCode="");const escOpen=escapeRegExp(open),escClose=escapeRegExp(close),answerRegex=new RegExp("".concat(escOpen,"\\s*ANSWER_CODE\\s*").concat(escClose),"g"),scratchpadRegex=new RegExp("".concat(escOpen,"\\s*SCRATCHPAD_CODE\\s*").concat(escClose),"g");return(template=template.replaceAll(answerRegex,answerCode)).replaceAll(scratchpadRegex,testCode)}(escape(serial.answer_code[0]),escape(serial.test_code[0]),serial.prefix_ans[0],this.runWrapper,this.uiParams.open_delimiter,this.uiParams.close_delimiter);this.outputDisplay.runCode(code,"",!0)}updateContext(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}readJson(preloadString){let serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch{serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}async reload(){const preloadString=this.textArea.value;let preload;try{preload=this.readJson(preloadString)}catch(error){return this.fail=!0,void(this.failString="scratchpad_ui_invalidserialisation")}this.updateContext(preload);try{const{html:html}=await _templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners()}catch(e){this.fail=!0,this.failString="scratchpad_ui_templateloadfail"}}drawUi(html){const wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}addAceUis(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}addEventListeners(){const runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(()=>this.handleRunButtonClick()))}resize(){}hasFocus(){var _this$answerCodeUi,_this$testCodeUi;let focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}destroy(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}}));
+define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_templates,_userinterfacewrapper,_outputdisplayarea){var obj;function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}function _defineProperties(target,props){for(var i=0;iarr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0}))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}},{key:"getElement",value:function(){return this.outerDiv}},{key:"handleRunButtonClick",value:function(){var _this=this;if(null!==this.outputDisplay){this.sync();var preloadString=this.textArea.value,serial=this.readJson(preloadString),escape=function(code){return _this.uiParams.escape?JSON.stringify(code).slice(1,-1):code},code=function(answerCode,testCode,prefixAns,template){var open=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"\\(",close=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"\\)";template||(template="".concat(open," ANSWER_CODE ").concat(close,"\n")+"".concat(open," SCRATCHPAD_CODE ").concat(close)),prefixAns||(answerCode="");var escOpen=escapeRegExp(open),escClose=escapeRegExp(close),answerRegex=new RegExp("".concat(escOpen,"\\s*ANSWER_CODE\\s*").concat(escClose),"g"),scratchpadRegex=new RegExp("".concat(escOpen,"\\s*SCRATCHPAD_CODE\\s*").concat(escClose),"g");return(template=template.replaceAll(answerRegex,answerCode)).replaceAll(scratchpadRegex,testCode)}(escape(serial.answer_code[0]),escape(serial.test_code[0]),serial.prefix_ans[0],this.runWrapper,this.uiParams.open_delimiter,this.uiParams.close_delimiter);this.outputDisplay.runCode(code,"",!0)}}},{key:"updateContext",value:function(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}},{key:"readJson",value:function(preloadString){var serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch(_unused){serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}},{key:"reload",value:(fn=regeneratorRuntime.mark((function _callee(){var preloadString,preload,_yield$Templates$rend,html;return regeneratorRuntime.wrap((function(_context){for(;;)switch(_context.prev=_context.next){case 0:preloadString=this.textArea.value,_context.prev=1,preload=this.readJson(preloadString),_context.next=10;break;case 5:return _context.prev=5,_context.t0=_context.catch(1),this.fail=!0,this.failString="scratchpad_ui_invalidserialisation",_context.abrupt("return");case 10:return this.updateContext(preload),_context.prev=11,_context.next=14,_templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);case 14:_yield$Templates$rend=_context.sent,html=_yield$Templates$rend.html,this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners(),_context.next=26;break;case 22:_context.prev=22,_context.t1=_context.catch(11),this.fail=!0,this.failString="scratchpad_ui_templateloadfail";case 26:case"end":return _context.stop()}}),_callee,this,[[1,5],[11,22]])})),_reload=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(){return _reload.apply(this,arguments)})},{key:"drawUi",value:function(html){var wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}},{key:"addAceUis",value:function(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}},{key:"addEventListeners",value:function(){var _this2=this,runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(function(){return _this2.handleRunButtonClick()}))}},{key:"resize",value:function(){}},{key:"hasFocus",value:function(){var _this$answerCodeUi,_this$testCodeUi,focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}},{key:"destroy",value:function(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}],protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),ScratchpadUi}();_exports.Constructor=ScratchpadUi}));
//# sourceMappingURL=ui_scratchpad.min.js.map
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js.map b/amd/build/ui_scratchpad.min.js.map
index b2958e3cd..e31f0915c 100644
--- a/amd/build/ui_scratchpad.min.js.map
+++ b/amd/build/ui_scratchpad.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text from first editor.\n * @param {string} testCode text from second editor.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @param {string} open delimiter to look for, e.g. '[['\n * @param {string} close delimiter to look for, e.g. ']]'\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template, open = '\\\\(', close = '\\\\)') => {\n if (!template) {\n template = `${open} ANSWER_CODE ${close}\\n` +\n `${open} SCRATCHPAD_CODE ${close}`;\n }\n if (!prefixAns) {\n answerCode = '';\n }\n const escOpen = escapeRegExp(open);\n const escClose = escapeRegExp(close);\n const answerRegex = new RegExp(`${escOpen}\\\\s*ANSWER_CODE\\\\s*${escClose}`, 'g');\n const scratchpadRegex = new RegExp(`${escOpen}\\\\s*SCRATCHPAD_CODE\\\\s*${escClose}`, 'g');\n template = template.replaceAll(answerRegex, answerCode);\n template = template.replaceAll(scratchpadRegex, testCode);\n return template;\n};\n\n/**\n * Escapes a string for use in regex.\n * @param {string} string to escape.\n * @returns {string} RegEx escaped string\n */\nconst escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"); // $& means the whole matched string\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the default value.\n * Does not add keys/values to the result if that key is not in defaults.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!(el.classList.contains('collapse') || el.classList.contains('collapsing'))) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null,\n open_delimiter: '{|',\n close_delimiter: '|}',\n escape: false\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n let serialisation = {\n answer_code: [''],\n test_code: [''],\n show_hide: [''],\n prefix_ans: ['']\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const escape = (code) => this.uiParams.escape ? JSON.stringify(code).slice(1, -1) : code;\n const answerCode = escape(serial.answer_code[0]);\n const testCode = escape(serial.test_code[0]);\n const code = fillWrapper(\n answerCode,\n testCode,\n serial.prefix_ans[0],\n this.runWrapper,\n this.uiParams.open_delimiter,\n this.uiParams.close_delimiter\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","escapeRegExp","string","replace","overwriteValues","defaults","prescribed","overwritten","key","value","Object","entries","constructor","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","open_delimiter","close_delimiter","escape","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","reload","wrapperSrc","dataset","failString","failed","failMessage","sync","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","test_code","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","checked","values","some","val","length","JSON","stringify","getElement","handleRunButtonClick","preloadString","serial","readJson","code","slice","answerCode","testCode","template","open","close","escOpen","escClose","answerRegex","RegExp","scratchpadRegex","replaceAll","fillWrapper","runCode","updateContext","preload","text","render","CSS","parse","hasOwnProperty","TypeError","error","html","Templates","renderForPromise","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","e","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","hasFocus","focused","_this$answerCodeUi","uiInstance","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text from first editor.\n * @param {string} testCode text from second editor.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @param {string} open delimiter to look for, e.g. '[['\n * @param {string} close delimiter to look for, e.g. ']]'\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template, open = '\\\\(', close = '\\\\)') => {\n if (!template) {\n template = `${open} ANSWER_CODE ${close}\\n` +\n `${open} SCRATCHPAD_CODE ${close}`;\n }\n if (!prefixAns) {\n answerCode = '';\n }\n const escOpen = escapeRegExp(open);\n const escClose = escapeRegExp(close);\n const answerRegex = new RegExp(`${escOpen}\\\\s*ANSWER_CODE\\\\s*${escClose}`, 'g');\n const scratchpadRegex = new RegExp(`${escOpen}\\\\s*SCRATCHPAD_CODE\\\\s*${escClose}`, 'g');\n template = template.replaceAll(answerRegex, answerCode);\n template = template.replaceAll(scratchpadRegex, testCode);\n return template;\n};\n\n/**\n * Escapes a string for use in regex.\n * @param {string} string to escape.\n * @returns {string} RegEx escaped string\n */\nconst escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"); // $& means the whole matched string\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the default value.\n * Does not add keys/values to the result if that key is not in defaults.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!(el.classList.contains('collapse') || el.classList.contains('collapsing'))) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null,\n open_delimiter: '{|',\n close_delimiter: '|}',\n escape: false\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n let serialisation = {\n answer_code: [''],\n test_code: [''],\n show_hide: [''],\n prefix_ans: ['']\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const escape = (code) => this.uiParams.escape ? JSON.stringify(code).slice(1, -1) : code;\n const answerCode = escape(serial.answer_code[0]);\n const testCode = escape(serial.test_code[0]);\n const code = fillWrapper(\n answerCode,\n testCode,\n serial.prefix_ans[0],\n this.runWrapper,\n this.uiParams.open_delimiter,\n this.uiParams.close_delimiter\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","escapeRegExp","string","replace","overwriteValues","defaults","prescribed","overwritten","Object","entries","key","value","ScratchpadUi","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","open_delimiter","close_delimiter","escape","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","reload","wrapperSrc","dataset","failString","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","test_code","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","checked","values","some","val","length","JSON","stringify","sync","preloadString","serial","readJson","code","_this","slice","answerCode","testCode","template","open","close","escOpen","escClose","answerRegex","RegExp","scratchpadRegex","replaceAll","fillWrapper","runCode","preload","text","render","CSS","parse","hasOwnProperty","TypeError","updateContext","Templates","renderForPromise","html","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","_this2","handleRunButtonClick","focused","_this$answerCodeUi","uiInstance","hasFocus","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/userinterfacewrapper.min.js b/amd/build/userinterfacewrapper.min.js
index 0825142de..09ae46ac3 100644
--- a/amd/build/userinterfacewrapper.min.js
+++ b/amd/build/userinterfacewrapper.min.js
@@ -96,6 +96,6 @@
* 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)
*
*****************************************************************************/
-define("qtype_coderunner/userinterfacewrapper",["jquery"],(function($){function InterfaceWrapper(uiname,textareaId){let t=this;this.GUTTER=14,this.DEFAULT_SYNC_INTERVAL_SECS=5;this.taId=textareaId,this.loadFailId=textareaId+"_loadfailerr";const ta=document.getElementById(textareaId);this.textArea=$(ta);const params=this.textArea.attr("data-params");this.uiParams=params?JSON.parse(params):{},this.uiParams.lang=this.textArea.attr("data-lang"),this.readOnly=this.textArea.prop("readonly"),this.isLoading=!1,this.loadFailed=!1,this.retries=0;let h=parseInt(this.textArea.css("height")),content_lines=this.textArea.val().split("\n").length,rows=ta.rows;content_lines>rows&&(rows=Math.min(content_lines,50)),h=Math.max(h,19*rows,50),this.wrapperNode=$(""),this.textArea.after(this.wrapperNode),this.wrapperNode.hide(),this.wrapperNode.css({resize:"vertical",overflow:"hidden",minHeight:h,width:"100%",border:"1px solid darkgrey"}),this.textArea.data("current-ui-wrapper",this),this.uiInstance=null,this.loadUi(uiname,this.uiParams),$(document).mousemove((function(){t.checkForResize()})),$(window).resize((function(){t.checkForResize()})),this.textArea.closest("form").submit((function(){null!==t.uiInstance&&t.uiInstance.sync()})),$(document.body).on("keydown",(function(e){77===e.keyCode&&e.ctrlKey&&e.altKey&&(null!==t.uiInstance||t.loadFailed?t.stop():t.restart())}))}return InterfaceWrapper.prototype.loadUi=function(uiname,params){const t=this;function syncIntervalSecsBase(){return params.hasOwnProperty("sync_interval_secs")?parseInt(params.sync_interval_secs):t.DEFAULT_SYNC_INTERVAL_SECS}if(this.isLoading)return this.retries+=1,void(this.retries>20?(alert("Failed to load "+uiname+" UI component. If this error persists, please report it to the forum on coderunner.org.nz"),this.retries=0,this.loading=0):setTimeout((function(){t.loadUi(uiname,params)}),200));this.retries=0,this.params=params,this.stop(),this.uiname=uiname,""===this.uiname||"none"===this.uiname||sessionStorage.getItem("disableUis")?this.uiInstance=null:(this.isLoading=!0,require(["qtype_coderunner/ui_"+this.uiname],(function(ui){const h=t.wrapperNode.innerHeight()-t.GUTTER,w=t.wrapperNode.innerWidth(),uiInstance=new ui.Constructor(t.taId,w,h,params);if(uiInstance.failed()){t.loadFailed=!0,t.wrapperNode.hide(),uiInstance.destroy(),t.uiInstance=null,t.textArea.addClass("uiloadfailed");const loadFailDiv='';let jqLoadFailDiv=$(loadFailDiv);jqLoadFailDiv.insertBefore(t.textArea),langString=uiInstance.failMessage(),errorDiv=jqLoadFailDiv,require(["core/str"],(function(str){const s=str.get_string(langString,"qtype_coderunner"),fallback=str.get_string("ui_fallback","qtype_coderunner");$.when(s,fallback).done((function(s,fallback){errorDiv.html(s+" "+fallback)}))}))}else{t.hLast=0,t.wLast=0,t.textArea.hide(),t.wrapperNode.show(),t.wrapperNode.append(uiInstance.getElement()),t.uiInstance=uiInstance,t.loadFailed=!1,t.checkForResize();let uiInstancePrototype=Object.getPrototypeOf(uiInstance);uiInstancePrototype.syncIntervalSecs=uiInstancePrototype.syncIntervalSecs||syncIntervalSecsBase,t.startSyncTimer(uiInstance)}var langString,errorDiv;t.isLoading=!1})))},InterfaceWrapper.prototype.startSyncTimer=function(uiInstance){const timeout=uiInstance.syncIntervalSecs();this.uiInstance.timer=timeout?setInterval((function(){uiInstance.sync()}),1e3*timeout):null},InterfaceWrapper.prototype.stopSyncTimer=function(uiInstance){uiInstance.timer&&clearTimeout(uiInstance.timer)},InterfaceWrapper.prototype.stop=function(){null!==this.uiInstance&&(this.stopSyncTimer(this.uiInstance),this.textArea.show(),this.uiInstance.hasFocus()&&(this.textArea.focus(),this.textArea[0].selectionStart=this.textArea[0].value.length),this.uiInstance.destroy(),this.uiInstance=null,this.wrapperNode.hide()),this.loadFailed=!1,this.textArea.removeClass("uiloadfailed"),$(document.getElementById(this.loadFailId)).remove()},InterfaceWrapper.prototype.restart=function(){null===this.uiInstance&&this.loadUi(this.uiname,this.params)},InterfaceWrapper.prototype.checkForResize=function(){if(this.uiInstance){const h=this.wrapperNode.innerHeight(),w=this.wrapperNode.innerWidth();if(h!=this.hLast||w!=this.wLast){const xLeft=this.wrapperNode.offset().left,maxWidth=$(window).innerWidth()-xLeft-25,hAdjusted=h-this.GUTTER,wAdjusted=Math.min(maxWidth,w);this.uiInstance.resize(wAdjusted,hAdjusted),this.hLast=this.wrapperNode.innerHeight(),this.wLast=this.wrapperNode.innerWidth()}}},{newUiWrapper:function(uiname,textareaId){return uiname?new InterfaceWrapper(uiname,textareaId):null},InterfaceWrapper:InterfaceWrapper}}));
+define("qtype_coderunner/userinterfacewrapper",["jquery"],(function($){function InterfaceWrapper(uiname,textareaId){var t=this;this.GUTTER=14,this.DEFAULT_SYNC_INTERVAL_SECS=5;this.taId=textareaId,this.loadFailId=textareaId+"_loadfailerr";var ta=document.getElementById(textareaId);this.textArea=$(ta);var params=this.textArea.attr("data-params");this.uiParams=params?JSON.parse(params):{},this.uiParams.lang=this.textArea.attr("data-lang"),this.readOnly=this.textArea.prop("readonly"),this.isLoading=!1,this.loadFailed=!1,this.retries=0;var h=parseInt(this.textArea.css("height")),content_lines=this.textArea.val().split("\n").length,rows=ta.rows;content_lines>rows&&(rows=Math.min(content_lines,50)),h=Math.max(h,19*rows,50),this.wrapperNode=$(""),this.textArea.after(this.wrapperNode),this.wrapperNode.hide(),this.wrapperNode.css({resize:"vertical",overflow:"hidden",minHeight:h,width:"100%",border:"1px solid darkgrey"}),this.textArea.data("current-ui-wrapper",this),this.uiInstance=null,this.loadUi(uiname,this.uiParams),$(document).mousemove((function(){t.checkForResize()})),$(window).resize((function(){t.checkForResize()})),this.textArea.closest("form").submit((function(){null!==t.uiInstance&&t.uiInstance.sync()})),$(document.body).on("keydown",(function(e){77===e.keyCode&&e.ctrlKey&&e.altKey&&(null!==t.uiInstance||t.loadFailed?t.stop():t.restart())}))}return InterfaceWrapper.prototype.loadUi=function(uiname,params){var t=this;function syncIntervalSecsBase(){return params.hasOwnProperty("sync_interval_secs")?parseInt(params.sync_interval_secs):t.DEFAULT_SYNC_INTERVAL_SECS}if(this.isLoading)return this.retries+=1,void(this.retries>20?(alert("Failed to load "+uiname+" UI component. If this error persists, please report it to the forum on coderunner.org.nz"),this.retries=0,this.loading=0):setTimeout((function(){t.loadUi(uiname,params)}),200));this.retries=0,this.params=params,this.stop(),this.uiname=uiname,""===this.uiname||"none"===this.uiname||sessionStorage.getItem("disableUis")?this.uiInstance=null:(this.isLoading=!0,require(["qtype_coderunner/ui_"+this.uiname],(function(ui){var langString,errorDiv,h=t.wrapperNode.innerHeight()-t.GUTTER,w=t.wrapperNode.innerWidth(),uiInstance=new ui.Constructor(t.taId,w,h,params);if(uiInstance.failed()){t.loadFailed=!0,t.wrapperNode.hide(),uiInstance.destroy(),t.uiInstance=null,t.textArea.addClass("uiloadfailed");var loadFailDiv='',jqLoadFailDiv=$(loadFailDiv);jqLoadFailDiv.insertBefore(t.textArea),langString=uiInstance.failMessage(),errorDiv=jqLoadFailDiv,require(["core/str"],(function(str){var s=str.get_string(langString,"qtype_coderunner"),fallback=str.get_string("ui_fallback","qtype_coderunner");$.when(s,fallback).done((function(s,fallback){errorDiv.html(s+" "+fallback)}))}))}else{t.hLast=0,t.wLast=0,t.textArea.hide(),t.wrapperNode.show(),t.wrapperNode.append(uiInstance.getElement()),t.uiInstance=uiInstance,t.loadFailed=!1,t.checkForResize();var uiInstancePrototype=Object.getPrototypeOf(uiInstance);uiInstancePrototype.syncIntervalSecs=uiInstancePrototype.syncIntervalSecs||syncIntervalSecsBase,t.startSyncTimer(uiInstance)}t.isLoading=!1})))},InterfaceWrapper.prototype.startSyncTimer=function(uiInstance){var timeout=uiInstance.syncIntervalSecs();this.uiInstance.timer=timeout?setInterval((function(){uiInstance.sync()}),1e3*timeout):null},InterfaceWrapper.prototype.stopSyncTimer=function(uiInstance){uiInstance.timer&&clearTimeout(uiInstance.timer)},InterfaceWrapper.prototype.stop=function(){null!==this.uiInstance&&(this.stopSyncTimer(this.uiInstance),this.textArea.show(),this.uiInstance.hasFocus()&&(this.textArea.focus(),this.textArea[0].selectionStart=this.textArea[0].value.length),this.uiInstance.destroy(),this.uiInstance=null,this.wrapperNode.hide()),this.loadFailed=!1,this.textArea.removeClass("uiloadfailed"),$(document.getElementById(this.loadFailId)).remove()},InterfaceWrapper.prototype.restart=function(){null===this.uiInstance&&this.loadUi(this.uiname,this.params)},InterfaceWrapper.prototype.checkForResize=function(){if(this.uiInstance){var h=this.wrapperNode.innerHeight(),w=this.wrapperNode.innerWidth();if(h!=this.hLast||w!=this.wLast){var xLeft=this.wrapperNode.offset().left,maxWidth=$(window).innerWidth()-xLeft-25,hAdjusted=h-this.GUTTER,wAdjusted=Math.min(maxWidth,w);this.uiInstance.resize(wAdjusted,hAdjusted),this.hLast=this.wrapperNode.innerHeight(),this.wLast=this.wrapperNode.innerWidth()}}},{newUiWrapper:function(uiname,textareaId){return uiname?new InterfaceWrapper(uiname,textareaId):null},InterfaceWrapper:InterfaceWrapper}}));
//# sourceMappingURL=userinterfacewrapper.min.js.map
\ No newline at end of file
diff --git a/amd/build/userinterfacewrapper.min.js.map b/amd/build/userinterfacewrapper.min.js.map
index f46f952fc..631c835e8 100644
--- a/amd/build/userinterfacewrapper.min.js.map
+++ b/amd/build/userinterfacewrapper.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"userinterfacewrapper.min.js","sources":["../src/userinterfacewrapper.js"],"sourcesContent":["/******************************************************************************\n *\n * This module provides a wrapper for user-interface modules, handling hiding\n * of the textArea that is being replaced by the UI element,\n * resizing of the UI component, and support of such usability functions as\n * ctrl-alt-M to disable/re-enable the entire user interface, including the\n * wrapper.\n *\n * @module coderunner/userinterfacewrapper\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * The InterfaceWrapper class is constructed either by Moodle PHP calls of\n * the form\n *\n * $PAGE->requires->js_call_amd($modulename, $functionname, $params)\n *\n * (e.g. from within render.php) or by JavaScript require calls, e.g. from\n * authorform.js when the question author changes UI type.\n *\n * The InterfaceWrapper provides:\n *\n * 1. A constructor InterfaceWrapper(uiname, textareaId) which\n * hides the given text area, replaces it with a wrapper div (resizable in\n * height by the user but with width resizing managed by changes in window\n * width), created an instance of nameInstance as defined in the file\n * ui_name.js (see below).\n * params is a record containing the decoded value of\n *\n * 2. A stop() method that destroys the embedded UI and hides the wrapper.\n *\n * 3. A restart() method that shows the wrapper again and re-creates the prior\n * embedded UI component within it.\n *\n * 4. A loadUi(uiname, params) method that kills any currently running UI element\n * (if there is one) and (re)loads the specified one. The params parameter\n * is a record that allows additional parameters to be passed in, such as\n * those from the question's uiParams field and, in the case of the\n * Ace UI, the 'lang' (language) that the editor is editing. This data\n * is supplied by the PHP via the data-params attribute of the answer's\n * base textarea.\n *\n * 5. Regular checking for any resizing of the wrapper, which are passed on to\n * the embedded UI element's resize() method.\n *\n * 6. Monitoring of alt-ctrl-M key presses which toggle the visibility of the\n * wrapper plus UI element and the syncronised textArea by calls to stop()\n * and restart\n *\n * =========================================================================\n *\n * The embedded user-interface module must be defined in a JavaScript file\n * of the form ui_name.js which must define a class nameInstance with\n * the following functionality:\n *\n * 1. A constructor SomeUiName(textareaId, width, height, params) that\n * builds an HTML component of the given width and height. textareaId is the\n * ID of the textArea from which the UI element should obtain its initial\n * serialisation and to which it should write the serialisation when its save\n * or destroy methods are called. params is a JavaScript object,\n * decoded from the JSON uiParams defined by the question plus any\n * additional data required, such as the 'lang' in the case of Ace.\n *\n * 2. A getElement() method that returns the HTML element that the\n * InterfaceWrapper is to insert into the document tree.\n *\n * 3. A method failed() that should return true unless the constructor\n * failed (e.g. because it was not able to de-serialise the text area's\n * contents). The wrapper will call destroy() on the object if failed()\n * returns true and abort the use of the UI element. The text area will\n * have the uiloadfailed class added, which CSS will display in some\n * error mode (e.g. a red border).\n *\n * 4. A method failMessage() that will be called only when failed() returns\n * True. It should be a defined CodeRunner language string key.\n *\n * 5. A sync() method that copies the serialised represention of the UI plugin's\n * data to the related TextArea. This is used when submit is clicked.\n *\n * 6. A destroy() method that should sync the contents to the text area then\n * destroy any HTML elements or other created content. This method is called\n * when CTRL-ALT-M is typed by the user to turn off all UI plugins\n *\n * 7. A resize(width, height) method that should resize the entire UI element\n * to the given dimensions.\n *\n * 8. A hasFocus() method that returns true if the UI element has focus.\n *\n * 9. A syncIntervalSecs() method that returns the time interval between\n * calls to the sync() method. 0 for no sync calls. The userinterfacewrapper\n * provides all instances with a generic (base-class) version that returns\n * the value of a UI parameter sync_interval_secs if given else uses the\n * UI interface wrapper default (currently 10).\n *\n * The return value from the module define is a record with a single field\n * 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)\n *\n *****************************************************************************/\n\n/**\n * 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 util.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\ndefine(['jquery'], function($) {\n /**\n * Constructor for a new user interface.\n * @param {string} uiname The name of the interface element (e.g. ace, graph, etc)\n * which should be in file ui_ace.js, ui_graph.js etc.\n * @param {string} textareaId The id of the text area that the UI is to manage.\n * The text area should have an attribute data-params, which is a\n * JSON encoded record containing whatever additional parameters might\n * be needed by the User interface. As a minimum it should contain all\n * the parameters from the uiparameters field of\n * the question so that question authors can pass in additional data\n * such as whether graph edges are bidirectional or not in the case of\n * the graph UI. Additionally the Ace editor requires a 'lang' field\n * to specify what language the editor is editing.\n * When the wrapper has been set up on a text area, the text area's\n * data attribute contains an entry for 'current-ui-wrapper' that is\n * a reference to the wrapper ('this').\n */\n function InterfaceWrapper(uiname, textareaId) {\n let t = this; // For use by embedded functions.\n\n this.GUTTER = 14; // Size of gutter at base of wrapper Node (pixels)\n this.DEFAULT_SYNC_INTERVAL_SECS = 5;\n\n const PIXELS_PER_ROW = 19; // For estimating height of textareas.\n const MAX_GROWN_ROWS = 50; // Upper limit to artifically grown textarea rows.\n const MIN_WRAPPER_HEIGHT = 50;\n\n this.taId = textareaId;\n this.loadFailId = textareaId + '_loadfailerr';\n const ta = document.getElementById(textareaId);\n this.textArea = $(ta);\n const params = this.textArea.attr('data-params');\n if (params) {\n this.uiParams = JSON.parse(params);\n } else {\n this.uiParams = {};\n }\n this.uiParams.lang = this.textArea.attr('data-lang');\n this.readOnly = this.textArea.prop('readonly');\n this.isLoading = false; // True if we're busy loading a UI element.\n this.loadFailed = false; // True if UI failed to initialise properly.\n this.retries = 0; // Number of failed attempts to load a UI component.\n\n let h = parseInt(this.textArea.css(\"height\"));\n let content_lines = this.textArea.val().split('\\n').length;\n let rows = ta.rows;\n if (content_lines > rows) {\n // Allow reloaded text areas with lots of text to grow bigger, within limits.\n rows = Math.min(content_lines, MAX_GROWN_ROWS);\n }\n h = Math.max(h, rows * PIXELS_PER_ROW, MIN_WRAPPER_HEIGHT);\n\n /**\n * Construct an empty hidden wrapper div, inserted directly after the\n * textArea, ready to contain the actual UI.\n */\n this.wrapperNode = $(\"\");\n this.textArea.after(this.wrapperNode);\n this.wrapperNode.hide();\n this.wrapperNode.css({\n resize: 'vertical',\n overflow: 'hidden',\n minHeight: h,\n width: \"100%\",\n border: \"1px solid darkgrey\"\n });\n\n /**\n * Record a reference to this wrapper in the text area's data attribute\n * for use by external javascript that needs to interact with the\n * wrapper, e.g. the multilanguage.js module.\n */\n this.textArea.data('current-ui-wrapper', this);\n\n /**\n * Load the UI into the wrapper (aysnchronous).\n */\n this.uiInstance = null; // Defined by loadUi asynchronously\n this.loadUi(uiname, this.uiParams); // Load the required UI element\n\n /**\n * Add event handlers\n */\n $(document).mousemove(function() {\n t.checkForResize();\n });\n $(window).resize(function() {\n t.checkForResize();\n });\n this.textArea.closest('form').submit(function() {\n if (t.uiInstance !== null) {\n t.uiInstance.sync();\n }\n });\n $(document.body).on('keydown', function(e) {\n const KEY_M = 77;\n if (e.keyCode === KEY_M && e.ctrlKey && e.altKey) {\n if (t.uiInstance !== null || t.loadFailed) {\n t.stop();\n } else {\n t.restart(); // Reactivate\n }\n }\n });\n }\n\n /**\n * Load the specified UI element (which in the case of Ace will need\n * to know the language, lang, as well - this must be supplied as\n * a 'lang' attribute of the record params.\n * When ui is up and running, this.uiInstance will reference it.\n * To avoid a potential race problem, if this method is already busy\n * with a load, try again in 200 msecs.\n * @param {string} uiname The name of the User Interface to be used.\n * @param {object} params The UI parameters object that passes parameters\n * to the actual UI object.\n */\n InterfaceWrapper.prototype.loadUi = function(uiname, params) {\n const t = this,\n errPart1 = 'Failed to load ',\n errPart2 = ' UI component. If this error persists, please report it to the forum on coderunner.org.nz';\n\n /**\n * Get the given language string and plug it into the given jQuery\n * div element as its html, plus a 'fallback' message on a separate line.\n * @param {string} langString The language string specifier for the error message,\n * to be loaded by AJAX.\n * @param {object} errorDiv The div object into which the error message\n * is to be inserted.\n */\n function setLoadFailMessage(langString, errorDiv) {\n require(['core/str'], function(str) {\n /**\n * Get langString text via AJAX\n */\n const\n s = str.get_string(langString, 'qtype_coderunner'),\n fallback = str.get_string('ui_fallback', 'qtype_coderunner');\n $.when(s, fallback).done(function(s, fallback) {\n errorDiv.html(s + ' ' + fallback);\n });\n });\n }\n\n /**\n * The default method for a UIs sync_interval_secs method.\n * Returns the sync_interval_secs parameter if given, else\n * DEFAULT_SYNC_INTERVAL_SECS.\n */\n function syncIntervalSecsBase() {\n if (params.hasOwnProperty('sync_interval_secs')) {\n return parseInt(params.sync_interval_secs);\n } else {\n return t.DEFAULT_SYNC_INTERVAL_SECS;\n }\n }\n\n if (this.isLoading) { // Oops, we're loading a UI element already\n this.retries += 1;\n if (this.retries > 20) {\n alert(errPart1 + uiname + errPart2);\n this.retries = 0;\n this.loading = 0;\n } else {\n setTimeout(function() {\n t.loadUi(uiname, params);\n }, 200); // Try again in 200 msecs\n }\n return;\n }\n this.retries = 0;\n this.params = params; // Save in case need to restart\n\n this.stop(); // Kill any active UI first\n this.uiname = uiname;\n\n if (this.uiname === '' || this.uiname === 'none' || sessionStorage.getItem('disableUis')) {\n this.uiInstance = null;\n } else {\n this.isLoading = true;\n require(['qtype_coderunner/ui_' + this.uiname],\n function(ui) {\n const h = t.wrapperNode.innerHeight() - t.GUTTER;\n const w = t.wrapperNode.innerWidth();\n const uiInstance = new ui.Constructor(t.taId, w, h, params);\n if (uiInstance.failed()) {\n /*\n * Constructor failed to load serialisation.\n * Set uiloadfailed class on text area.\n */\n t.loadFailed = true;\n t.wrapperNode.hide();\n uiInstance.destroy();\n t.uiInstance = null;\n t.textArea.addClass('uiloadfailed');\n const loadFailDiv = '';\n let jqLoadFailDiv = $(loadFailDiv);\n jqLoadFailDiv.insertBefore(t.textArea);\n setLoadFailMessage(uiInstance.failMessage(), jqLoadFailDiv); // Insert error by AJAX\n } else {\n t.hLast = 0; // Force resize (and hence redraw)\n t.wLast = 0; // ... on first call to checkForResize\n t.textArea.hide();\n t.wrapperNode.show();\n t.wrapperNode.append(uiInstance.getElement());\n t.uiInstance = uiInstance;\n t.loadFailed = false;\n t.checkForResize();\n\n /*\n * Set a default syncIntervalSecs method if uiInstance lacks one.\n */\n let uiInstancePrototype = Object.getPrototypeOf(uiInstance);\n uiInstancePrototype.syncIntervalSecs = uiInstancePrototype.syncIntervalSecs || syncIntervalSecsBase;\n t.startSyncTimer(uiInstance);\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Start a sync timer on the given uiInstance, unless its time interval is 0.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.startSyncTimer = function(uiInstance) {\n const timeout = uiInstance.syncIntervalSecs();\n if (timeout) {\n this.uiInstance.timer = setInterval(function () {\n uiInstance.sync();\n }, timeout * 1000);\n } else {\n this.uiInstance.timer = null;\n }\n };\n\n\n /**\n * Stop the sync timer on the given uiInstance, if running.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.stopSyncTimer = function(uiInstance) {\n if (uiInstance.timer) {\n clearTimeout(uiInstance.timer);\n }\n };\n\n\n InterfaceWrapper.prototype.stop = function() {\n /*\n * Disable (shutdown) the embedded ui component.\n * The wrapper remains active for ctrl-alt-M events, but is hidden.\n */\n if (this.uiInstance !== null) {\n this.stopSyncTimer(this.uiInstance);\n this.textArea.show();\n if (this.uiInstance.hasFocus()) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n this.uiInstance.destroy();\n this.uiInstance = null;\n this.wrapperNode.hide();\n }\n this.loadFailed = false;\n this.textArea.removeClass('uiloadfailed'); // Just in case it failed before\n $(document.getElementById(this.loadFailId)).remove();\n };\n\n /*\n * Re-enable the ui element (e.g. after alt-cntrl-M). This is\n * a full re-initialisation of the ui element.\n */\n InterfaceWrapper.prototype.restart = function() {\n if (this.uiInstance === null) {\n /**\n * Restart the UI component in the textarea\n */\n this.loadUi(this.uiname, this.params);\n }\n };\n\n\n /**\n * Check for wrapper resize - propagate to ui element.\n */\n InterfaceWrapper.prototype.checkForResize = function() {\n const SIZE_HACK = 25; // Horrible but best I can do. TODO: FIXME\n\n if (this.uiInstance) {\n const h = this.wrapperNode.innerHeight();\n const w = this.wrapperNode.innerWidth();\n if (h != this.hLast || w != this.wLast) {\n const xLeft = this.wrapperNode.offset().left;\n const maxWidth = $(window).innerWidth() - xLeft - SIZE_HACK;\n const hAdjusted = h - this.GUTTER;\n const wAdjusted = Math.min(maxWidth, w);\n this.uiInstance.resize(wAdjusted, hAdjusted);\n this.hLast = this.wrapperNode.innerHeight();\n this.wLast = this.wrapperNode.innerWidth();\n }\n }\n };\n\n /**\n * The external entry point from the PHP.\n * @param {string} uiname The name of the User Interface to use e.g. 'ace'\n * @param {string} textareaId The ID of the textarea to be wrapped.\n */\n function newUiWrapper(uiname, textareaId) {\n if (uiname) {\n return new InterfaceWrapper(uiname, textareaId);\n } else {\n return null;\n }\n }\n\n\n return {\n newUiWrapper: newUiWrapper,\n InterfaceWrapper: InterfaceWrapper\n };\n});\n"],"names":["define","$","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","taId","loadFailId","ta","document","getElementById","textArea","params","attr","uiParams","JSON","parse","lang","readOnly","prop","isLoading","loadFailed","retries","h","parseInt","css","content_lines","val","split","length","rows","Math","min","max","wrapperNode","after","hide","resize","overflow","minHeight","width","border","data","uiInstance","loadUi","mousemove","checkForResize","window","closest","submit","sync","body","on","e","keyCode","ctrlKey","altKey","stop","restart","prototype","syncIntervalSecsBase","hasOwnProperty","sync_interval_secs","alert","loading","setTimeout","sessionStorage","getItem","require","ui","innerHeight","w","innerWidth","Constructor","failed","destroy","addClass","loadFailDiv","jqLoadFailDiv","insertBefore","langString","failMessage","errorDiv","str","s","get_string","fallback","when","done","html","hLast","wLast","show","append","getElement","uiInstancePrototype","Object","getPrototypeOf","syncIntervalSecs","startSyncTimer","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","remove","xLeft","offset","left","maxWidth","hAdjusted","wAdjusted","newUiWrapper"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"userinterfacewrapper.min.js","sources":["../src/userinterfacewrapper.js"],"sourcesContent":["/******************************************************************************\n *\n * This module provides a wrapper for user-interface modules, handling hiding\n * of the textArea that is being replaced by the UI element,\n * resizing of the UI component, and support of such usability functions as\n * ctrl-alt-M to disable/re-enable the entire user interface, including the\n * wrapper.\n *\n * @module coderunner/userinterfacewrapper\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * The InterfaceWrapper class is constructed either by Moodle PHP calls of\n * the form\n *\n * $PAGE->requires->js_call_amd($modulename, $functionname, $params)\n *\n * (e.g. from within render.php) or by JavaScript require calls, e.g. from\n * authorform.js when the question author changes UI type.\n *\n * The InterfaceWrapper provides:\n *\n * 1. A constructor InterfaceWrapper(uiname, textareaId) which\n * hides the given text area, replaces it with a wrapper div (resizable in\n * height by the user but with width resizing managed by changes in window\n * width), created an instance of nameInstance as defined in the file\n * ui_name.js (see below).\n * params is a record containing the decoded value of\n *\n * 2. A stop() method that destroys the embedded UI and hides the wrapper.\n *\n * 3. A restart() method that shows the wrapper again and re-creates the prior\n * embedded UI component within it.\n *\n * 4. A loadUi(uiname, params) method that kills any currently running UI element\n * (if there is one) and (re)loads the specified one. The params parameter\n * is a record that allows additional parameters to be passed in, such as\n * those from the question's uiParams field and, in the case of the\n * Ace UI, the 'lang' (language) that the editor is editing. This data\n * is supplied by the PHP via the data-params attribute of the answer's\n * base textarea.\n *\n * 5. Regular checking for any resizing of the wrapper, which are passed on to\n * the embedded UI element's resize() method.\n *\n * 6. Monitoring of alt-ctrl-M key presses which toggle the visibility of the\n * wrapper plus UI element and the syncronised textArea by calls to stop()\n * and restart\n *\n * =========================================================================\n *\n * The embedded user-interface module must be defined in a JavaScript file\n * of the form ui_name.js which must define a class nameInstance with\n * the following functionality:\n *\n * 1. A constructor SomeUiName(textareaId, width, height, params) that\n * builds an HTML component of the given width and height. textareaId is the\n * ID of the textArea from which the UI element should obtain its initial\n * serialisation and to which it should write the serialisation when its save\n * or destroy methods are called. params is a JavaScript object,\n * decoded from the JSON uiParams defined by the question plus any\n * additional data required, such as the 'lang' in the case of Ace.\n *\n * 2. A getElement() method that returns the HTML element that the\n * InterfaceWrapper is to insert into the document tree.\n *\n * 3. A method failed() that should return true unless the constructor\n * failed (e.g. because it was not able to de-serialise the text area's\n * contents). The wrapper will call destroy() on the object if failed()\n * returns true and abort the use of the UI element. The text area will\n * have the uiloadfailed class added, which CSS will display in some\n * error mode (e.g. a red border).\n *\n * 4. A method failMessage() that will be called only when failed() returns\n * True. It should be a defined CodeRunner language string key.\n *\n * 5. A sync() method that copies the serialised represention of the UI plugin's\n * data to the related TextArea. This is used when submit is clicked.\n *\n * 6. A destroy() method that should sync the contents to the text area then\n * destroy any HTML elements or other created content. This method is called\n * when CTRL-ALT-M is typed by the user to turn off all UI plugins\n *\n * 7. A resize(width, height) method that should resize the entire UI element\n * to the given dimensions.\n *\n * 8. A hasFocus() method that returns true if the UI element has focus.\n *\n * 9. A syncIntervalSecs() method that returns the time interval between\n * calls to the sync() method. 0 for no sync calls. The userinterfacewrapper\n * provides all instances with a generic (base-class) version that returns\n * the value of a UI parameter sync_interval_secs if given else uses the\n * UI interface wrapper default (currently 10).\n *\n * The return value from the module define is a record with a single field\n * 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)\n *\n *****************************************************************************/\n\n/**\n * 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 util.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\ndefine(['jquery'], function($) {\n /**\n * Constructor for a new user interface.\n * @param {string} uiname The name of the interface element (e.g. ace, graph, etc)\n * which should be in file ui_ace.js, ui_graph.js etc.\n * @param {string} textareaId The id of the text area that the UI is to manage.\n * The text area should have an attribute data-params, which is a\n * JSON encoded record containing whatever additional parameters might\n * be needed by the User interface. As a minimum it should contain all\n * the parameters from the uiparameters field of\n * the question so that question authors can pass in additional data\n * such as whether graph edges are bidirectional or not in the case of\n * the graph UI. Additionally the Ace editor requires a 'lang' field\n * to specify what language the editor is editing.\n * When the wrapper has been set up on a text area, the text area's\n * data attribute contains an entry for 'current-ui-wrapper' that is\n * a reference to the wrapper ('this').\n */\n function InterfaceWrapper(uiname, textareaId) {\n let t = this; // For use by embedded functions.\n\n this.GUTTER = 14; // Size of gutter at base of wrapper Node (pixels)\n this.DEFAULT_SYNC_INTERVAL_SECS = 5;\n\n const PIXELS_PER_ROW = 19; // For estimating height of textareas.\n const MAX_GROWN_ROWS = 50; // Upper limit to artifically grown textarea rows.\n const MIN_WRAPPER_HEIGHT = 50;\n\n this.taId = textareaId;\n this.loadFailId = textareaId + '_loadfailerr';\n const ta = document.getElementById(textareaId);\n this.textArea = $(ta);\n const params = this.textArea.attr('data-params');\n if (params) {\n this.uiParams = JSON.parse(params);\n } else {\n this.uiParams = {};\n }\n this.uiParams.lang = this.textArea.attr('data-lang');\n this.readOnly = this.textArea.prop('readonly');\n this.isLoading = false; // True if we're busy loading a UI element.\n this.loadFailed = false; // True if UI failed to initialise properly.\n this.retries = 0; // Number of failed attempts to load a UI component.\n\n let h = parseInt(this.textArea.css(\"height\"));\n let content_lines = this.textArea.val().split('\\n').length;\n let rows = ta.rows;\n if (content_lines > rows) {\n // Allow reloaded text areas with lots of text to grow bigger, within limits.\n rows = Math.min(content_lines, MAX_GROWN_ROWS);\n }\n h = Math.max(h, rows * PIXELS_PER_ROW, MIN_WRAPPER_HEIGHT);\n\n /**\n * Construct an empty hidden wrapper div, inserted directly after the\n * textArea, ready to contain the actual UI.\n */\n this.wrapperNode = $(\"\");\n this.textArea.after(this.wrapperNode);\n this.wrapperNode.hide();\n this.wrapperNode.css({\n resize: 'vertical',\n overflow: 'hidden',\n minHeight: h,\n width: \"100%\",\n border: \"1px solid darkgrey\"\n });\n\n /**\n * Record a reference to this wrapper in the text area's data attribute\n * for use by external javascript that needs to interact with the\n * wrapper, e.g. the multilanguage.js module.\n */\n this.textArea.data('current-ui-wrapper', this);\n\n /**\n * Load the UI into the wrapper (aysnchronous).\n */\n this.uiInstance = null; // Defined by loadUi asynchronously\n this.loadUi(uiname, this.uiParams); // Load the required UI element\n\n /**\n * Add event handlers\n */\n $(document).mousemove(function() {\n t.checkForResize();\n });\n $(window).resize(function() {\n t.checkForResize();\n });\n this.textArea.closest('form').submit(function() {\n if (t.uiInstance !== null) {\n t.uiInstance.sync();\n }\n });\n $(document.body).on('keydown', function(e) {\n const KEY_M = 77;\n if (e.keyCode === KEY_M && e.ctrlKey && e.altKey) {\n if (t.uiInstance !== null || t.loadFailed) {\n t.stop();\n } else {\n t.restart(); // Reactivate\n }\n }\n });\n }\n\n /**\n * Load the specified UI element (which in the case of Ace will need\n * to know the language, lang, as well - this must be supplied as\n * a 'lang' attribute of the record params.\n * When ui is up and running, this.uiInstance will reference it.\n * To avoid a potential race problem, if this method is already busy\n * with a load, try again in 200 msecs.\n * @param {string} uiname The name of the User Interface to be used.\n * @param {object} params The UI parameters object that passes parameters\n * to the actual UI object.\n */\n InterfaceWrapper.prototype.loadUi = function(uiname, params) {\n const t = this,\n errPart1 = 'Failed to load ',\n errPart2 = ' UI component. If this error persists, please report it to the forum on coderunner.org.nz';\n\n /**\n * Get the given language string and plug it into the given jQuery\n * div element as its html, plus a 'fallback' message on a separate line.\n * @param {string} langString The language string specifier for the error message,\n * to be loaded by AJAX.\n * @param {object} errorDiv The div object into which the error message\n * is to be inserted.\n */\n function setLoadFailMessage(langString, errorDiv) {\n require(['core/str'], function(str) {\n /**\n * Get langString text via AJAX\n */\n const\n s = str.get_string(langString, 'qtype_coderunner'),\n fallback = str.get_string('ui_fallback', 'qtype_coderunner');\n $.when(s, fallback).done(function(s, fallback) {\n errorDiv.html(s + ' ' + fallback);\n });\n });\n }\n\n /**\n * The default method for a UIs sync_interval_secs method.\n * Returns the sync_interval_secs parameter if given, else\n * DEFAULT_SYNC_INTERVAL_SECS.\n */\n function syncIntervalSecsBase() {\n if (params.hasOwnProperty('sync_interval_secs')) {\n return parseInt(params.sync_interval_secs);\n } else {\n return t.DEFAULT_SYNC_INTERVAL_SECS;\n }\n }\n\n if (this.isLoading) { // Oops, we're loading a UI element already\n this.retries += 1;\n if (this.retries > 20) {\n alert(errPart1 + uiname + errPart2);\n this.retries = 0;\n this.loading = 0;\n } else {\n setTimeout(function() {\n t.loadUi(uiname, params);\n }, 200); // Try again in 200 msecs\n }\n return;\n }\n this.retries = 0;\n this.params = params; // Save in case need to restart\n\n this.stop(); // Kill any active UI first\n this.uiname = uiname;\n\n if (this.uiname === '' || this.uiname === 'none' || sessionStorage.getItem('disableUis')) {\n this.uiInstance = null;\n } else {\n this.isLoading = true;\n require(['qtype_coderunner/ui_' + this.uiname],\n function(ui) {\n const h = t.wrapperNode.innerHeight() - t.GUTTER;\n const w = t.wrapperNode.innerWidth();\n const uiInstance = new ui.Constructor(t.taId, w, h, params);\n if (uiInstance.failed()) {\n /*\n * Constructor failed to load serialisation.\n * Set uiloadfailed class on text area.\n */\n t.loadFailed = true;\n t.wrapperNode.hide();\n uiInstance.destroy();\n t.uiInstance = null;\n t.textArea.addClass('uiloadfailed');\n const loadFailDiv = '';\n let jqLoadFailDiv = $(loadFailDiv);\n jqLoadFailDiv.insertBefore(t.textArea);\n setLoadFailMessage(uiInstance.failMessage(), jqLoadFailDiv); // Insert error by AJAX\n } else {\n t.hLast = 0; // Force resize (and hence redraw)\n t.wLast = 0; // ... on first call to checkForResize\n t.textArea.hide();\n t.wrapperNode.show();\n t.wrapperNode.append(uiInstance.getElement());\n t.uiInstance = uiInstance;\n t.loadFailed = false;\n t.checkForResize();\n\n /*\n * Set a default syncIntervalSecs method if uiInstance lacks one.\n */\n let uiInstancePrototype = Object.getPrototypeOf(uiInstance);\n uiInstancePrototype.syncIntervalSecs = uiInstancePrototype.syncIntervalSecs || syncIntervalSecsBase;\n t.startSyncTimer(uiInstance);\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Start a sync timer on the given uiInstance, unless its time interval is 0.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.startSyncTimer = function(uiInstance) {\n const timeout = uiInstance.syncIntervalSecs();\n if (timeout) {\n this.uiInstance.timer = setInterval(function () {\n uiInstance.sync();\n }, timeout * 1000);\n } else {\n this.uiInstance.timer = null;\n }\n };\n\n\n /**\n * Stop the sync timer on the given uiInstance, if running.\n * @param {object} uiInstance The instance of the user interface object whose\n * timer is to be set up.\n */\n InterfaceWrapper.prototype.stopSyncTimer = function(uiInstance) {\n if (uiInstance.timer) {\n clearTimeout(uiInstance.timer);\n }\n };\n\n\n InterfaceWrapper.prototype.stop = function() {\n /*\n * Disable (shutdown) the embedded ui component.\n * The wrapper remains active for ctrl-alt-M events, but is hidden.\n */\n if (this.uiInstance !== null) {\n this.stopSyncTimer(this.uiInstance);\n this.textArea.show();\n if (this.uiInstance.hasFocus()) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n this.uiInstance.destroy();\n this.uiInstance = null;\n this.wrapperNode.hide();\n }\n this.loadFailed = false;\n this.textArea.removeClass('uiloadfailed'); // Just in case it failed before\n $(document.getElementById(this.loadFailId)).remove();\n };\n\n /*\n * Re-enable the ui element (e.g. after alt-cntrl-M). This is\n * a full re-initialisation of the ui element.\n */\n InterfaceWrapper.prototype.restart = function() {\n if (this.uiInstance === null) {\n /**\n * Restart the UI component in the textarea\n */\n this.loadUi(this.uiname, this.params);\n }\n };\n\n\n /**\n * Check for wrapper resize - propagate to ui element.\n */\n InterfaceWrapper.prototype.checkForResize = function() {\n const SIZE_HACK = 25; // Horrible but best I can do. TODO: FIXME\n\n if (this.uiInstance) {\n const h = this.wrapperNode.innerHeight();\n const w = this.wrapperNode.innerWidth();\n if (h != this.hLast || w != this.wLast) {\n const xLeft = this.wrapperNode.offset().left;\n const maxWidth = $(window).innerWidth() - xLeft - SIZE_HACK;\n const hAdjusted = h - this.GUTTER;\n const wAdjusted = Math.min(maxWidth, w);\n this.uiInstance.resize(wAdjusted, hAdjusted);\n this.hLast = this.wrapperNode.innerHeight();\n this.wLast = this.wrapperNode.innerWidth();\n }\n }\n };\n\n /**\n * The external entry point from the PHP.\n * @param {string} uiname The name of the User Interface to use e.g. 'ace'\n * @param {string} textareaId The ID of the textarea to be wrapped.\n */\n function newUiWrapper(uiname, textareaId) {\n if (uiname) {\n return new InterfaceWrapper(uiname, textareaId);\n } else {\n return null;\n }\n }\n\n\n return {\n newUiWrapper: newUiWrapper,\n InterfaceWrapper: InterfaceWrapper\n };\n});\n"],"names":["define","$","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","taId","loadFailId","ta","document","getElementById","textArea","params","attr","uiParams","JSON","parse","lang","readOnly","prop","isLoading","loadFailed","retries","h","parseInt","css","content_lines","val","split","length","rows","Math","min","max","wrapperNode","after","hide","resize","overflow","minHeight","width","border","data","uiInstance","loadUi","mousemove","checkForResize","window","closest","submit","sync","body","on","e","keyCode","ctrlKey","altKey","stop","restart","prototype","syncIntervalSecsBase","hasOwnProperty","sync_interval_secs","alert","loading","setTimeout","sessionStorage","getItem","require","ui","langString","errorDiv","innerHeight","w","innerWidth","Constructor","failed","destroy","addClass","loadFailDiv","jqLoadFailDiv","insertBefore","failMessage","str","s","get_string","fallback","when","done","html","hLast","wLast","show","append","getElement","uiInstancePrototype","Object","getPrototypeOf","syncIntervalSecs","startSyncTimer","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","remove","xLeft","offset","left","maxWidth","hAdjusted","wAdjusted","newUiWrapper"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/ui_ace_gapfiller.js b/amd/src/ui_ace_gapfiller.js
index 7db4437fa..f81753994 100644
--- a/amd/src/ui_ace_gapfiller.js
+++ b/amd/src/ui_ace_gapfiller.js
@@ -59,7 +59,6 @@ define(['jquery'], function($) {
var Range; // Can't load this until ace has loaded.
const fillChar = " ";
const validChars = /[ !"#$%&'()*+,`\-./0-9:;<=>?@A-Z\[\]\\^_a-z{}|~]/;
- const ACE_DARK_THEME = 'ace/theme/tomorrow_night';
const ACE_LIGHT_THEME = 'ace/theme/textmate';
/**
@@ -121,11 +120,8 @@ define(['jquery'], function($) {
});
this.editor.$blockScrolling = Infinity;
- // Set theme to dark if user-prefers-color-scheme is dark,
- // else use the uiParams theme if provided else use light.
- if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
- this.editor.setTheme(ACE_DARK_THEME);
- } else if (uiParams.theme) {
+ // Use the uiParams theme if provided else use light.
+ if (uiParams.theme) {
this.editor.setTheme("ace/theme/" + uiParams.theme);
} else {
this.editor.setTheme(ACE_LIGHT_THEME);
From 6b6891af2912dfc5cdb3f22c45b1025f6629e579 Mon Sep 17 00:00:00 2001
From: Michelle Hsieh <96509698+fastsandslash@users.noreply.github.com>
Date: Mon, 20 Feb 2023 09:05:18 +1300
Subject: [PATCH 065/188] Merge request (#166)
* Hides test cases if prototype. Better error if trying to run prototype.
---
amd/build/authorform.min.js | 3 ++-
amd/build/authorform.min.js.map | 2 +-
amd/src/authorform.js | 7 ++++++-
classes/jobrunner.php | 6 +++++-
lang/en/qtype_coderunner.php | 1 +
5 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/amd/build/authorform.min.js b/amd/build/authorform.min.js
index 1d7c6c937..85240cc10 100644
--- a/amd/build/authorform.min.js
+++ b/amd/build/authorform.min.js
@@ -5,6 +5,7 @@
* @copyright Richard Lobb, 2015, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/authorform",["jquery","qtype_coderunner/userinterfacewrapper","core/str"],(function($,ui,str){var currentQtype="",JSON_TO_FORM_MAP={template:["#id_template","value",""],iscombinatortemplate:["#id_iscombinatortemplate","checked","",function(value){return"1"===value}],cputimelimitsecs:["#id_cputimelimitsecs","value",""],memlimitmb:["#id_memlimitmb","value",""],sandbox:["#id_sandbox","value","DEFAULT"],sandboxparams:["#id_sandboxparams","value",""],testsplitterre:["#id_testsplitterre","value","",function(splitter){return splitter.replace("\n","\\n")}],allowmultiplestdins:["#id_allowmultiplestdins","checked","",function(value){return"1"===value}],grader:["#id_grader","value","EqualityGrader"],resultcolumns:["#id_resultcolumns","value",""],language:["#id_language","value",""],acelang:["#id_acelang","value",""],uiplugin:["#id_uiplugin","value","ace"]};return{initEditForm:function(){var messagePara,typeCombo=$("#id_coderunnertype"),prototypeDisplay=$("#id_isprototype"),template=$("#id_template"),evaluatePerStudent=$("#id_templateparamsevalpertry"),globalextra=$("#id_globalextra"),prototypeextra=$("#id_prototypeextra"),useace=$("#id_useace"),language=$("#id_language"),acelang=$("#id_acelang"),customise=$("#id_customise"),isCombinator=$("#id_iscombinatortemplate"),testSplitterRe=$("#id_testsplitterre"),allowMultipleStdins=$("#id_allowmultiplestdins"),customisationFieldSet=$("#id_customisationheader"),advancedCustomisation=$("#id_advancedcustomisationheader"),isCustomised=customise.prop("checked"),prototypeType=$("#id_prototypetype"),preloadHdr=$("#id_answerpreloadhdr"),courseId=$('input[name="courseid"]').prop("value"),questiontypeHelpDiv=$("#qtype-help"),precheck=$("select#id_precheck"),testtypedivs=$("div.testtype"),brokenQuestion=$("#id_broken_question"),badQuestionLoad=$("#id_bad_question_load"),uiplugin=$("#id_uiplugin"),uiparameters=$("#id_uiparameters");function setUi(taId,uiname){var lang,uiWrapper,ta=$(document.getElementById(taId)),currentLang=ta.attr("data-lang"),paramsJson=ta.attr("data-params"),params={};ta.attr("data-prototypeextra",prototypeextra.val()),ta.attr("data-globalextra",globalextra.val()),ta.attr("data-test0",$("#id_testcode_0").val());try{params=JSON.parse(paramsJson)}catch(err){}"none"===(uiname=uiname.toLowerCase())&&(uiname=""),"id_templateparams"==taId||"id_uiparameters"==taId?lang="":(lang=language.prop("value"),"id_template"!==taId&&acelang.prop("value")&&(lang=function(acelang){var langs,i;if(acelang.indexOf(",")<0)return acelang;for(langs=acelang.split(","),i=0;i0?langs[0]:""}(acelang.prop("value")))),(uiWrapper=ta.data("current-ui-wrapper"))&&uiWrapper.uiname===uiname&¤tLang==lang||(ta.attr("data-lang",lang),uiWrapper?(params.lang=lang,uiWrapper.loadUi(uiname,params)):uiWrapper=new ui.InterfaceWrapper(uiname,taId))}function setUis(){var uiname=uiplugin.val(),answer=$("#id_answer"),enableUi=!0;if("html"===uiname&&""!==answer.attr("data-params"))try{!1===JSON.parse(answer.attr("data-params")).enable_in_editor&&(enableUi=!1)}catch(error){alert("Invalid UI parameters.")}enableUi&&(setUi("id_answer",uiname),setUi("id_answerpreload",uiname))}function setCustomisationVisibility(isVisible){var display=isVisible?"block":"none";customisationFieldSet.css("display",display),advancedCustomisation.css("display",display),isVisible&&useace.prop("checked")&&setUi("id_template","ace")}function copyFieldsFromQuestionType(newType,response){var formspecifier,attrval,isCombinatorEnabled;for(var key in function(stateOn){var uiWrapper,taIds=["id_template","id_uiparameters"];if(useace.prop("checked"))for(var i=0;i3&&(attrval=(0,formspecifier[3])(attrval)),$(formspecifier[0]).prop(formspecifier[1],attrval);customise.prop("checked",!1),str.get_string("coderunner_question_type","qtype_coderunner").then((function(s){var title,coderunner_descr,html,resultHtml;questiontypeHelpDiv.html((title=newType,coderunner_descr=s,html=response.questiontext,resultHtml='
")}))):loadCustomisationFields(),set_testtype_visibilities(),useace.prop("checked")&&(setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")),loadUiParametersDescription(),customise.on("change",(function(){customise.prop("checked")?setCustomisationVisibility(!0):str.get_string("confirm_proceed","qtype_coderunner").then((function(s){window.confirm(s)?setCustomisationVisibility(!1):customise.prop("checked",!0)}))})),acelang.on("change",check_ace_lang),language.on("change",(function(){useace.prop("checked")&&setUi("id_template","ace"),check_ace_lang()})),typeCombo.on("change",(function(){customise.prop("checked")?str.get_string("question_type_changed","qtype_coderunner").then((function(s){window.confirm(s)&&loadCustomisationFields()})):loadCustomisationFields()})),useace.on("change",(function(){useace.prop("checked")?(setUi("id_template","ace"),setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")):(setUi("id_template",""),setUi("id_templateparams",""),setUi("id_uiparameters",""))})),evaluatePerStudent.on("change",(function(){evaluatePerStudent.is(":checked")&&langStringAlert("templateparamsusingsandbox")})),uiplugin.on("change",(function(){setUis(),loadUiParametersDescription()})),precheck.on("change",set_testtype_visibilities),prototypeType.on("change",(function(){"0"==prototypeType.prop("value")?(testsection.css("display","block"),prototypeDisplay.attr("hidden","1")):(testsection.css("display","none"),prototypeDisplay.removeAttr("hidden"))})),new MutationObserver((function(){setUis()})).observe(preloadHdr.get(0),{attributes:!0}),$("button.replaceexpectedwithgot").click((function(){var gotPre=$(this).prev('pre[id^="id_got_"]'),testCaseId=gotPre.attr("id").replace("id_got_","");$("#id_expected_"+testCaseId).val(gotPre.text()),$("#id_fail_expected_"+testCaseId).html(gotPre.text()),$(".failrow_"+testCaseId).addClass("fixed"),$(this).prop("disabled",!0)})),$(".btn-primary").click((function(){typeCombo.prop("disabled",!1)}))}}}));
+
//# sourceMappingURL=authorform.min.js.map
\ No newline at end of file
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
index adb230c7d..f98a86fd7 100644
--- a/amd/build/authorform.min.js.map
+++ b/amd/build/authorform.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * The sample answer and answer preload have the data-params attribute which contains\n * the UI params in a JSON from the current question merged with the prototype.\n * Both of them are identical and are changed simultaneously; only checking\n * answer as state is identical.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n *\n */\n function setUis() {\n let uiname = uiplugin.val();\n let answer = $('#id_answer');\n let enableUi = true;\n if (uiname === 'html' && answer.attr('data-params') !== '') {\n try {\n let answerparams = JSON.parse(answer.attr('data-params'));\n if (answerparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype.\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n prototypeDisplay.attr('hidden', '1');\n } else {\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","messagePara","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","answer","enableUi","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","trim","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":";;;;;;;AAuBAA,qCAAO,CAAC,SAAU,wCAAyC,aAAa,SAASC,EAAGC,GAAIC,SAGhFC,aAAe,GASfC,iBAAmB,CACnBC,SAAqB,CAAC,eAAgB,QAAS,IAC/CC,qBAAqB,CAAC,2BAA4B,UAAW,GACrC,SAAUC,aACW,MAAVA,QAEnCC,iBAAqB,CAAC,uBAAwB,QAAS,IACvDC,WAAqB,CAAC,iBAAkB,QAAS,IACjDC,QAAqB,CAAC,cAAe,QAAS,WAC9CC,cAAqB,CAAC,oBAAqB,QAAS,IACpDC,eAAqB,CAAC,qBAAsB,QAAS,GAC7B,SAAUC,iBACCA,SAASC,QAAQ,KAAM,SAE1DC,oBAAqB,CAAC,0BAA2B,UAAW,GACpC,SAAUR,aACW,MAAVA,QAEnCS,OAAqB,CAAC,aAAc,QAAS,kBAC7CC,cAAqB,CAAC,oBAAqB,QAAS,IACpDC,SAAqB,CAAC,eAAgB,QAAS,IAC/CC,QAAqB,CAAC,cAAe,QAAS,IAC9CC,SAAqB,CAAC,eAAgB,QAAS,cAomB5C,CAACC,4BAnKIC,YAvbJC,UAAYvB,EAAE,sBACdwB,iBAAmBxB,EAAE,mBACrBK,SAAWL,EAAE,gBACbyB,mBAAqBzB,EAAE,gCACvB0B,YAAc1B,EAAE,mBAChB2B,eAAiB3B,EAAE,sBACnB4B,OAAS5B,EAAE,cACXkB,SAAWlB,EAAE,gBACbmB,QAAUnB,EAAE,eACZ6B,UAAY7B,EAAE,iBACd8B,aAAe9B,EAAE,4BACjB+B,eAAiB/B,EAAE,sBACnBgC,oBAAsBhC,EAAE,2BACxBiC,sBAAwBjC,EAAE,2BAC1BkC,sBAAwBlC,EAAE,mCAC1BmC,aAAeN,UAAUO,KAAK,WAC9BC,cAAgBrC,EAAE,qBAClBsC,WAAatC,EAAE,wBACfuC,SAAWvC,EAAE,0BAA0BoC,KAAK,SAC5CI,oBAAsBxC,EAAE,eACxByC,SAAWzC,EAAE,sBACb0C,aAAe1C,EAAE,gBACjB2C,eAAiB3C,EAAE,uBACnB4C,gBAAkB5C,EAAE,yBACpBoB,SAAWpB,EAAE,gBACb6C,aAAe7C,EAAE,6BAWZ8C,MAAMC,KAAMC,YAEbC,KAIAC,UALAC,GAAKnD,EAAEoD,SAASC,eAAeN,OAE/BO,YAAcH,GAAGI,KAAK,aACtBC,WAAaL,GAAGI,KAAK,eACrBE,OAAS,GAKbN,GAAGI,KAAK,sBAAuB5B,eAAe+B,OAC9CP,GAAGI,KAAK,mBAAoB7B,YAAYgC,OACxCP,GAAGI,KAAK,aAAcvD,EAAE,kBAAkB0D,WAEtCD,OAASE,KAAKC,MAAMJ,YACtB,MAAMK,MAEO,UADfb,OAASA,OAAOc,iBAEZd,OAAS,IAGD,qBAARD,MAAuC,mBAARA,KAC/BE,KAAO,IAEPA,KAAO/B,SAASkB,KAAK,SACR,gBAATW,MAA0B5B,QAAQiB,KAAK,WACvCa,cA2Mc9B,aAClB4C,MAAOC,KACP7C,QAAQ8C,QAAQ,KAAO,SAChB9C,YAEP4C,MAAQ5C,QAAQ+C,MAAM,KACjBF,EAAI,EAAGA,EAAID,MAAMI,OAAQH,OACtBD,MAAMC,GAAGI,SAAS,YACXL,MAAMC,GAAGK,OAAO,EAAGN,MAAMC,GAAGG,OAAS,UAG7CJ,MAAMI,OAAS,EAAIJ,MAAM,GAAK,GAtN1BO,CAAiBnD,QAAQiB,KAAK,aAI7Cc,UAAYC,GAAGoB,KAAK,wBAEHrB,UAAUF,SAAWA,QAAUM,aAAeL,OAI/DE,GAAGI,KAAK,YAAaN,MAEhBC,WAIDO,OAAOR,KAAOA,KACdC,UAAUsB,OAAOxB,OAAQS,SAJzBP,UAAY,IAAIjD,GAAGwE,iBAAiBzB,OAAQD,gBAoB3C2B,aACD1B,OAAS5B,SAASsC,MAClBiB,OAAS3E,EAAE,cACX4E,UAAW,KACA,SAAX5B,QAAoD,KAA/B2B,OAAOpB,KAAK,oBAGS,IADnBI,KAAKC,MAAMe,OAAOpB,KAAK,gBACzBsB,mBACbD,UAAW,GAEjB,MAAOE,OACLC,MAAM,0BAGVH,WACA9B,MAAM,YAAaE,QACnBF,MAAM,mBAAoBE,kBAQzBgC,2BAA2BC,eAC5BC,QAAUD,UAAY,QAAU,OACpChD,sBAAsBkD,IAAI,UAAWD,SACrChD,sBAAsBiD,IAAI,UAAWD,SACjCD,WAAarD,OAAOQ,KAAK,YACzBU,MAAM,cAAe,gBA+CpBsC,2BAA2BC,QAASC,cACrCC,cAAeC,QAZfC,wBAeC,IAAIC,gBAxCwBC,aAE7BzC,UADA0C,MAAQ,CAAC,cAAe,sBAExBhE,OAAOQ,KAAK,eACR,IAAI4B,EAAI,EAAGA,EAAI4B,MAAMzB,OAAQH,KAE7Bd,UADKlD,EAAEoD,SAASC,eAAeuC,MAAM5B,KACtBO,KAAK,wBACHoB,QACbzC,UAAU2C,UACH3C,YAAcyC,SACrBzC,UAAU4C,OA6BtBC,EAA4B,GACZ3F,iBACZmF,cAAgBnF,iBAAiBsF,KACjCF,QAAUF,SAASI,KAAOJ,SAASI,KAAOH,cAAc,GACpDA,cAAcpB,OAAS,IAEvBqB,SADAQ,EAAST,cAAc,IACNC,UAErBxF,EAAEuF,cAAc,IAAInD,KAAKmD,cAAc,GAAIC,SAG/C3D,UAAUO,KAAK,WAAW,GAC1BlC,IAAI+F,WAAW,2BAA4B,oBAAoBC,MAAK,SAAUC,OA2C7DC,MAAOC,iBAAkBC,KAEtCC,WA5CA/D,oBAAoB8D,MA0CPF,MA1CwBf,QA0CjBgB,iBA1C0BF,EA0CRG,KA1CWhB,SAASkB,aA4C1DD,WAAa,2CACjBA,YAAcF,iBACdE,YAAcH,MAAQ,SAAWE,UA3CjCtB,4BAA2B,GA9BvBS,oBAAsB3D,aAAaM,KAAK,WAE5CL,eAAeK,KAAK,YAAaqD,qBACjCzD,oBAAoBI,KAAK,YAAaqD,8BAiFjCgB,gBAAgBf,IAAKgB,OACtBC,OAAOC,eAAe,iBAAmBD,OAAOE,cAGpD3G,IAAI+F,WAAWP,IAAK,oBAAoBQ,MAAK,SAASC,OAC9CW,QAAUX,EAAErF,QAAQ,MAAO,KAC3B4F,QACAI,SAAW,KAAOJ,OAEtB3B,MAAM+B,qBAgCLC,8BACD1B,QAAU9D,UAAUyF,SAAS,mBAAmBC,OAEpC,KAAZ5B,SAA8B,cAAZA,UAElB9D,UAAUyF,SAAS,sBAAsB5E,KAAK,WAAY,YAG1DpC,EAAEkH,QAAQC,EAAEC,IAAIC,QAAU,qCACtB,CACIC,MAAOjC,QACPkC,SAAUhF,SACViF,QAASL,EAAEC,IAAII,UAEnB,SAAUC,YAENzH,EAAE,oCAAoC0H,QAClCD,QAAQE,QACRvC,2BAA2BC,QAASoC,SACpC/C,SAEAvE,aAAekF,QACfrF,EAAE,kCAAkC0H,YAEnC,KACKE,qBAzGLC,aAAc/C,WACzB8C,YAAcjE,KAAKC,MAAMkB,cAC/B5E,IAAI+F,WAAW,kBAAmB,oBAAoBC,MAAK,SAASC,GAChEjG,IAAI+F,WAAW2B,YAAY7C,MAAO,mBAAoB8C,cAAc3B,MAAK,SAAShG,KAC9EuG,gBAAgB,yBAA0BvG,SACtC4H,aAAe3B,EAAI,KACvB2B,cAAgB5H,IAAM,KACtB4H,cAAgB,aAAevF,SAAW,YAAcsF,aACxDxH,SAAS+B,KAAK,QAAS0F,oBAGxBF,YA8F6BG,CAAY1C,QAASoC,QAAQ3C,OAG7C3E,eAAiBkF,SAAiC,uBAAtBuC,YAAY9C,kBA4IrCkD,YAAaJ,YAAavC,SACjDnF,IAAI+F,WAAW,qBAAsB,mBACjC,CAAEgC,QAAUD,YAAaE,OAAS7C,QAAS8C,aAAeP,YAAYQ,SAC/DlC,MAAK,SAAShG,KACrBF,EAAE,oCAAoCqI,OAAOrI,EAAE,MAAQE,IAAM,YA/I7CoI,CAAkBnI,aAAcyH,YAAavC,SAC7CrF,EAAE,sBAAsB0D,IAAIvD,mBAI1CoI,MAAK,WAIH9B,gBAAgB,2BAChBpG,SAAS+B,KAAK,QAAS,wCACvBlC,IAAI+F,WAAW,aAAc,oBAAoBC,MAAK,SAASC,GAC3D9F,SAAS+B,KAAK,QAAS+D,mBA4B9BqC,kCACDC,MAAQrH,SAAS4F,SAAS,mBAAmBC,OACjDjH,EAAEkH,QAAQC,EAAEC,IAAIC,QAAU,qCACtB,CACIjG,SAAUqH,MACVlB,SAAUhF,SACViF,QAASL,EAAEC,IAAII,UAEnB,SAAUkB,YAIFC,MAHAC,oBAAsB/F,aAAaa,MACnCmF,oBAAsB7I,EAAE,wBACxB8I,eAAiB9I,EAAE,iDAAmD0I,OAAOK,YAAc,aAE/FF,oBAAoBnB,QACpBmB,oBAAoBR,OAAOK,OAAOM,QACC,GAA/BN,OAAOO,cAAc9E,QAA8C,KAA/ByE,oBAAoBM,QACxDrG,aAAaa,IAAI,IACjB1D,EAAE,+BAA+BmJ,SAEE,GAA/BT,OAAOO,cAAc9E,SACrB0E,oBAAoBR,OAAOS,gBAC3BH,MAAQ3I,WArCSoJ,iBAEKC,MAAOrF,EADzCsC,KAAO,8DACPgD,KAAOF,YAAYG,kBACvBjD,MAAQ,WAAagD,KAAK,GAAK,YAAcA,KAAK,GAAK,YAAcA,KAAK,GAAK,eAC1EtF,EAAI,EAAGA,EAAIoF,YAAYH,cAAc9E,OAAQH,IAE9CsC,MAAQ,YADR+C,MAAQD,YAAYH,cAAcjF,IACP,GAAK,YAAcqF,MAAM,GAAK,YAAcA,MAAM,GAAK,sBAEtF/C,KAAQ,mBA6BkBkD,CAA4Bd,SACtCG,oBAAoBR,OAAOM,OAC3BA,MAAMQ,OACNL,eAAeW,OAAM,WACbX,eAAexC,QAAUoC,OAAOK,aAChCJ,MAAMe,OACNZ,eAAexC,KAAKoC,OAAOiB,eAE3BhB,MAAMQ,OACNL,eAAexC,KAAKoC,OAAOK,kBAIvC/I,EAAE,+BAA+B0J,OAC7B9H,OAAOQ,KAAK,YACZU,MAAM,kBAAmB,WAIvCyF,MAAK,WAEH9B,gBAAgB,sCAQfmD,4BACkB,MAAnBnH,SAASiB,MACThB,aAAagH,OAEbhH,aAAayG,gBAQZU,iBACkB,QAAnBzI,SAASsC,OACTgB,SAiD2B,GAA/BrC,cAAcD,KAAK,WAEnBZ,iBAAiBsI,WAAW,UACO,GAA/BzH,cAAcD,KAAK,WAEnBlC,IAAI+F,WAAW,sBAAuB,oBAAoBC,MAAK,SAASC,GACpEpB,MAAMoB,MAEV9D,cAAcD,KAAK,YAAY,GAC/BP,UAAUO,KAAK,YAAY,KArC3Bd,YAAc,KACY,KAFFqB,eAAeP,KAAK,WAG5Cd,YAActB,EAAE,MAAQ2C,eAAeP,KAAK,SAAW,QACvDpC,EAAE,kCAAkCqI,OAAO/G,cAuCnDsB,gBAAgBR,KAAK,UAErBjC,aAAeoB,UAAUyF,SAAS,mBAAmBC,OAErDjC,2BAA2B7C,cACtBA,cAIDuC,SACAxE,IAAI+F,WAAW,mBAAoB,oBAAoBC,MAAK,SAASC,GACjE3D,oBAAoB8D,KAAK,MAAQH,EAAI,YAJzCY,0BAQJ6C,4BAEIhI,OAAOQ,KAAK,aACZU,MAAM,oBAAqB,OAC3BA,MAAM,kBAAmB,QAG7B0F,8BAIA3G,UAAUkI,GAAG,UAAU,WACAlI,UAAUO,KAAK,WAG9B4C,4BAA2B,GAE3B9E,IAAI+F,WAAW,kBAAmB,oBAAoBC,MAAK,SAASC,GAC5DQ,OAAOqD,QAAQ7D,GACfnB,4BAA2B,GAE3BnD,UAAUO,KAAK,WAAW,SAM1CjB,QAAQ4I,GAAG,SAAUF,gBACrB3I,SAAS6I,GAAG,UAAU,WAjGdnI,OAAOQ,KAAK,YACZU,MAAM,cAAe,OAkGzB+G,oBAGJtI,UAAUwI,GAAG,UAAU,WACflI,UAAUO,KAAK,WAEflC,IAAI+F,WAAW,wBAAyB,oBAAoBC,MAAK,SAAUC,GACnEQ,OAAOqD,QAAQ7D,IACfY,6BAIRA,6BAIRnF,OAAOmI,GAAG,UAAU,WACEnI,OAAOQ,KAAK,YAE1BU,MAAM,cAAe,OACrBA,MAAM,oBAAqB,OAC3BA,MAAM,kBAAmB,SAEzBA,MAAM,cAAe,IACrBA,MAAM,oBAAqB,IAC3BA,MAAM,kBAAmB,QAIjCrB,mBAAmBsI,GAAG,UAAU,WACxBtI,mBAAmBwI,GAAG,aACtBxD,gBAAgB,iCAIxBrF,SAAS2I,GAAG,UAAU,WAClBrF,SACA8D,iCAGJ/F,SAASsH,GAAG,SAAUH,2BAGtBvH,cAAc0H,GAAG,UAAU,WACY,KAA/B1H,cAAcD,KAAK,SACnBZ,iBAAiB+B,KAAK,SAAU,KAEhC/B,iBAAiBsI,WAAW,aAOrB,IAAII,kBAAkB,WACjCxF,YAEKyF,QAAQ7H,WAAW8H,IAAI,GAAI,aAAe,IAInDpK,EAAE,iCAAiCyJ,OAAM,eACjCY,OAASrK,EAAEsK,MAAMC,KAAK,sBACtBC,WAAaH,OAAO9G,KAAK,MAAMzC,QAAQ,UAAW,IACtDd,EAAE,gBAAkBwK,YAAY9G,IAAI2G,OAAOpD,QAC3CjH,EAAE,qBAAuBwK,YAAYlE,KAAK+D,OAAOpD,QACjDjH,EAAE,YAAcwK,YAAYC,SAAS,SACrCzK,EAAEsK,MAAMlI,KAAK,YAAY,MAI7BpC,EAAE,gBAAgByJ,OAAM,WACpBlI,UAAUa,KAAK,YAAY"}
\ No newline at end of file
+{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n testsection = $('#id_testcasehdr'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * The sample answer and answer preload have the data-params attribute which contains\n * the UI params in a JSON from the current question merged with the prototype.\n * Both of them are identical and are changed simultaneously; only checking\n * answer as state is identical.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n *\n */\n function setUis() {\n let uiname = uiplugin.val();\n let answer = $('#id_answer');\n let enableUi = true;\n if (uiname === 'html' && answer.attr('data-params') !== '') {\n try {\n let answerparams = JSON.parse(answer.attr('data-params'));\n if (answerparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype and hide testboxes.\n testsection.css('display', 'none');\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n // Also hides/shows the test cases section if prototype/not prototype.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n testsection.css('display', 'block');\n prototypeDisplay.attr('hidden', '1');\n } else {\n testsection.css('display', 'none');\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","testsection","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","answer","enableUi","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","trim","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
diff --git a/amd/src/authorform.js b/amd/src/authorform.js
index 66f7df7bf..dfd45cfd6 100644
--- a/amd/src/authorform.js
+++ b/amd/src/authorform.js
@@ -87,6 +87,7 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
questiontypeHelpDiv = $('#qtype-help'),
precheck = $('select#id_precheck'),
testtypedivs = $('div.testtype'),
+ testsection = $('#id_testcasehdr'),
brokenQuestion = $('#id_broken_question'),
badQuestionLoad = $('#id_bad_question_load'),
uiplugin = $('#id_uiplugin'),
@@ -533,7 +534,8 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
*************************************************************/
if (prototypeType.prop('value') != 0) {
- // Display the prototype warning if it's a prototype.
+ // Display the prototype warning if it's a prototype and hide testboxes.
+ testsection.css('display', 'none');
prototypeDisplay.removeAttr('hidden');
if (prototypeType.prop('value') == 1) {
// Editing a built-in question type: Dangerous!
@@ -634,10 +636,13 @@ define(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function
precheck.on('change', set_testtype_visibilities);
// Displays and hides the reason for the question type to be disabled.
+ // Also hides/shows the test cases section if prototype/not prototype.
prototypeType.on('change', function () {
if (prototypeType.prop('value') == '0') {
+ testsection.css('display', 'block');
prototypeDisplay.attr('hidden', '1');
} else {
+ testsection.css('display', 'none');
prototypeDisplay.removeAttr('hidden');
}
});
diff --git a/classes/jobrunner.php b/classes/jobrunner.php
index fe51514b8..087ed501c 100644
--- a/classes/jobrunner.php
+++ b/classes/jobrunner.php
@@ -48,8 +48,12 @@ public function run_tests($question, $code, $attachments, $testcases, $isprechec
if (empty($question->prototype)) {
// Missing prototype. We can't run this question.
$outcome = new qtype_coderunner_testing_outcome(0, 0, false);
- $message = get_string('missingprototypewhenrunning', 'qtype_coderunner',
+ if ($question->prototypetype != 0) {
+ $message = get_string('cannotrunprototype', 'qtype_coderunner');
+ } else {
+ $message = get_string('missingprototypewhenrunning', 'qtype_coderunner',
array('crtype' => $question->coderunnertype));
+ }
$outcome->set_status(qtype_coderunner_testing_outcome::STATUS_MISSING_PROTOTYPE, $message);
return $outcome;
}
diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php
index 67a6dd147..c0d566213 100644
--- a/lang/en/qtype_coderunner.php
+++ b/lang/en/qtype_coderunner.php
@@ -98,6 +98,7 @@
$string['bulktestrun'] = 'Run all the question tests for all the questions in the system (slow, admin only)';
$string['bulktesttitle'] = 'Testing questions in {$a}';
+$string['cannotrunprototype'] = 'This is a prototype and cannot be run. If you wish to use this prototype, create a new question and set this question type.';
$string['coderunnercategories'] = 'Categories with CodeRunner questions';
$string['coderunnercontexts'] = 'Contexts with CodeRunner questions';
$string['coderunner'] = 'Program Code';
From 30901f8eaff41c74a4e70ca46f3afadd81af903c Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Mon, 20 Feb 2023 20:49:58 +1300
Subject: [PATCH 066/188] Bug fix: extraneous errors with questions that did
not have sample answer attachments.
---
questiontestrun.php | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/questiontestrun.php b/questiontestrun.php
index 90e0acb1e..4ac1db06e 100644
--- a/questiontestrun.php
+++ b/questiontestrun.php
@@ -96,7 +96,10 @@
// Test the question with its sample answer.
$response = $question->get_correct_response();
-$runparams = array('-submit' => 'Submit', 'answer' => $response['answer'], 'attachments' => $response['attachments']);
+$runparams = array('-submit' => 'Submit', 'answer' => $response['answer']);
+if (isset($response['attachments'])) {
+ $runparams['attachments'] = $response['attachments'];
+}
$templateparams = isset($question->templateparams) ? json_decode($question->templateparams, true) : array();
if (isset($templateparams['answer_language'])) {
$runparams['language'] = $templateparams['answer_language'];
From 07e30771936d34f25c7b244ee00cdabf5ca6d60a Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Thu, 23 Feb 2023 20:55:06 +1300
Subject: [PATCH 067/188] Legacy support for all question types with
unspecified uiplugin.
---
edit_coderunner_form.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/edit_coderunner_form.php b/edit_coderunner_form.php
index 0a72a3b95..f2ad0ba3a 100644
--- a/edit_coderunner_form.php
+++ b/edit_coderunner_form.php
@@ -1006,7 +1006,7 @@ private function validate_template_params() {
if ($errormessage === '') {
// Check for legacy case of ui parameters defined within the template params.
- $uiplugin = $this->formquestion->uiplugin;
+ $uiplugin = $this->formquestion->uiplugin ? $this->formquestion->uiplugin : 'ace';
$uiparams = new qtype_coderunner_ui_parameters($uiplugin);
$templateparamsnoprototype = json_decode($this->formquestion->template_params_json($seed), true);
$alluiparamnames = $uiparams->all_names();
From 79ab52f6f3546fddeadca17e5d217fa922cee3ab Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Tue, 7 Mar 2023 14:03:55 +1300
Subject: [PATCH 068/188] Tweak name of behat alert testing function to avoid
apparent conflict with the same test in the ace_inline_filter plugin.
---
tests/behat/behat_coderunner.php | 2 +-
tests/behat/scratchpad_ui_params.feature | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/tests/behat/behat_coderunner.php b/tests/behat/behat_coderunner.php
index 9e3ec5565..e107eda8e 100644
--- a/tests/behat/behat_coderunner.php
+++ b/tests/behat/behat_coderunner.php
@@ -288,7 +288,7 @@ public function i_set_the_field_and_dismiss_the_alert($field, $value) {
/**
* Presses a named button. Checks if there is a specified error text displayed.
*
- * @Then I should see an alert of :error when I press :button
+ * @Then I should see the alert :error when I press :button
* @param string $errortext The expected error message when alerted
* @param string $button The name of the alert button.
*/
diff --git a/tests/behat/scratchpad_ui_params.feature b/tests/behat/scratchpad_ui_params.feature
index 5b4d7faa8..58fcd7a98 100644
--- a/tests/behat/scratchpad_ui_params.feature
+++ b/tests/behat/scratchpad_ui_params.feature
@@ -468,7 +468,7 @@ Feature: Test the Scratchpad UI, UI Params
'stderr' : '',
'files' : ''
"""
- Then I should see an alert of "Error parsing display JSON output:" when I press "Run"
+ Then I should see the alert "Error parsing display JSON output:" when I press "Run"
Scenario: Set output_display_mode to json, 'print' a hidden div to output area
And I set the field "id_answer" to ""
@@ -491,7 +491,7 @@ Feature: Test the Scratchpad UI, UI Params
}
print(json.dumps(output))
"""
- Then I should see an alert of "stderr" when I press "Run"
+ Then I should see the alert "stderr" when I press "Run"
Scenario: Set output_display_mode to json, get no output.
And I set the field "id_answer" to ""
From 2905590f11f49da2ddbe9915f58ea0e47e0b464f Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Tue, 7 Mar 2023 16:19:47 +1300
Subject: [PATCH 069/188] Increase time allowed for question initialisation on
Jobe to 10 secs.
---
question.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/question.php b/question.php
index 9c669a256..8e84044c2 100644
--- a/question.php
+++ b/question.php
@@ -262,7 +262,7 @@ private function evaluate_template_params_on_jobe($templateparams, $lang, $seed)
$value = preg_replace("/[^A-Za-z0-9]/", '', $this->student->$key);
$runargs[] = "$key=" . $value;
}
- $sandboxparams = array("runargs" => $runargs);
+ $sandboxparams = array("runargs" => $runargs, "cputime" => 10);
$sandbox = $this->get_sandbox();
$run = $sandbox->execute($templateparams, $lang, $input, $files, $sandboxparams);
if ($run->error === qtype_coderunner_sandbox::SERVER_OVERLOAD) {
From 9e4d3424183c6c7f3ccd2e8fd2512fb03b158e4c Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Tue, 14 Mar 2023 09:48:45 +1300
Subject: [PATCH 070/188] Bug fix: per-question Ace theme setting not working
properly.
---
amd/build/authorform.min.js | 3 +--
amd/build/authorform.min.js.map | 2 +-
amd/build/ui_ace.min.js | 2 +-
amd/build/ui_ace.min.js.map | 2 +-
amd/src/ui_ace.js | 43 +++++++++++++++++----------------
5 files changed, 26 insertions(+), 26 deletions(-)
diff --git a/amd/build/authorform.min.js b/amd/build/authorform.min.js
index 85240cc10..2e94ca808 100644
--- a/amd/build/authorform.min.js
+++ b/amd/build/authorform.min.js
@@ -5,7 +5,6 @@
* @copyright Richard Lobb, 2015, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/authorform",["jquery","qtype_coderunner/userinterfacewrapper","core/str"],(function($,ui,str){let currentQtype="";var JSON_TO_FORM_MAP={template:["#id_template","value",""],iscombinatortemplate:["#id_iscombinatortemplate","checked","",function(value){return"1"===value}],cputimelimitsecs:["#id_cputimelimitsecs","value",""],memlimitmb:["#id_memlimitmb","value",""],sandbox:["#id_sandbox","value","DEFAULT"],sandboxparams:["#id_sandboxparams","value",""],testsplitterre:["#id_testsplitterre","value","",function(splitter){return splitter.replace("\n","\\n")}],allowmultiplestdins:["#id_allowmultiplestdins","checked","",function(value){return"1"===value}],grader:["#id_grader","value","EqualityGrader"],resultcolumns:["#id_resultcolumns","value",""],language:["#id_language","value",""],acelang:["#id_acelang","value",""],uiplugin:["#id_uiplugin","value","ace"]};return{initEditForm:function(){var typeCombo=$("#id_coderunnertype"),prototypeDisplay=$("#id_isprototype"),template=$("#id_template"),evaluatePerStudent=$("#id_templateparamsevalpertry"),globalextra=$("#id_globalextra"),prototypeextra=$("#id_prototypeextra"),useace=$("#id_useace"),language=$("#id_language"),acelang=$("#id_acelang"),customise=$("#id_customise"),isCombinator=$("#id_iscombinatortemplate"),testSplitterRe=$("#id_testsplitterre"),allowMultipleStdins=$("#id_allowmultiplestdins"),customisationFieldSet=$("#id_customisationheader"),advancedCustomisation=$("#id_advancedcustomisationheader"),isCustomised=customise.prop("checked"),prototypeType=$("#id_prototypetype"),preloadHdr=$("#id_answerpreloadhdr"),courseId=$('input[name="courseid"]').prop("value"),questiontypeHelpDiv=$("#qtype-help"),precheck=$("select#id_precheck"),testtypedivs=$("div.testtype"),testsection=$("#id_testcasehdr"),brokenQuestion=$("#id_broken_question"),badQuestionLoad=$("#id_bad_question_load"),uiplugin=$("#id_uiplugin"),uiparameters=$("#id_uiparameters");function setUi(taId,uiname){var lang,uiWrapper,ta=$(document.getElementById(taId)),currentLang=ta.attr("data-lang"),paramsJson=ta.attr("data-params"),params={};ta.attr("data-prototypeextra",prototypeextra.val()),ta.attr("data-globalextra",globalextra.val()),ta.attr("data-test0",$("#id_testcode_0").val());try{params=JSON.parse(paramsJson)}catch(err){}"none"===(uiname=uiname.toLowerCase())&&(uiname=""),"id_templateparams"==taId||"id_uiparameters"==taId?lang="":(lang=language.prop("value"),"id_template"!==taId&&acelang.prop("value")&&(lang=function(acelang){var langs,i;if(acelang.indexOf(",")<0)return acelang;for(langs=acelang.split(","),i=0;i0?langs[0]:""}(acelang.prop("value")))),(uiWrapper=ta.data("current-ui-wrapper"))&&uiWrapper.uiname===uiname&¤tLang==lang||(ta.attr("data-lang",lang),uiWrapper?(params.lang=lang,uiWrapper.loadUi(uiname,params)):uiWrapper=new ui.InterfaceWrapper(uiname,taId))}function setUis(){let uiname=uiplugin.val(),answer=$("#id_answer"),enableUi=!0;if("html"===uiname&&""!==answer.attr("data-params"))try{!1===JSON.parse(answer.attr("data-params")).enable_in_editor&&(enableUi=!1)}catch(error){alert("Invalid UI parameters.")}enableUi&&(setUi("id_answer",uiname),setUi("id_answerpreload",uiname))}function setCustomisationVisibility(isVisible){var display=isVisible?"block":"none";customisationFieldSet.css("display",display),advancedCustomisation.css("display",display),isVisible&&useace.prop("checked")&&setUi("id_template","ace")}function copyFieldsFromQuestionType(newType,response){var formspecifier,attrval,isCombinatorEnabled;for(var key in function(stateOn){var uiWrapper,taIds=["id_template","id_uiparameters"];if(useace.prop("checked"))for(var i=0;i3&&(attrval=(0,formspecifier[3])(attrval)),$(formspecifier[0]).prop(formspecifier[1],attrval);customise.prop("checked",!1),str.get_string("coderunner_question_type","qtype_coderunner").then((function(s){var title,coderunner_descr,html,resultHtml;questiontypeHelpDiv.html((title=newType,coderunner_descr=s,html=response.questiontext,resultHtml='
")}))):loadCustomisationFields(),set_testtype_visibilities(),useace.prop("checked")&&(setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")),loadUiParametersDescription(),customise.on("change",(function(){customise.prop("checked")?setCustomisationVisibility(!0):str.get_string("confirm_proceed","qtype_coderunner").then((function(s){window.confirm(s)?setCustomisationVisibility(!1):customise.prop("checked",!0)}))})),acelang.on("change",check_ace_lang),language.on("change",(function(){useace.prop("checked")&&setUi("id_template","ace"),check_ace_lang()})),typeCombo.on("change",(function(){customise.prop("checked")?str.get_string("question_type_changed","qtype_coderunner").then((function(s){window.confirm(s)&&loadCustomisationFields()})):loadCustomisationFields()})),useace.on("change",(function(){useace.prop("checked")?(setUi("id_template","ace"),setUi("id_templateparams","ace"),setUi("id_uiparameters","ace")):(setUi("id_template",""),setUi("id_templateparams",""),setUi("id_uiparameters",""))})),evaluatePerStudent.on("change",(function(){evaluatePerStudent.is(":checked")&&langStringAlert("templateparamsusingsandbox")})),uiplugin.on("change",(function(){setUis(),loadUiParametersDescription()})),precheck.on("change",set_testtype_visibilities),prototypeType.on("change",(function(){"0"==prototypeType.prop("value")?(testsection.css("display","block"),prototypeDisplay.attr("hidden","1")):(testsection.css("display","none"),prototypeDisplay.removeAttr("hidden"))})),new MutationObserver((function(){setUis()})).observe(preloadHdr.get(0),{attributes:!0}),$("button.replaceexpectedwithgot").click((function(){var gotPre=$(this).prev('pre[id^="id_got_"]'),testCaseId=gotPre.attr("id").replace("id_got_","");$("#id_expected_"+testCaseId).val(gotPre.text()),$("#id_fail_expected_"+testCaseId).html(gotPre.text()),$(".failrow_"+testCaseId).addClass("fixed"),$(this).prop("disabled",!0)})),$(".btn-primary").click((function(){typeCombo.prop("disabled",!1)}))}}}));
//# sourceMappingURL=authorform.min.js.map
\ No newline at end of file
diff --git a/amd/build/authorform.min.js.map b/amd/build/authorform.min.js.map
index f98a86fd7..668cca1e1 100644
--- a/amd/build/authorform.min.js.map
+++ b/amd/build/authorform.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n testsection = $('#id_testcasehdr'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * The sample answer and answer preload have the data-params attribute which contains\n * the UI params in a JSON from the current question merged with the prototype.\n * Both of them are identical and are changed simultaneously; only checking\n * answer as state is identical.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n *\n */\n function setUis() {\n let uiname = uiplugin.val();\n let answer = $('#id_answer');\n let enableUi = true;\n if (uiname === 'html' && answer.attr('data-params') !== '') {\n try {\n let answerparams = JSON.parse(answer.attr('data-params'));\n if (answerparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype and hide testboxes.\n testsection.css('display', 'none');\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n // Also hides/shows the test cases section if prototype/not prototype.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n testsection.css('display', 'block');\n prototypeDisplay.attr('hidden', '1');\n } else {\n testsection.css('display', 'none');\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","testsection","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","answer","enableUi","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","trim","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","messagePara","checkForBrokenQuestion","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":"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"}
+{"version":3,"file":"authorform.min.js","sources":["../src/authorform.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 * JavaScript for handling UI actions in the question authoring form.\n *\n * @module qtype_coderunner/authorform\n * @copyright Richard Lobb, 2015, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'qtype_coderunner/userinterfacewrapper', 'core/str'], function($, ui, str) {\n\n // We need this to keep track of the current question type.\n let currentQtype = \"\";\n\n // Define a mapping from the fields of the JSON object returned by an AJAX\n // 'get question type' request to the form elements. Only fields that\n // belong to the question type should appear here. Keys are JSON field\n // names, values are a 3- or 4-element array of: a jQuery form element selector;\n // the element property to be set; a default value if the JSON field is\n // empty and an optional filter function to apply to the field value before\n // setting the property with it.\n var JSON_TO_FORM_MAP = {\n template: ['#id_template', 'value', ''],\n iscombinatortemplate:['#id_iscombinatortemplate', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }], // Need nice clean boolean for 'checked' attribute.\n cputimelimitsecs: ['#id_cputimelimitsecs', 'value', ''],\n memlimitmb: ['#id_memlimitmb', 'value', ''],\n sandbox: ['#id_sandbox', 'value', 'DEFAULT'],\n sandboxparams: ['#id_sandboxparams', 'value', ''],\n testsplitterre: ['#id_testsplitterre', 'value', '',\n function (splitter) {\n return splitter.replace('\\n', '\\\\n');\n }],\n allowmultiplestdins: ['#id_allowmultiplestdins', 'checked', '',\n function (value) {\n return value === '1' ? true : false;\n }],\n grader: ['#id_grader', 'value', 'EqualityGrader'],\n resultcolumns: ['#id_resultcolumns', 'value', ''],\n language: ['#id_language', 'value', ''],\n acelang: ['#id_acelang', 'value', ''],\n uiplugin: ['#id_uiplugin', 'value', 'ace']\n };\n\n /**\n * Set up the author edit form UI plugins and event handlers.\n * The template parameters and Ace language are passed to each\n * text area from PHP by setting its data-params and\n * data-lang attributes.\n */\n function initEditForm() {\n var typeCombo = $('#id_coderunnertype'),\n prototypeDisplay = $('#id_isprototype'),\n template = $('#id_template'),\n evaluatePerStudent = $('#id_templateparamsevalpertry'),\n globalextra = $('#id_globalextra'),\n prototypeextra = $('#id_prototypeextra'),\n useace = $('#id_useace'),\n language = $('#id_language'),\n acelang = $('#id_acelang'),\n customise = $('#id_customise'),\n isCombinator = $('#id_iscombinatortemplate'),\n testSplitterRe = $('#id_testsplitterre'),\n allowMultipleStdins = $('#id_allowmultiplestdins'),\n customisationFieldSet = $('#id_customisationheader'),\n advancedCustomisation = $('#id_advancedcustomisationheader'),\n isCustomised = customise.prop('checked'),\n prototypeType = $('#id_prototypetype'),\n preloadHdr = $('#id_answerpreloadhdr'),\n courseId = $('input[name=\"courseid\"]').prop('value'),\n questiontypeHelpDiv = $('#qtype-help'),\n precheck = $('select#id_precheck'),\n testtypedivs = $('div.testtype'),\n testsection = $('#id_testcasehdr'),\n brokenQuestion = $('#id_broken_question'),\n badQuestionLoad = $('#id_bad_question_load'),\n uiplugin = $('#id_uiplugin'),\n uiparameters = $('#id_uiparameters');\n\n /**\n * Set up the UI controller for a given textarea (one of template,\n * answer or answerpreload).\n * We don't attempt to process changes in template parameters,\n * as these need to be merged with those of the prototype. But we do handle\n * changes in the language.\n * @param {string} taId The ID of the textarea element.\n * @param {string} uiname The name of the UI controller (may be empty or none).\n */\n function setUi(taId, uiname) {\n var ta = $(document.getElementById(taId)), // The jquery text area element(s).\n lang,\n currentLang = ta.attr('data-lang'), // Language set by PHP.\n paramsJson = ta.attr('data-params'), // Ui params set by PHP.\n params = {},\n uiWrapper;\n\n // Set data attributes in the text area for UI components that need\n // global extra or testcase data (e.g. gapfiller UI).\n ta.attr('data-prototypeextra', prototypeextra.val());\n ta.attr('data-globalextra', globalextra.val());\n ta.attr('data-test0', $('#id_testcode_0').val());\n try {\n params = JSON.parse(paramsJson);\n } catch(err) {}\n uiname = uiname.toLowerCase();\n if (uiname === 'none') {\n uiname = '';\n }\n\n if (taId == 'id_templateparams' || taId == 'id_uiparameters') {\n lang = ''; // These fields may be twigged, so can't be parsed by Ace.\n } else {\n lang = language.prop('value');\n if (taId !== \"id_template\" && acelang.prop('value')) {\n lang = preferredAceLang(acelang.prop('value'));\n }\n }\n\n uiWrapper = ta.data('current-ui-wrapper'); // Currently-active UI wrapper on this ta.\n\n if (uiWrapper && uiWrapper.uiname === uiname && currentLang == lang) {\n return; // We already have what we want - give up.\n }\n\n ta.attr('data-lang', lang);\n\n if (!uiWrapper) {\n uiWrapper = new ui.InterfaceWrapper(uiname, taId);\n } else {\n // Wrapper has already been set up - just reload the reqd UI.\n params.lang = lang;\n uiWrapper.loadUi(uiname, params);\n }\n\n }\n\n /**\n * Set the correct Ui controller on both the sample answer and the answer preload.\n * The sample answer and answer preload have the data-params attribute which contains\n * the UI params in a JSON from the current question merged with the prototype.\n * Both of them are identical and are changed simultaneously; only checking\n * answer as state is identical.\n * As a special case, we don't turn on the Ui controller in the answer\n * and answer preload fields when using Html-Ui and the ui-parameter\n * enable_in_editor is false.\n *\n */\n function setUis() {\n let uiname = uiplugin.val();\n let answer = $('#id_answer');\n let enableUi = true;\n if (uiname === 'html' && answer.attr('data-params') !== '') {\n try {\n let answerparams = JSON.parse(answer.attr('data-params'));\n if (answerparams.enable_in_editor === false) {\n enableUi = false;\n }\n } catch (error) {\n alert(\"Invalid UI parameters.\");\n }\n }\n if (enableUi) {\n setUi('id_answer', uiname);\n setUi('id_answerpreload', uiname);\n }\n }\n\n /**\n * Display or Hide all customisation parts of the form.\n * @param {bool} isVisible True to show, false to hide.\n */\n function setCustomisationVisibility(isVisible) {\n var display = isVisible ? 'block' : 'none';\n customisationFieldSet.css('display', display);\n advancedCustomisation.css('display', display);\n if (isVisible && useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n\n /**\n * Turn on or off the Ace editor in the template and uiparameters fields\n * so we can reload the textareas with JavaScript.\n * Ignore if UseAce is unchecked.\n * @param {bool} stateOn True to stop Ace, false to restart it.\n */\n function enableAceInCustomisedFields(stateOn) {\n var taIds = ['id_template', 'id_uiparameters'];\n var uiWrapper, ta;\n if (useace.prop('checked')) {\n for(var i = 0; i < taIds.length; i++) {\n ta = $(document.getElementById(taIds[i]));\n uiWrapper = ta.data('current-ui-wrapper');\n if (uiWrapper && stateOn) {\n uiWrapper.restart();\n } else if (uiWrapper && !stateOn) {\n uiWrapper.stop();\n }\n }\n }\n }\n\n\n /**\n * After loading the form with new question type data we have to\n * enable or disable both the testsplitterre and the allow multiple\n * stdins field. These are subsequently enabled/disabled via event handlers\n * set up by code in edit_coderunner_form.php (q.v.) but those event\n * handlers do not handle the freshly downloaded state.\n */\n function enableTemplateSupportFields() {\n var isCombinatorEnabled = isCombinator.prop('checked');\n\n testSplitterRe.prop('disabled', !isCombinatorEnabled);\n allowMultipleStdins.prop('disabled', !isCombinatorEnabled);\n }\n\n /**\n * Copy fields from the AJAX \"get question type\" response into the form.\n * @param {string} newType the newly selected question type.\n * @param {object} response The AJAX resopnse.\n */\n function copyFieldsFromQuestionType(newType, response) {\n var formspecifier, attrval, filter;\n\n enableAceInCustomisedFields(false);\n for (var key in JSON_TO_FORM_MAP) {\n formspecifier = JSON_TO_FORM_MAP[key];\n attrval = response[key] ? response[key] : formspecifier[2];\n if (formspecifier.length > 3) {\n filter = formspecifier[3];\n attrval = filter(attrval);\n }\n $(formspecifier[0]).prop(formspecifier[1], attrval);\n }\n\n customise.prop('checked', false);\n str.get_string('coderunner_question_type', 'qtype_coderunner').then(function (s) {\n questiontypeHelpDiv.html(detailsHtml(newType, s, response.questiontext));\n });\n\n setCustomisationVisibility(false);\n enableTemplateSupportFields();\n }\n\n /**\n * A JSON request for a question type returned a 'failure' response - probably a\n * missing question type. Report the error with an alert, and replace\n * the template contents with an error message in case the user\n * saves the question and later wonders why it breaks.\n * Returns the JSON error object for further use.\n * @param {string} questionType The CodeRunner (sub) question type.\n * @param {string} error The error message as JSON encoded error => langstring,\n * extra => components string.\n * @return {JSON object} The JSON error object for further parsing.\n */\n function reportError(questionType, error) {\n const errorObject = JSON.parse(error);\n str.get_string('prototype_error', 'qtype_coderunner').then(function(s) {\n str.get_string(errorObject.alert, 'qtype_coderunner', questionType).then(function(str) {\n langStringAlert('prototype_load_failure', str);\n let errorMessage = s + \"\\n\";\n errorMessage += str + '\\n';\n errorMessage += \"CourseId: \" + courseId + \", qtype: \" + questionType;\n template.prop('value', errorMessage);\n });\n });\n return errorObject;\n }\n\n /**\n * Local function to return the HTML to display in the\n * question type details section of the form.\n * @param {string} title The type of the question being described.\n * @param {string} coderunner_descr The language string to introduce\n * the detail.\n * @param {html} html The HTML description of the question type, namely\n * the question text from its prototype.\n * @return {html} The composite HTML describing the question type.\n */\n function detailsHtml(title, coderunner_descr, html) {\n\n var resultHtml = '
';\n resultHtml += coderunner_descr;\n resultHtml += title + '
\\n' + html;\n return resultHtml;\n\n }\n\n /**\n * Raise an alert with the given language string and possible additional\n * extra text.\n * @param {string} key The language string to put in the Alert.\n * @param {string} extra Extra text to append.\n */\n function langStringAlert(key, extra) {\n if (window.hasOwnProperty('behattesting') && window.behattesting) {\n return;\n }\n str.get_string(key, 'qtype_coderunner').then(function(s) {\n var message = s.replace(/\\n/g, \" \");\n if (extra) {\n message += '\\n' + extra;\n }\n alert(message);\n });\n }\n\n /**\n * Get the \"preferred language\" from the AceLang string supplied.\n * @param {string} acelang The AceLang string.\n * For multilanguage questions, this is either the default (i.e.,\n * the language with a '*' suffix), or the first language. Otherwise\n * it is simply the entire AceLang string.\n * @return {string} The language to pass to Ace for syntax highlighting.\n */\n function preferredAceLang(acelang) {\n var langs, i;\n if (acelang.indexOf(',') < 0) {\n return acelang;\n } else {\n langs = acelang.split(',');\n for (i = 0; i < langs.length; i++) {\n if (langs[i].endsWith('*')) {\n return langs[i].substr(0, langs[i].length - 1);\n }\n }\n return langs.length > 0 ? langs[0] : '';\n }\n }\n\n /**\n * Load the various customisation fields into the form from the\n * CodeRunner question type currently selected by the combobox.\n * Looks at the preexisting type of the selected field.\n */\n function loadCustomisationFields() {\n let newType = typeCombo.children('option:selected').text();\n\n if (newType !== '' && newType !== 'Undefined') {\n // Prevent 'Undefined' ever being reselected.\n typeCombo.children('option:first-child').prop('disabled', 'disabled');\n\n // Load question type with ajax.\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n qtype: newType,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (outcome) {\n // Clean all warnings regardless.\n $('#id_qtype_coderunner_warning_div').empty();\n if (outcome.success) {\n copyFieldsFromQuestionType(newType, outcome);\n setUis();\n // Success, so remove the errors and change the current Qtype.\n currentQtype = newType;\n $('#id_qtype_coderunner_error_div').empty();\n }\n else {\n const errorObject = reportError(newType, outcome.error);\n // Checks to see if there has been a change in type from last saved.\n // If so, put up a load error and keep type unchanged.\n if (currentQtype !== newType && errorObject.error === 'duplicateprototype') {\n showLoadTypeError(currentQtype, errorObject, newType);\n $(\"#id_coderunnertype\").val(currentQtype);\n }\n }\n }\n ).fail(function () {\n // AJAX failed. We're dead, Fred. The attempt to get the\n // language translation for the error message will likely\n // fail too, so use English for a start.\n langStringAlert('error_loading_prototype');\n template.prop('value', '*** AJAX ERROR. DON\\'T SAVE THIS! ***');\n str.get_string('ajax_error', 'qtype_coderunner').then(function(s) {\n template.prop('value', s); // Translates into current language (if it works).\n });\n });\n }\n }\n\n /**\n * Build an HTML table describing all the UI parameters.\n * @param {object} uiParamInfo The object describing the parameters\n * for a particular UI.\n * @return {string} An HTML table describing each UI parameter.\n */\n function UiParameterDescriptionTable(uiParamInfo) {\n var html = '
\\n',\n hdrs = uiParamInfo.columnheaders, param, i;\n html += \"
\" + hdrs[0] + \"
\" + hdrs[1] + \"
\" + hdrs[2] + \"
\\n\";\n for (i = 0; i < uiParamInfo.uiparamstable.length; i++) {\n param = uiParamInfo.uiparamstable[i];\n html += \"
\" + param[0] + \"
\" + param[1] + \"
\" + param[2] + \"
\\n\";\n }\n html += \"
\\n\";\n return html;\n }\n\n /**\n * Load the UI parameter description field by Ajax when the UI plugin\n * is changed.\n */\n function loadUiParametersDescription() {\n let newUi = uiplugin.children('option:selected').text();\n $.getJSON(M.cfg.wwwroot + '/question/type/coderunner/ajax.php',\n {\n uiplugin: newUi,\n courseid: courseId,\n sesskey: M.cfg.sesskey\n },\n function (uiInfo) {\n var currentuiparameters = uiparameters.val(),\n paramDescriptionDiv = $('.ui_parameters_descr'),\n showhidebutton = $(''),\n table;\n paramDescriptionDiv.empty();\n paramDescriptionDiv.append(uiInfo.header);\n if (uiInfo.uiparamstable.length == 0 && currentuiparameters.trim() === '') {\n uiparameters.val(''); // Remove stray white space.\n $('#fgroup_id_uiparametergroup').hide();\n } else {\n if (uiInfo.uiparamstable.length != 0) {\n paramDescriptionDiv.append(showhidebutton);\n table = $(UiParameterDescriptionTable(uiInfo));\n paramDescriptionDiv.append(table);\n table.hide();\n showhidebutton.click(function () {\n if (showhidebutton.html() == uiInfo.showdetails) {\n table.show();\n showhidebutton.html(uiInfo.hidedetails);\n } else {\n table.hide();\n showhidebutton.html(uiInfo.showdetails);\n }\n });\n }\n $('#fgroup_id_uiparametergroup').show();\n if (useace.prop('checked')) {\n setUi('id_uiparameters', 'ace');\n }\n }\n }\n ).fail(function () {\n // AJAX failed.\n langStringAlert('error_loading_ui_descr');\n });\n }\n\n /**\n * Show/hide all testtype divs in the testcases according to the\n * 'Precheck' selector.\n */\n function set_testtype_visibilities() {\n if (precheck.val() === '3') { // Show only for case of 'Selected'.\n testtypedivs.show();\n } else {\n testtypedivs.hide();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the answer and\n * answer preload fields.\n */\n function check_ace_lang() {\n if (uiplugin.val() === 'ace'){\n setUis();\n }\n }\n\n /**\n * Check that the Ace language is correctly set for the template,\n * if template_uses_ace is checked.\n */\n function check_template_lang() {\n if (useace.prop('checked')) {\n setUi('id_template', 'ace');\n }\n }\n\n /**\n * If the brokenquestionmessage hidden element is not empty, insert the\n * given message as an error at the top of the question.\n * itself to go back to the last valid value.\n */\n function checkForBrokenQuestion() {\n let brokenQuestionMessage = brokenQuestion.prop('value'),\n messagePara = null;\n if (brokenQuestionMessage !== '') {\n messagePara = $('
' + brokenQuestion.prop('value') + '
');\n $('#id_qtype_coderunner_error_div').append(messagePara);\n }\n }\n\n /**\n * Shows the load type error of the selected type if the selected type is\n * faulty.\n * @param {string} currentType The current type with its errors.\n * @param {JSON Object} errorObject The JSON object containing a list of all the errors.\n * @param {string} newType The new type string which it failed to load.\n */\n function showLoadTypeError(currentType, errorObject, newType) {\n str.get_string('loadprototypeerror', 'qtype_coderunner',\n { oldtype : currentType, crtype : newType, outputstring : errorObject.extras })\n .then(function(str) {\n $('#id_qtype_coderunner_warning_div').append($('
' + str + '
'));\n });\n }\n\n /*************************************************************\n *\n * Body of initEditFormWhenReady starts here.\n *\n *************************************************************/\n\n if (prototypeType.prop('value') != 0) {\n // Display the prototype warning if it's a prototype and hide testboxes.\n testsection.css('display', 'none');\n prototypeDisplay.removeAttr('hidden');\n if (prototypeType.prop('value') == 1) {\n // Editing a built-in question type: Dangerous!\n str.get_string('proceed_at_own_risk', 'qtype_coderunner').then(function(s) {\n alert(s);\n });\n prototypeType.prop('disabled', true);\n customise.prop('disabled', true);\n }\n }\n\n checkForBrokenQuestion();\n badQuestionLoad.prop('hidden'); // Until we check it once.\n // Keep track of the current prototype loaded.\n currentQtype = typeCombo.children('option:selected').text();\n\n setCustomisationVisibility(isCustomised);\n if (!isCustomised) {\n // Not customised so have to load fields from prototype.\n loadCustomisationFields(); // setUis is called when this completes.\n } else {\n setUis(); // Set up UI controllers on answer and answerpreload.\n str.get_string('info_unavailable', 'qtype_coderunner').then(function(s) {\n questiontypeHelpDiv.html(\"
\" + s + \"
\");\n });\n }\n\n set_testtype_visibilities();\n\n if (useace.prop('checked')) {\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n }\n\n loadUiParametersDescription();\n\n // Set up event Handlers.\n\n customise.on('change', function() {\n let isCustomised = customise.prop('checked');\n if (isCustomised) {\n // Customisation is being turned on.\n setCustomisationVisibility(true);\n } else { // Customisation being turned off.\n str.get_string('confirm_proceed', 'qtype_coderunner').then(function(s) {\n if (window.confirm(s)) {\n setCustomisationVisibility(false);\n } else {\n customise.prop('checked', true);\n }\n });\n }\n });\n\n acelang.on('change', check_ace_lang);\n language.on('change', function() {\n check_template_lang();\n check_ace_lang();\n });\n\n typeCombo.on('change', function() {\n if (customise.prop('checked')) {\n // Author has customised the question. Ask if they want to reload inherited stuff.\n str.get_string('question_type_changed', 'qtype_coderunner').then(function (s) {\n if (window.confirm(s)) {\n loadCustomisationFields();\n }\n });\n } else {\n loadCustomisationFields();\n }\n });\n\n useace.on('change', function() {\n var isTurningOn = useace.prop('checked');\n if (isTurningOn) {\n setUi('id_template', 'ace');\n setUi('id_templateparams', 'ace');\n setUi('id_uiparameters', 'ace');\n } else {\n setUi('id_template', '');\n setUi('id_templateparams', '');\n setUi('id_uiparameters', '');\n }\n });\n\n evaluatePerStudent.on('change', function() {\n if (evaluatePerStudent.is(':checked')) {\n langStringAlert('templateparamsusingsandbox');\n }\n });\n\n uiplugin.on('change', function () {\n setUis();\n loadUiParametersDescription();\n });\n\n precheck.on('change', set_testtype_visibilities);\n\n // Displays and hides the reason for the question type to be disabled.\n // Also hides/shows the test cases section if prototype/not prototype.\n prototypeType.on('change', function () {\n if (prototypeType.prop('value') == '0') {\n testsection.css('display', 'block');\n prototypeDisplay.attr('hidden', '1');\n } else {\n testsection.css('display', 'none');\n prototypeDisplay.removeAttr('hidden');\n }\n });\n\n // In order to initialise the Ui plugin when the answer preload section is\n // expanded, we monitor attribute mutations in the Answer Preload\n // header.\n var observer = new MutationObserver( function () {\n setUis();\n });\n observer.observe(preloadHdr.get(0), {'attributes': true});\n\n // Setup click handler for the buttons that allow users to replace the\n // expected output with the output got from testing the answer program.\n $('button.replaceexpectedwithgot').click(function() {\n var gotPre = $(this).prev('pre[id^=\"id_got_\"]');\n var testCaseId = gotPre.attr('id').replace('id_got_', '');\n $('#id_expected_' + testCaseId).val(gotPre.text());\n $('#id_fail_expected_' + testCaseId).html(gotPre.text());\n $('.failrow_' + testCaseId).addClass('fixed'); // Fixed row.\n $(this).prop('disabled', true);\n });\n\n // On reloading the page, enable the typeCombo so that its value is POSTed.\n $('.btn-primary').click(function() {\n typeCombo.prop('disabled', false);\n });\n }\n\n return {initEditForm: initEditForm};\n});"],"names":["define","$","ui","str","currentQtype","JSON_TO_FORM_MAP","template","iscombinatortemplate","value","cputimelimitsecs","memlimitmb","sandbox","sandboxparams","testsplitterre","splitter","replace","allowmultiplestdins","grader","resultcolumns","language","acelang","uiplugin","initEditForm","messagePara","typeCombo","prototypeDisplay","evaluatePerStudent","globalextra","prototypeextra","useace","customise","isCombinator","testSplitterRe","allowMultipleStdins","customisationFieldSet","advancedCustomisation","isCustomised","prop","prototypeType","preloadHdr","courseId","questiontypeHelpDiv","precheck","testtypedivs","testsection","brokenQuestion","badQuestionLoad","uiparameters","setUi","taId","uiname","lang","uiWrapper","ta","document","getElementById","currentLang","attr","paramsJson","params","val","JSON","parse","err","toLowerCase","langs","i","indexOf","split","length","endsWith","substr","preferredAceLang","data","loadUi","InterfaceWrapper","setUis","answer","enableUi","enable_in_editor","error","alert","setCustomisationVisibility","isVisible","display","css","copyFieldsFromQuestionType","newType","response","formspecifier","attrval","isCombinatorEnabled","key","stateOn","taIds","restart","stop","enableAceInCustomisedFields","filter","get_string","then","s","title","coderunner_descr","html","resultHtml","questiontext","langStringAlert","extra","window","hasOwnProperty","behattesting","message","loadCustomisationFields","children","text","getJSON","M","cfg","wwwroot","qtype","courseid","sesskey","outcome","empty","success","errorObject","questionType","errorMessage","reportError","currentType","oldtype","crtype","outputstring","extras","append","showLoadTypeError","fail","loadUiParametersDescription","newUi","uiInfo","table","currentuiparameters","paramDescriptionDiv","showhidebutton","showdetails","header","uiparamstable","trim","hide","uiParamInfo","param","hdrs","columnheaders","UiParameterDescriptionTable","click","show","hidedetails","set_testtype_visibilities","check_ace_lang","removeAttr","on","confirm","is","MutationObserver","observe","get","gotPre","this","prev","testCaseId","addClass"],"mappings":";;;;;;;AAuBAA,qCAAO,CAAC,SAAU,wCAAyC,aAAa,SAASC,EAAGC,GAAIC,SAGhFC,aAAe,GASfC,iBAAmB,CACnBC,SAAqB,CAAC,eAAgB,QAAS,IAC/CC,qBAAqB,CAAC,2BAA4B,UAAW,GACrC,SAAUC,aACW,MAAVA,QAEnCC,iBAAqB,CAAC,uBAAwB,QAAS,IACvDC,WAAqB,CAAC,iBAAkB,QAAS,IACjDC,QAAqB,CAAC,cAAe,QAAS,WAC9CC,cAAqB,CAAC,oBAAqB,QAAS,IACpDC,eAAqB,CAAC,qBAAsB,QAAS,GAC7B,SAAUC,iBACCA,SAASC,QAAQ,KAAM,SAE1DC,oBAAqB,CAAC,0BAA2B,UAAW,GACpC,SAAUR,aACW,MAAVA,QAEnCS,OAAqB,CAAC,aAAc,QAAS,kBAC7CC,cAAqB,CAAC,oBAAqB,QAAS,IACpDC,SAAqB,CAAC,eAAgB,QAAS,IAC/CC,QAAqB,CAAC,cAAe,QAAS,IAC9CC,SAAqB,CAAC,eAAgB,QAAS,cAymB5C,CAACC,4BAvKIC,YAxbJC,UAAYvB,EAAE,sBACdwB,iBAAmBxB,EAAE,mBACrBK,SAAWL,EAAE,gBACbyB,mBAAqBzB,EAAE,gCACvB0B,YAAc1B,EAAE,mBAChB2B,eAAiB3B,EAAE,sBACnB4B,OAAS5B,EAAE,cACXkB,SAAWlB,EAAE,gBACbmB,QAAUnB,EAAE,eACZ6B,UAAY7B,EAAE,iBACd8B,aAAe9B,EAAE,4BACjB+B,eAAiB/B,EAAE,sBACnBgC,oBAAsBhC,EAAE,2BACxBiC,sBAAwBjC,EAAE,2BAC1BkC,sBAAwBlC,EAAE,mCAC1BmC,aAAeN,UAAUO,KAAK,WAC9BC,cAAgBrC,EAAE,qBAClBsC,WAAatC,EAAE,wBACfuC,SAAWvC,EAAE,0BAA0BoC,KAAK,SAC5CI,oBAAsBxC,EAAE,eACxByC,SAAWzC,EAAE,sBACb0C,aAAe1C,EAAE,gBACjB2C,YAAc3C,EAAE,mBAChB4C,eAAiB5C,EAAE,uBACnB6C,gBAAkB7C,EAAE,yBACpBoB,SAAWpB,EAAE,gBACb8C,aAAe9C,EAAE,6BAWZ+C,MAAMC,KAAMC,YAEbC,KAIAC,UALAC,GAAKpD,EAAEqD,SAASC,eAAeN,OAE/BO,YAAcH,GAAGI,KAAK,aACtBC,WAAaL,GAAGI,KAAK,eACrBE,OAAS,GAKbN,GAAGI,KAAK,sBAAuB7B,eAAegC,OAC9CP,GAAGI,KAAK,mBAAoB9B,YAAYiC,OACxCP,GAAGI,KAAK,aAAcxD,EAAE,kBAAkB2D,WAEtCD,OAASE,KAAKC,MAAMJ,YACtB,MAAMK,MAEO,UADfb,OAASA,OAAOc,iBAEZd,OAAS,IAGD,qBAARD,MAAuC,mBAARA,KAC/BE,KAAO,IAEPA,KAAOhC,SAASkB,KAAK,SACR,gBAATY,MAA0B7B,QAAQiB,KAAK,WACvCc,cA2Mc/B,aAClB6C,MAAOC,KACP9C,QAAQ+C,QAAQ,KAAO,SAChB/C,YAEP6C,MAAQ7C,QAAQgD,MAAM,KACjBF,EAAI,EAAGA,EAAID,MAAMI,OAAQH,OACtBD,MAAMC,GAAGI,SAAS,YACXL,MAAMC,GAAGK,OAAO,EAAGN,MAAMC,GAAGG,OAAS,UAG7CJ,MAAMI,OAAS,EAAIJ,MAAM,GAAK,GAtN1BO,CAAiBpD,QAAQiB,KAAK,aAI7Ce,UAAYC,GAAGoB,KAAK,wBAEHrB,UAAUF,SAAWA,QAAUM,aAAeL,OAI/DE,GAAGI,KAAK,YAAaN,MAEhBC,WAIDO,OAAOR,KAAOA,KACdC,UAAUsB,OAAOxB,OAAQS,SAJzBP,UAAY,IAAIlD,GAAGyE,iBAAiBzB,OAAQD,gBAoB3C2B,aACD1B,OAAS7B,SAASuC,MAClBiB,OAAS5E,EAAE,cACX6E,UAAW,KACA,SAAX5B,QAAoD,KAA/B2B,OAAOpB,KAAK,oBAGS,IADnBI,KAAKC,MAAMe,OAAOpB,KAAK,gBACzBsB,mBACbD,UAAW,GAEjB,MAAOE,OACLC,MAAM,0BAGVH,WACA9B,MAAM,YAAaE,QACnBF,MAAM,mBAAoBE,kBAQzBgC,2BAA2BC,eAC5BC,QAAUD,UAAY,QAAU,OACpCjD,sBAAsBmD,IAAI,UAAWD,SACrCjD,sBAAsBkD,IAAI,UAAWD,SACjCD,WAAatD,OAAOQ,KAAK,YACzBW,MAAM,cAAe,gBA+CpBsC,2BAA2BC,QAASC,cACrCC,cAAeC,QAZfC,wBAeC,IAAIC,gBAxCwBC,aAE7BzC,UADA0C,MAAQ,CAAC,cAAe,sBAExBjE,OAAOQ,KAAK,eACR,IAAI6B,EAAI,EAAGA,EAAI4B,MAAMzB,OAAQH,KAE7Bd,UADKnD,EAAEqD,SAASC,eAAeuC,MAAM5B,KACtBO,KAAK,wBACHoB,QACbzC,UAAU2C,UACH3C,YAAcyC,SACrBzC,UAAU4C,OA6BtBC,EAA4B,GACZ5F,iBACZoF,cAAgBpF,iBAAiBuF,KACjCF,QAAUF,SAASI,KAAOJ,SAASI,KAAOH,cAAc,GACpDA,cAAcpB,OAAS,IAEvBqB,SADAQ,EAAST,cAAc,IACNC,UAErBzF,EAAEwF,cAAc,IAAIpD,KAAKoD,cAAc,GAAIC,SAG/C5D,UAAUO,KAAK,WAAW,GAC1BlC,IAAIgG,WAAW,2BAA4B,oBAAoBC,MAAK,SAAUC,OA2C7DC,MAAOC,iBAAkBC,KAEtCC,WA5CAhE,oBAAoB+D,MA0CPF,MA1CwBf,QA0CjBgB,iBA1C0BF,EA0CRG,KA1CWhB,SAASkB,aA4C1DD,WAAa,2CACjBA,YAAcF,iBACdE,YAAcH,MAAQ,SAAWE,UA3CjCtB,4BAA2B,GA9BvBS,oBAAsB5D,aAAaM,KAAK,WAE5CL,eAAeK,KAAK,YAAasD,qBACjC1D,oBAAoBI,KAAK,YAAasD,8BAiFjCgB,gBAAgBf,IAAKgB,OACtBC,OAAOC,eAAe,iBAAmBD,OAAOE,cAGpD5G,IAAIgG,WAAWP,IAAK,oBAAoBQ,MAAK,SAASC,OAC9CW,QAAUX,EAAEtF,QAAQ,MAAO,KAC3B6F,QACAI,SAAW,KAAOJ,OAEtB3B,MAAM+B,qBAgCLC,8BACD1B,QAAU/D,UAAU0F,SAAS,mBAAmBC,OAEpC,KAAZ5B,SAA8B,cAAZA,UAElB/D,UAAU0F,SAAS,sBAAsB7E,KAAK,WAAY,YAG1DpC,EAAEmH,QAAQC,EAAEC,IAAIC,QAAU,qCACtB,CACIC,MAAOjC,QACPkC,SAAUjF,SACVkF,QAASL,EAAEC,IAAII,UAEnB,SAAUC,YAEN1H,EAAE,oCAAoC2H,QAClCD,QAAQE,QACRvC,2BAA2BC,QAASoC,SACpC/C,SAEAxE,aAAemF,QACftF,EAAE,kCAAkC2H,YAEnC,KACKE,qBAzGLC,aAAc/C,WACzB8C,YAAcjE,KAAKC,MAAMkB,cAC/B7E,IAAIgG,WAAW,kBAAmB,oBAAoBC,MAAK,SAASC,GAChElG,IAAIgG,WAAW2B,YAAY7C,MAAO,mBAAoB8C,cAAc3B,MAAK,SAASjG,KAC9EwG,gBAAgB,yBAA0BxG,SACtC6H,aAAe3B,EAAI,KACvB2B,cAAgB7H,IAAM,KACtB6H,cAAgB,aAAexF,SAAW,YAAcuF,aACxDzH,SAAS+B,KAAK,QAAS2F,oBAGxBF,YA8F6BG,CAAY1C,QAASoC,QAAQ3C,OAG7C5E,eAAiBmF,SAAiC,uBAAtBuC,YAAY9C,kBA4IrCkD,YAAaJ,YAAavC,SACjDpF,IAAIgG,WAAW,qBAAsB,mBACjC,CAAEgC,QAAUD,YAAaE,OAAS7C,QAAS8C,aAAeP,YAAYQ,SAC/DlC,MAAK,SAASjG,KACrBF,EAAE,oCAAoCsI,OAAOtI,EAAE,MAAQE,IAAM,YA/I7CqI,CAAkBpI,aAAc0H,YAAavC,SAC7CtF,EAAE,sBAAsB2D,IAAIxD,mBAI1CqI,MAAK,WAIH9B,gBAAgB,2BAChBrG,SAAS+B,KAAK,QAAS,wCACvBlC,IAAIgG,WAAW,aAAc,oBAAoBC,MAAK,SAASC,GAC3D/F,SAAS+B,KAAK,QAASgE,mBA4B9BqC,kCACDC,MAAQtH,SAAS6F,SAAS,mBAAmBC,OACjDlH,EAAEmH,QAAQC,EAAEC,IAAIC,QAAU,qCACtB,CACIlG,SAAUsH,MACVlB,SAAUjF,SACVkF,QAASL,EAAEC,IAAII,UAEnB,SAAUkB,YAIFC,MAHAC,oBAAsB/F,aAAaa,MACnCmF,oBAAsB9I,EAAE,wBACxB+I,eAAiB/I,EAAE,iDAAmD2I,OAAOK,YAAc,aAE/FF,oBAAoBnB,QACpBmB,oBAAoBR,OAAOK,OAAOM,QACC,GAA/BN,OAAOO,cAAc9E,QAA8C,KAA/ByE,oBAAoBM,QACxDrG,aAAaa,IAAI,IACjB3D,EAAE,+BAA+BoJ,SAEE,GAA/BT,OAAOO,cAAc9E,SACrB0E,oBAAoBR,OAAOS,gBAC3BH,MAAQ5I,WArCSqJ,iBAEKC,MAAOrF,EADzCsC,KAAO,8DACPgD,KAAOF,YAAYG,kBACvBjD,MAAQ,WAAagD,KAAK,GAAK,YAAcA,KAAK,GAAK,YAAcA,KAAK,GAAK,eAC1EtF,EAAI,EAAGA,EAAIoF,YAAYH,cAAc9E,OAAQH,IAE9CsC,MAAQ,YADR+C,MAAQD,YAAYH,cAAcjF,IACP,GAAK,YAAcqF,MAAM,GAAK,YAAcA,MAAM,GAAK,sBAEtF/C,KAAQ,mBA6BkBkD,CAA4Bd,SACtCG,oBAAoBR,OAAOM,OAC3BA,MAAMQ,OACNL,eAAeW,OAAM,WACbX,eAAexC,QAAUoC,OAAOK,aAChCJ,MAAMe,OACNZ,eAAexC,KAAKoC,OAAOiB,eAE3BhB,MAAMQ,OACNL,eAAexC,KAAKoC,OAAOK,kBAIvChJ,EAAE,+BAA+B2J,OAC7B/H,OAAOQ,KAAK,YACZW,MAAM,kBAAmB,WAIvCyF,MAAK,WAEH9B,gBAAgB,sCAQfmD,4BACkB,MAAnBpH,SAASkB,MACTjB,aAAaiH,OAEbjH,aAAa0G,gBAQZU,iBACkB,QAAnB1I,SAASuC,OACTgB,SAiD2B,GAA/BtC,cAAcD,KAAK,WAEnBO,YAAYyC,IAAI,UAAW,QAC3B5D,iBAAiBuI,WAAW,UACO,GAA/B1H,cAAcD,KAAK,WAEnBlC,IAAIgG,WAAW,sBAAuB,oBAAoBC,MAAK,SAASC,GACpEpB,MAAMoB,MAEV/D,cAAcD,KAAK,YAAY,GAC/BP,UAAUO,KAAK,YAAY,KAtC3Bd,YAAc,KACY,KAFFsB,eAAeR,KAAK,WAG5Cd,YAActB,EAAE,MAAQ4C,eAAeR,KAAK,SAAW,QACvDpC,EAAE,kCAAkCsI,OAAOhH,cAwCnDuB,gBAAgBT,KAAK,UAErBjC,aAAeoB,UAAU0F,SAAS,mBAAmBC,OAErDjC,2BAA2B9C,cACtBA,cAIDwC,SACAzE,IAAIgG,WAAW,mBAAoB,oBAAoBC,MAAK,SAASC,GACjE5D,oBAAoB+D,KAAK,MAAQH,EAAI,YAJzCY,0BAQJ6C,4BAEIjI,OAAOQ,KAAK,aACZW,MAAM,oBAAqB,OAC3BA,MAAM,kBAAmB,QAG7B0F,8BAIA5G,UAAUmI,GAAG,UAAU,WACAnI,UAAUO,KAAK,WAG9B6C,4BAA2B,GAE3B/E,IAAIgG,WAAW,kBAAmB,oBAAoBC,MAAK,SAASC,GAC5DQ,OAAOqD,QAAQ7D,GACfnB,4BAA2B,GAE3BpD,UAAUO,KAAK,WAAW,SAM1CjB,QAAQ6I,GAAG,SAAUF,gBACrB5I,SAAS8I,GAAG,UAAU,WAlGdpI,OAAOQ,KAAK,YACZW,MAAM,cAAe,OAmGzB+G,oBAGJvI,UAAUyI,GAAG,UAAU,WACfnI,UAAUO,KAAK,WAEflC,IAAIgG,WAAW,wBAAyB,oBAAoBC,MAAK,SAAUC,GACnEQ,OAAOqD,QAAQ7D,IACfY,6BAIRA,6BAIRpF,OAAOoI,GAAG,UAAU,WACEpI,OAAOQ,KAAK,YAE1BW,MAAM,cAAe,OACrBA,MAAM,oBAAqB,OAC3BA,MAAM,kBAAmB,SAEzBA,MAAM,cAAe,IACrBA,MAAM,oBAAqB,IAC3BA,MAAM,kBAAmB,QAIjCtB,mBAAmBuI,GAAG,UAAU,WACxBvI,mBAAmByI,GAAG,aACtBxD,gBAAgB,iCAIxBtF,SAAS4I,GAAG,UAAU,WAClBrF,SACA8D,iCAGJhG,SAASuH,GAAG,SAAUH,2BAItBxH,cAAc2H,GAAG,UAAU,WACY,KAA/B3H,cAAcD,KAAK,UACnBO,YAAYyC,IAAI,UAAW,SAC3B5D,iBAAiBgC,KAAK,SAAU,OAEhCb,YAAYyC,IAAI,UAAW,QAC3B5D,iBAAiBuI,WAAW,cAOrB,IAAII,kBAAkB,WACjCxF,YAEKyF,QAAQ9H,WAAW+H,IAAI,GAAI,aAAe,IAInDrK,EAAE,iCAAiC0J,OAAM,eACjCY,OAAStK,EAAEuK,MAAMC,KAAK,sBACtBC,WAAaH,OAAO9G,KAAK,MAAM1C,QAAQ,UAAW,IACtDd,EAAE,gBAAkByK,YAAY9G,IAAI2G,OAAOpD,QAC3ClH,EAAE,qBAAuByK,YAAYlE,KAAK+D,OAAOpD,QACjDlH,EAAE,YAAcyK,YAAYC,SAAS,SACrC1K,EAAEuK,MAAMnI,KAAK,YAAY,MAI7BpC,EAAE,gBAAgB0J,OAAM,WACpBnI,UAAUa,KAAK,YAAY"}
\ No newline at end of file
diff --git a/amd/build/ui_ace.min.js b/amd/build/ui_ace.min.js
index b2ca7b514..775917dc3 100644
--- a/amd/build/ui_ace.min.js
+++ b/amd/build/ui_ace.min.js
@@ -14,6 +14,6 @@
* @copyright Richard Lobb, 2015, 2017, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_ace",["jquery"],(function($){function AceWrapper(textareaId,w,h,params){var session,code,textarea=$(document.getElementById(textareaId)),wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=textarea[0]===document.activeElement,lang=params.lang,t=this;try{window.ace.require("ace/ext/language_tools"),this.modelist=window.ace.require("ace/ext/modelist"),this.textareaId=textareaId,this.textarea=textarea,this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),textarea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({enableBasicAutocompletion:!0,enableLiveAutocompletion:params.live_autocompletion,fontSize:params.font_size?params.font_size:"14px",newLineMode:"unix"}),this.editor.$blockScrolling=1/0,session=this.editor.getSession(),code=this.textarea.val(),(void 0===params.import_from_scratchpad||params.import_from_scratchpad)&&(code=this.extract_from_json_maybe(code)),session.setValue(code);var userTheme=window.localStorage.getItem("qtype_coderunner.ace.theme"),consider_prefers=params.auto_switch_light_dark&&window.matchMedia;null!==userTheme?this.editor.setTheme(userTheme):consider_prefers&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):consider_prefers&&window.matchMedia("(prefers-color-scheme: light)").matches?this.editor.setTheme("ace/theme/textmate"):params.theme?this.editor.setTheme("ace/theme/"+params.theme):this.editor.setTheme("ace/theme/textmate"),this.fixSlowLoad(),this.setLanguage(lang),this.setEventHandlers(textarea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.fail=!1}catch(err){this.fail=!0}}return AceWrapper.prototype.extract_from_json_maybe=function(code){try{code=JSON.parse(code).answer_code[0]}catch(err){}return code},AceWrapper.prototype.failed=function(){return this.fail},AceWrapper.prototype.failMessage=function(){return"ace_ui_notready"},AceWrapper.prototype.sync=function(){var thisThemeNow=this.editor.getOption("theme"),thisEditorKey="qtype_coderunner.".concat(this.textareaId,".theme"),lastTheme=window.sessionStorage.getItem(thisEditorKey),globalTheme=window.localStorage.getItem("qtype_coderunner.ace.theme");null!==lastTheme&&lastTheme!=thisThemeNow?window.localStorage.setItem("qtype_coderunner.ace.theme",thisThemeNow):globalTheme&&globalTheme!==thisThemeNow&&(this.editor.setOption("theme",globalTheme),function(name){throw new TypeError('"'+name+'" is read-only')}("thisThemeNow")),window.sessionStorage.setItem(thisEditorKey,thisThemeNow)},AceWrapper.prototype.syncIntervalSecs=function(){return 2},AceWrapper.prototype.setLanguage=function(language){var session=this.editor.getSession(),mode=this.findMode(language);mode&&session.setMode(mode.mode)},AceWrapper.prototype.getElement=function(){return this.editNode},AceWrapper.prototype.captureTab=function(){this.capturingTab=!0,this.editor.commands.bindKeys({Tab:"indent","Shift-Tab":"outdent"})},AceWrapper.prototype.releaseTab=function(){this.capturingTab=!1,this.editor.commands.bindKeys({Tab:null,"Shift-Tab":null})},AceWrapper.prototype.fixSlowLoad=function(){var observer=new IntersectionObserver((function(){$(document).trigger("mousemove")})),editNode=this.editNode.get(0);observer.observe(editNode)},AceWrapper.prototype.setEventHandlers=function(){var t=this;this.editor.getSession().on("change",(function(){t.textarea.val(t.editor.getSession().getValue()),t.contents_changed=!0})),this.editor.on("blur",(function(){t.contents_changed&&t.textarea.trigger("change")})),this.editor.on("mousedown",(function(){t.clickInProgress=!0})),this.editor.on("focus",(function(){t.clickInProgress?t.captureTab():t.releaseTab()})),this.editor.on("click",(function(){t.clickInProgress=!1})),this.editor.container.addEventListener("keydown",(function(e){void 0!==e.which&&0===e.which||(77===e.keyCode&&e.ctrlKey&&!e.altKey?(t.capturingTab?t.releaseTab():t.captureTab(),e.preventDefault()):27===e.keyCode?t.releaseTab():e.shiftKey||e.ctrlKey||e.altKey||9==e.keyCode||t.captureTab())}),!0)},AceWrapper.prototype.destroy=function(){var focused;this.fail||(focused=this.editor.isFocused(),this.textarea.val(this.editor.getSession().getValue()),this.editor.destroy(),$(this.editNode).remove(),focused&&(this.textarea.focus(),this.textarea[0].selectionStart=this.textarea[0].value.length))},AceWrapper.prototype.hasFocus=function(){return this.editor.isFocused()},AceWrapper.prototype.findMode=function(language){var candidate,filename,result,candidates,nameMap={octave:"matlab",nodejs:"javascript","c#":"cs"};if("string"==typeof language){language.toLowerCase()in nameMap&&(language=nameMap[language.toLowerCase()]),candidates=[language,language.replace(/\d+$/,"")];for(var i=0;i
"),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),textarea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({enableBasicAutocompletion:!0,enableLiveAutocompletion:params.live_autocompletion,fontSize:params.font_size?params.font_size:"14px",newLineMode:"unix"}),this.editor.$blockScrolling=1/0,session=this.editor.getSession(),code=this.textarea.val(),(void 0===params.import_from_scratchpad||params.import_from_scratchpad)&&(code=this.extract_from_json_maybe(code)),session.setValue(code);var userTheme=window.localStorage.getItem("qtype_coderunner.ace.theme"),consider_prefers=params.auto_switch_light_dark&&window.matchMedia;null!==userTheme?this.editor.setTheme(userTheme):consider_prefers&&window.matchMedia("(prefers-color-scheme: dark)").matches?this.editor.setTheme("ace/theme/tomorrow_night"):consider_prefers&&window.matchMedia("(prefers-color-scheme: light)").matches?this.editor.setTheme("ace/theme/textmate"):params.theme?this.editor.setTheme("ace/theme/"+params.theme):this.editor.setTheme("ace/theme/textmate"),this.currentTheme=this.editor.getTheme(),this.fixSlowLoad(),this.setLanguage(lang),this.setEventHandlers(textarea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.fail=!1}catch(err){this.fail=!0}}return AceWrapper.prototype.extract_from_json_maybe=function(code){try{code=JSON.parse(code).answer_code[0]}catch(err){}return code},AceWrapper.prototype.failed=function(){return this.fail},AceWrapper.prototype.failMessage=function(){return"ace_ui_notready"},AceWrapper.prototype.sync=function(){var thisThemeNow=this.editor.getTheme(),globalTheme=window.localStorage.getItem("qtype_coderunner.ace.theme");thisThemeNow!==this.currentTheme?(this.currentTheme=thisThemeNow,window.localStorage.setItem("qtype_coderunner.ace.theme",thisThemeNow)):globalTheme&&thisThemeNow!=globalTheme&&(this.editor.setTheme(globalTheme),this.currentTheme=globalTheme)},AceWrapper.prototype.syncIntervalSecs=function(){return 2},AceWrapper.prototype.setLanguage=function(language){var session=this.editor.getSession(),mode=this.findMode(language);mode&&session.setMode(mode.mode)},AceWrapper.prototype.getElement=function(){return this.editNode},AceWrapper.prototype.captureTab=function(){this.capturingTab=!0,this.editor.commands.bindKeys({Tab:"indent","Shift-Tab":"outdent"})},AceWrapper.prototype.releaseTab=function(){this.capturingTab=!1,this.editor.commands.bindKeys({Tab:null,"Shift-Tab":null})},AceWrapper.prototype.fixSlowLoad=function(){var observer=new IntersectionObserver((function(){$(document).trigger("mousemove")})),editNode=this.editNode.get(0);observer.observe(editNode)},AceWrapper.prototype.setEventHandlers=function(){var t=this;this.editor.getSession().on("change",(function(){t.textarea.val(t.editor.getSession().getValue()),t.contents_changed=!0})),this.editor.on("blur",(function(){t.contents_changed&&t.textarea.trigger("change")})),this.editor.on("mousedown",(function(){t.clickInProgress=!0})),this.editor.on("focus",(function(){t.clickInProgress?t.captureTab():t.releaseTab()})),this.editor.on("click",(function(){t.clickInProgress=!1})),this.editor.container.addEventListener("keydown",(function(e){void 0!==e.which&&0===e.which||(77===e.keyCode&&e.ctrlKey&&!e.altKey?(t.capturingTab?t.releaseTab():t.captureTab(),e.preventDefault()):27===e.keyCode?t.releaseTab():e.shiftKey||e.ctrlKey||e.altKey||9==e.keyCode||t.captureTab())}),!0)},AceWrapper.prototype.destroy=function(){var focused;this.fail||(focused=this.editor.isFocused(),this.textarea.val(this.editor.getSession().getValue()),this.editor.destroy(),$(this.editNode).remove(),focused&&(this.textarea.focus(),this.textarea[0].selectionStart=this.textarea[0].value.length))},AceWrapper.prototype.hasFocus=function(){return this.editor.isFocused()},AceWrapper.prototype.findMode=function(language){var candidate,filename,result,candidates,nameMap={octave:"matlab",nodejs:"javascript","c#":"cs"};if("string"==typeof language){language.toLowerCase()in nameMap&&(language=nameMap[language.toLowerCase()]),candidates=[language,language.replace(/\d+$/,"")];for(var i=0;i.\n\n/**\n * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n code,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n this.textareaId = textareaId;\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n enableLiveAutocompletion: params.live_autocompletion,\n fontSize: params.font_size ? params.font_size : \"14px\",\n newLineMode: \"unix\",\n });\n\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n code = this.textarea.val();\n if (params.import_from_scratchpad === undefined || params.import_from_scratchpad) {\n code = this.extract_from_json_maybe(code);\n }\n session.setValue(code);\n\n // If there's a user-defined theme in local storage, use that.\n // Otherwise use the 'prefers-color-scheme' option if given or\n // the question/system defaults if not.\n const userTheme = window.localStorage.getItem('qtype_coderunner.ace.theme');\n const consider_prefers = params.auto_switch_light_dark && window.matchMedia;\n if (userTheme !== null) {\n this.editor.setTheme(userTheme);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: light)').matches) {\n this.editor.setTheme(ACE_LIGHT_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.fixSlowLoad();\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n AceWrapper.prototype.extract_from_json_maybe = function(code) {\n // If the given code looks like JSON from the Scratchpad UI,\n // extract and return the answer_code attribute.\n try {\n const jsonObj = JSON.parse(code);\n code = jsonObj.answer_code[0];\n } catch(err) {}\n\n return code;\n };\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // The data is always sync'd to the text area. But here we use sync to\n // poll the value of the current theme and record in browser local\n // storage if the value for this particular Ace instance has changed\n // (as recorded in session storage). The theme to use for all Ace windows\n // from now on in this browser is recorded in browser local storage.\n const thisThemeNow = this.editor.getOption('theme');\n const thisEditorKey = `qtype_coderunner.${this.textareaId}.theme`;\n const lastTheme = window.sessionStorage.getItem(thisEditorKey);\n const globalKey = 'qtype_coderunner.ace.theme';\n const globalTheme = window.localStorage.getItem(globalKey);\n\n if (lastTheme !== null && lastTheme != thisThemeNow) {\n // The theme in this Ace window has been changed by the user.\n window.localStorage.setItem(globalKey, thisThemeNow);\n } else if (globalTheme && globalTheme !== thisThemeNow) {\n // A theme has been defined by another Ace window since we loaded.\n // Switch to that theme.\n this.editor.setOption('theme', globalTheme);\n thisThemeNow = globalTheme;\n }\n window.sessionStorage.setItem(thisEditorKey, thisThemeNow);\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 2;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n // Sometimes Ace editors do not load until the mouse is moved. To fix this,\n // 'move' the mouse using JQuery when the editor div enters the viewport.\n AceWrapper.prototype.fixSlowLoad = function () {\n const observer = new IntersectionObserver( () => {\n $(document).trigger('mousemove');\n });\n const editNode = this.editNode.get(0); // Non-JQuerry node.\n observer.observe(editNode);\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","session","code","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","enableLiveAutocompletion","live_autocompletion","fontSize","font_size","newLineMode","$blockScrolling","Infinity","getSession","val","undefined","import_from_scratchpad","extract_from_json_maybe","setValue","userTheme","localStorage","getItem","consider_prefers","auto_switch_light_dark","matchMedia","setTheme","matches","theme","fixSlowLoad","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","JSON","parse","answer_code","failed","failMessage","sync","thisThemeNow","getOption","thisEditorKey","lastTheme","sessionStorage","globalTheme","setItem","setOption","syncIntervalSecs","language","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","observer","IntersectionObserver","trigger","observe","getValue","container","addEventListener","e","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_ace.min.js","sources":["../src/ui_ace.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 * JavaScript to interface to the Ace editor, which is used both in\n * the author editing page and by the student question submission page.\n * The class defined in this module is a plugin for the InterfaceWrapper class\n * declared in userinterfacewrapper.js. See that file for an explanation of\n * the interface to this module.\n *\n * A special case behaviour of the AceWrapper is that it needs to know\n * the Programming language that is being edited. This MUST be provided in\n * the constructor params parameter (an associative array) as a string\n * with key 'lang'.\n *\n * @module qtype_coderunner/ui_ace\n * @copyright Richard Lobb, 2015, 2017, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// Thanks to Ulrich Dangel for the initial implementation of Ace within\n// CodeRunner.\n\n// WARNING: The ace editor must have already been loaded before this\n// module is used, as it assumes window.ace exists.\n\ndefine(['jquery'], function($) {\n const GLOBAL_THEME_KEY = 'qtype_coderunner.ace.theme';\n const ACE_DARK_THEME = 'ace/theme/tomorrow_night';\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n /**\n * Constructor for the Ace interface object.\n * @param {string} textareaId The ID of the HTML textarea element to be wrapped.\n * @param {int} w The width in pixels of the textarea.\n * @param {int} h The height in pixels of the textarea.\n * @param {object} params The UI parameter object.\n */\n function AceWrapper(textareaId, w, h, params) {\n var textarea = $(document.getElementById(textareaId)),\n wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = textarea[0] === document.activeElement,\n lang = params.lang,\n session,\n code,\n t = this; // For embedded callbacks.\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n this.modelist = window.ace.require('ace/ext/modelist');\n this.textareaId = textareaId;\n this.textarea = textarea;\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (textarea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n enableBasicAutocompletion: true,\n enableLiveAutocompletion: params.live_autocompletion,\n fontSize: params.font_size ? params.font_size : \"14px\",\n newLineMode: \"unix\",\n });\n\n this.editor.$blockScrolling = Infinity;\n\n session = this.editor.getSession();\n code = this.textarea.val();\n if (params.import_from_scratchpad === undefined || params.import_from_scratchpad) {\n code = this.extract_from_json_maybe(code);\n }\n session.setValue(code);\n\n // If there's a user-defined theme in local storage, use that.\n // Otherwise use the 'prefers-color-scheme' option if given or\n // the question/system defaults if not.\n const userTheme = window.localStorage.getItem(GLOBAL_THEME_KEY);\n const consider_prefers = params.auto_switch_light_dark && window.matchMedia;\n if (userTheme !== null) {\n this.editor.setTheme(userTheme);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n this.editor.setTheme(ACE_DARK_THEME);\n } else if (consider_prefers && window.matchMedia('(prefers-color-scheme: light)').matches) {\n this.editor.setTheme(ACE_LIGHT_THEME);\n } else if (params.theme) {\n this.editor.setTheme(\"ace/theme/\" + params.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n this.currentTheme = this.editor.getTheme();\n\n this.fixSlowLoad();\n\n this.setLanguage(lang);\n\n this.setEventHandlers(textarea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.fail = false;\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n AceWrapper.prototype.extract_from_json_maybe = function(code) {\n // If the given code looks like JSON from the Scratchpad UI,\n // extract and return the answer_code attribute.\n try {\n const jsonObj = JSON.parse(code);\n code = jsonObj.answer_code[0];\n } catch(err) {}\n\n return code;\n };\n\n AceWrapper.prototype.failed = function() {\n return this.fail;\n };\n\n AceWrapper.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n // Sync to TextArea\n AceWrapper.prototype.sync = function() {\n // The data is always sync'd to the text area. But here we use sync to\n // poll the value of the current theme and record in browser local\n // storage if the value for this particular Ace instance has changed\n // from the current working theme (set by code),\n // implying a user menu action. If that happens the global user theme\n // is set and is subsequently used by all Ace windows.\n const thisThemeNow = this.editor.getTheme();\n const globalTheme = window.localStorage.getItem(GLOBAL_THEME_KEY);\n if (thisThemeNow !== this.currentTheme) {\n // User has changed the theme via menu. Record in global storage so\n // other editor instances can switch to it.\n this.currentTheme = thisThemeNow;\n window.localStorage.setItem(GLOBAL_THEME_KEY, thisThemeNow);\n // console.log(`Menu theme change. Global theme now ${thisThemeNow}`);\n } else if (globalTheme && thisThemeNow != globalTheme) {\n // Another window has set the theme (since if there had been a\n // global theme when we started, we'd have used it.\n this.editor.setTheme(globalTheme);\n this.currentTheme = globalTheme;\n // console.log(`Global theme change found: ${globalTheme}`);\n }\n };\n\n AceWrapper.prototype.syncIntervalSecs = function() {\n return 2;\n };\n\n AceWrapper.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceWrapper.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceWrapper.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceWrapper.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n // Sometimes Ace editors do not load until the mouse is moved. To fix this,\n // 'move' the mouse using JQuery when the editor div enters the viewport.\n AceWrapper.prototype.fixSlowLoad = function () {\n const observer = new IntersectionObserver( () => {\n $(document).trigger('mousemove');\n });\n const editNode = this.editNode.get(0); // Non-JQuerry node.\n observer.observe(editNode);\n };\n\n AceWrapper.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.textarea.val(t.editor.getSession().getValue());\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textarea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceWrapper.prototype.destroy = function () {\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.textarea.val(this.editor.getSession().getValue()); // Copy data back\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textarea.focus();\n this.textarea[0].selectionStart = this.textarea[0].value.length;\n }\n }\n };\n\n AceWrapper.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceWrapper.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceWrapper.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n return {\n Constructor: AceWrapper\n };\n});\n"],"names":["define","$","AceWrapper","textareaId","w","h","params","session","code","textarea","document","getElementById","wrapper","focused","activeElement","lang","t","this","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","enableBasicAutocompletion","enableLiveAutocompletion","live_autocompletion","fontSize","font_size","newLineMode","$blockScrolling","Infinity","getSession","val","undefined","import_from_scratchpad","extract_from_json_maybe","setValue","userTheme","localStorage","getItem","consider_prefers","auto_switch_light_dark","matchMedia","setTheme","matches","theme","currentTheme","getTheme","fixSlowLoad","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","attr","aceTextarea","fail","err","prototype","JSON","parse","answer_code","failed","failMessage","sync","thisThemeNow","globalTheme","setItem","syncIntervalSecs","language","mode","findMode","setMode","getElement","commands","bindKeys","releaseTab","observer","IntersectionObserver","trigger","observe","getValue","container","addEventListener","e","which","keyCode","ctrlKey","altKey","preventDefault","shiftKey","destroy","isFocused","remove","selectionStart","value","length","hasFocus","candidate","filename","result","candidates","nameMap","toLowerCase","replace","i","modesByName","getModeForPath","name","outerHeight","outerWidth","Constructor"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/ui_ace.js b/amd/src/ui_ace.js
index a83a2d55f..29535be37 100644
--- a/amd/src/ui_ace.js
+++ b/amd/src/ui_ace.js
@@ -37,6 +37,9 @@
// module is used, as it assumes window.ace exists.
define(['jquery'], function($) {
+ const GLOBAL_THEME_KEY = 'qtype_coderunner.ace.theme';
+ const ACE_DARK_THEME = 'ace/theme/tomorrow_night';
+ const ACE_LIGHT_THEME = 'ace/theme/textmate';
/**
* Constructor for the Ace interface object.
* @param {string} textareaId The ID of the HTML textarea element to be wrapped.
@@ -45,9 +48,6 @@ define(['jquery'], function($) {
* @param {object} params The UI parameter object.
*/
function AceWrapper(textareaId, w, h, params) {
- const ACE_DARK_THEME = 'ace/theme/tomorrow_night';
- const ACE_LIGHT_THEME = 'ace/theme/textmate';
-
var textarea = $(document.getElementById(textareaId)),
wrapper = $(document.getElementById(textareaId + '_wrapper')),
focused = textarea[0] === document.activeElement,
@@ -97,7 +97,7 @@ define(['jquery'], function($) {
// If there's a user-defined theme in local storage, use that.
// Otherwise use the 'prefers-color-scheme' option if given or
// the question/system defaults if not.
- const userTheme = window.localStorage.getItem('qtype_coderunner.ace.theme');
+ const userTheme = window.localStorage.getItem(GLOBAL_THEME_KEY);
const consider_prefers = params.auto_switch_light_dark && window.matchMedia;
if (userTheme !== null) {
this.editor.setTheme(userTheme);
@@ -110,6 +110,7 @@ define(['jquery'], function($) {
} else {
this.editor.setTheme(ACE_LIGHT_THEME);
}
+ this.currentTheme = this.editor.getTheme();
this.fixSlowLoad();
@@ -172,24 +173,24 @@ define(['jquery'], function($) {
// The data is always sync'd to the text area. But here we use sync to
// poll the value of the current theme and record in browser local
// storage if the value for this particular Ace instance has changed
- // (as recorded in session storage). The theme to use for all Ace windows
- // from now on in this browser is recorded in browser local storage.
- const thisThemeNow = this.editor.getOption('theme');
- const thisEditorKey = `qtype_coderunner.${this.textareaId}.theme`;
- const lastTheme = window.sessionStorage.getItem(thisEditorKey);
- const globalKey = 'qtype_coderunner.ace.theme';
- const globalTheme = window.localStorage.getItem(globalKey);
-
- if (lastTheme !== null && lastTheme != thisThemeNow) {
- // The theme in this Ace window has been changed by the user.
- window.localStorage.setItem(globalKey, thisThemeNow);
- } else if (globalTheme && globalTheme !== thisThemeNow) {
- // A theme has been defined by another Ace window since we loaded.
- // Switch to that theme.
- this.editor.setOption('theme', globalTheme);
- thisThemeNow = globalTheme;
+ // from the current working theme (set by code),
+ // implying a user menu action. If that happens the global user theme
+ // is set and is subsequently used by all Ace windows.
+ const thisThemeNow = this.editor.getTheme();
+ const globalTheme = window.localStorage.getItem(GLOBAL_THEME_KEY);
+ if (thisThemeNow !== this.currentTheme) {
+ // User has changed the theme via menu. Record in global storage so
+ // other editor instances can switch to it.
+ this.currentTheme = thisThemeNow;
+ window.localStorage.setItem(GLOBAL_THEME_KEY, thisThemeNow);
+ // console.log(`Menu theme change. Global theme now ${thisThemeNow}`);
+ } else if (globalTheme && thisThemeNow != globalTheme) {
+ // Another window has set the theme (since if there had been a
+ // global theme when we started, we'd have used it.
+ this.editor.setTheme(globalTheme);
+ this.currentTheme = globalTheme;
+ // console.log(`Global theme change found: ${globalTheme}`);
}
- window.sessionStorage.setItem(thisEditorKey, thisThemeNow);
};
AceWrapper.prototype.syncIntervalSecs = function() {
From 4c195de5e259b7604af280f8f0bedff3b1f158a5 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Thu, 16 Mar 2023 17:38:00 +1300
Subject: [PATCH 071/188] Bug fix: race problem with ui_scratchpad could result
in all scratchpad contents in a page being cleared if user clicked Check
straight after Precheck and before page rendering was complete.
---
amd/build/ui_scratchpad.min.js | 2 +-
amd/build/ui_scratchpad.min.js.map | 2 +-
amd/src/ui_scratchpad.js | 28 ++++++++++++++--------------
3 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/amd/build/ui_scratchpad.min.js b/amd/build/ui_scratchpad.min.js
index 6a3e92a04..b75962a9b 100644
--- a/amd/build/ui_scratchpad.min.js
+++ b/amd/build/ui_scratchpad.min.js
@@ -1,3 +1,3 @@
-define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_templates,_userinterfacewrapper,_outputdisplayarea){var obj;function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}function _defineProperties(target,props){for(var i=0;iarr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0}))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}},{key:"getElement",value:function(){return this.outerDiv}},{key:"handleRunButtonClick",value:function(){var _this=this;if(null!==this.outputDisplay){this.sync();var preloadString=this.textArea.value,serial=this.readJson(preloadString),escape=function(code){return _this.uiParams.escape?JSON.stringify(code).slice(1,-1):code},code=function(answerCode,testCode,prefixAns,template){var open=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"\\(",close=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"\\)";template||(template="".concat(open," ANSWER_CODE ").concat(close,"\n")+"".concat(open," SCRATCHPAD_CODE ").concat(close)),prefixAns||(answerCode="");var escOpen=escapeRegExp(open),escClose=escapeRegExp(close),answerRegex=new RegExp("".concat(escOpen,"\\s*ANSWER_CODE\\s*").concat(escClose),"g"),scratchpadRegex=new RegExp("".concat(escOpen,"\\s*SCRATCHPAD_CODE\\s*").concat(escClose),"g");return(template=template.replaceAll(answerRegex,answerCode)).replaceAll(scratchpadRegex,testCode)}(escape(serial.answer_code[0]),escape(serial.test_code[0]),serial.prefix_ans[0],this.runWrapper,this.uiParams.open_delimiter,this.uiParams.close_delimiter);this.outputDisplay.runCode(code,"",!0)}}},{key:"updateContext",value:function(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}},{key:"readJson",value:function(preloadString){var serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch(_unused){serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}},{key:"reload",value:(fn=regeneratorRuntime.mark((function _callee(){var preloadString,preload,_yield$Templates$rend,html;return regeneratorRuntime.wrap((function(_context){for(;;)switch(_context.prev=_context.next){case 0:preloadString=this.textArea.value,_context.prev=1,preload=this.readJson(preloadString),_context.next=10;break;case 5:return _context.prev=5,_context.t0=_context.catch(1),this.fail=!0,this.failString="scratchpad_ui_invalidserialisation",_context.abrupt("return");case 10:return this.updateContext(preload),_context.prev=11,_context.next=14,_templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);case 14:_yield$Templates$rend=_context.sent,html=_yield$Templates$rend.html,this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners(),_context.next=26;break;case 22:_context.prev=22,_context.t1=_context.catch(11),this.fail=!0,this.failString="scratchpad_ui_templateloadfail";case 26:case"end":return _context.stop()}}),_callee,this,[[1,5],[11,22]])})),_reload=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(){return _reload.apply(this,arguments)})},{key:"drawUi",value:function(html){var wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}},{key:"addAceUis",value:function(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}},{key:"addEventListeners",value:function(){var _this2=this,runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(function(){return _this2.handleRunButtonClick()}))}},{key:"resize",value:function(){}},{key:"hasFocus",value:function(){var _this$answerCodeUi,_this$testCodeUi,focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}},{key:"destroy",value:function(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}],protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),ScratchpadUi}();_exports.Constructor=ScratchpadUi}));
+define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_templates,_userinterfacewrapper,_outputdisplayarea){var obj;function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}function _defineProperties(target,props){for(var i=0;iarr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0}))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}},{key:"getElement",value:function(){return this.outerDiv}},{key:"handleRunButtonClick",value:function(){var _this=this;if(null!==this.outputDisplay){this.sync();var preloadString=this.textArea.value,serial=this.readJson(preloadString),escape=function(code){return _this.uiParams.escape?JSON.stringify(code).slice(1,-1):code},code=function(answerCode,testCode,prefixAns,template){var open=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"\\(",close=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"\\)";template||(template="".concat(open," ANSWER_CODE ").concat(close,"\n")+"".concat(open," SCRATCHPAD_CODE ").concat(close)),prefixAns||(answerCode="");var escOpen=escapeRegExp(open),escClose=escapeRegExp(close),answerRegex=new RegExp("".concat(escOpen,"\\s*ANSWER_CODE\\s*").concat(escClose),"g"),scratchpadRegex=new RegExp("".concat(escOpen,"\\s*SCRATCHPAD_CODE\\s*").concat(escClose),"g");return(template=template.replaceAll(answerRegex,answerCode)).replaceAll(scratchpadRegex,testCode)}(escape(serial.answer_code[0]),escape(serial.test_code[0]),serial.prefix_ans[0],this.runWrapper,this.uiParams.open_delimiter,this.uiParams.close_delimiter);this.outputDisplay.runCode(code,"",!0)}}},{key:"updateContext",value:function(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}},{key:"readJson",value:function(preloadString){var serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch(_unused){serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}},{key:"reload",value:(fn=regeneratorRuntime.mark((function _callee(){var _yield$Templates$rend,html;return regeneratorRuntime.wrap((function(_context){for(;;)switch(_context.prev=_context.next){case 0:return _context.prev=0,_context.next=3,_templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);case 3:_yield$Templates$rend=_context.sent,html=_yield$Templates$rend.html,this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners(),_context.next=15;break;case 11:_context.prev=11,_context.t0=_context.catch(0),this.fail=!0,this.failString="scratchpad_ui_templateloadfail";case 15:case"end":return _context.stop()}}),_callee,this,[[0,11]])})),_reload=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(){return _reload.apply(this,arguments)})},{key:"drawUi",value:function(html){var wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}},{key:"addAceUis",value:function(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}},{key:"addEventListeners",value:function(){var _this2=this,runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(function(){return _this2.handleRunButtonClick()}))}},{key:"resize",value:function(){}},{key:"hasFocus",value:function(){var _this$answerCodeUi,_this$testCodeUi,focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}},{key:"destroy",value:function(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}],protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),ScratchpadUi}();_exports.Constructor=ScratchpadUi}));
//# sourceMappingURL=ui_scratchpad.min.js.map
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js.map b/amd/build/ui_scratchpad.min.js.map
index e31f0915c..9498a8dc4 100644
--- a/amd/build/ui_scratchpad.min.js.map
+++ b/amd/build/ui_scratchpad.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text from first editor.\n * @param {string} testCode text from second editor.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @param {string} open delimiter to look for, e.g. '[['\n * @param {string} close delimiter to look for, e.g. ']]'\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template, open = '\\\\(', close = '\\\\)') => {\n if (!template) {\n template = `${open} ANSWER_CODE ${close}\\n` +\n `${open} SCRATCHPAD_CODE ${close}`;\n }\n if (!prefixAns) {\n answerCode = '';\n }\n const escOpen = escapeRegExp(open);\n const escClose = escapeRegExp(close);\n const answerRegex = new RegExp(`${escOpen}\\\\s*ANSWER_CODE\\\\s*${escClose}`, 'g');\n const scratchpadRegex = new RegExp(`${escOpen}\\\\s*SCRATCHPAD_CODE\\\\s*${escClose}`, 'g');\n template = template.replaceAll(answerRegex, answerCode);\n template = template.replaceAll(scratchpadRegex, testCode);\n return template;\n};\n\n/**\n * Escapes a string for use in regex.\n * @param {string} string to escape.\n * @returns {string} RegEx escaped string\n */\nconst escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"); // $& means the whole matched string\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the default value.\n * Does not add keys/values to the result if that key is not in defaults.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!(el.classList.contains('collapse') || el.classList.contains('collapsing'))) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null,\n open_delimiter: '{|',\n close_delimiter: '|}',\n escape: false\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n let serialisation = {\n answer_code: [''],\n test_code: [''],\n show_hide: [''],\n prefix_ans: ['']\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const escape = (code) => this.uiParams.escape ? JSON.stringify(code).slice(1, -1) : code;\n const answerCode = escape(serial.answer_code[0]);\n const testCode = escape(serial.test_code[0]);\n const code = fillWrapper(\n answerCode,\n testCode,\n serial.prefix_ans[0],\n this.runWrapper,\n this.uiParams.open_delimiter,\n this.uiParams.close_delimiter\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","escapeRegExp","string","replace","overwriteValues","defaults","prescribed","overwritten","Object","entries","key","value","ScratchpadUi","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","open_delimiter","close_delimiter","escape","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","reload","wrapperSrc","dataset","failString","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","test_code","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","checked","values","some","val","length","JSON","stringify","sync","preloadString","serial","readJson","code","_this","slice","answerCode","testCode","template","open","close","escOpen","escClose","answerRegex","RegExp","scratchpadRegex","replaceAll","fillWrapper","runCode","preload","text","render","CSS","parse","hasOwnProperty","TypeError","updateContext","Templates","renderForPromise","html","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","_this2","handleRunButtonClick","focused","_this$answerCodeUi","uiInstance","hasFocus","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text from first editor.\n * @param {string} testCode text from second editor.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @param {string} open delimiter to look for, e.g. '[['\n * @param {string} close delimiter to look for, e.g. ']]'\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template, open = '\\\\(', close = '\\\\)') => {\n if (!template) {\n template = `${open} ANSWER_CODE ${close}\\n` +\n `${open} SCRATCHPAD_CODE ${close}`;\n }\n if (!prefixAns) {\n answerCode = '';\n }\n const escOpen = escapeRegExp(open);\n const escClose = escapeRegExp(close);\n const answerRegex = new RegExp(`${escOpen}\\\\s*ANSWER_CODE\\\\s*${escClose}`, 'g');\n const scratchpadRegex = new RegExp(`${escOpen}\\\\s*SCRATCHPAD_CODE\\\\s*${escClose}`, 'g');\n template = template.replaceAll(answerRegex, answerCode);\n template = template.replaceAll(scratchpadRegex, testCode);\n return template;\n};\n\n/**\n * Escapes a string for use in regex.\n * @param {string} string to escape.\n * @returns {string} RegEx escaped string\n */\nconst escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"); // $& means the whole matched string\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the default value.\n * Does not add keys/values to the result if that key is not in defaults.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!(el.classList.contains('collapse') || el.classList.contains('collapsing'))) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null,\n open_delimiter: '{|',\n close_delimiter: '|}',\n escape: false\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n let serialisation = {\n answer_code: [this.context.answer_code.text],\n test_code: [this.context.test_code.text],\n show_hide: [this.context.show_hide.show],\n prefix_ans: [this.context.prefix_ans.checked]\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const escape = (code) => this.uiParams.escape ? JSON.stringify(code).slice(1, -1) : code;\n const answerCode = escape(serial.answer_code[0]);\n const testCode = escape(serial.test_code[0]);\n const code = fillWrapper(\n answerCode,\n testCode,\n serial.prefix_ans[0],\n this.runWrapper,\n this.uiParams.open_delimiter,\n this.uiParams.close_delimiter\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","escapeRegExp","string","replace","overwriteValues","defaults","prescribed","overwritten","Object","entries","key","value","ScratchpadUi","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","open_delimiter","close_delimiter","escape","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","preload","preloadString","readJson","error","failString","updateContext","reload","wrapperSrc","dataset","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","text","test_code","show","checked","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","values","some","val","length","JSON","stringify","sync","serial","code","_this","slice","answerCode","testCode","template","open","close","escOpen","escClose","answerRegex","RegExp","scratchpadRegex","replaceAll","fillWrapper","runCode","render","CSS","parse","hasOwnProperty","TypeError","Templates","renderForPromise","html","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","_this2","handleRunButtonClick","focused","_this$answerCodeUi","uiInstance","hasFocus","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/ui_scratchpad.js b/amd/src/ui_scratchpad.js
index 9abb59582..800a30fc7 100644
--- a/amd/src/ui_scratchpad.js
+++ b/amd/src/ui_scratchpad.js
@@ -184,6 +184,16 @@ class ScratchpadUi {
this.numRows = this.textArea.rows;
this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);
this.runWrapper = this.getRunWrapper();
+ const preloadString = this.textArea.value;
+ let preload;
+ try {
+ preload = this.readJson(preloadString);
+ } catch (error) {
+ this.fail = true;
+ this.failString = 'scratchpad_ui_invalidserialisation';
+ return;
+ }
+ this.updateContext(preload);
this.reload(); // Draw my beautiful blobs.
}
@@ -221,10 +231,10 @@ class ScratchpadUi {
const prefixAns = document.getElementById(this.context.prefix_ans.id);
const showHide = document.getElementById(this.context.show_hide.id);
let serialisation = {
- answer_code: [''],
- test_code: [''],
- show_hide: [''],
- prefix_ans: ['']
+ answer_code: [this.context.answer_code.text],
+ test_code: [this.context.test_code.text],
+ show_hide: [this.context.show_hide.show],
+ prefix_ans: [this.context.prefix_ans.checked]
};
if (this.answerTextarea) {
serialisation.answer_code = [this.answerTextarea.value];
@@ -350,16 +360,6 @@ class ScratchpadUi {
}
async reload() {
- const preloadString = this.textArea.value;
- let preload;
- try {
- preload = this.readJson(preloadString);
- } catch (error) {
- this.fail = true;
- this.failString = 'scratchpad_ui_invalidserialisation';
- return;
- }
- this.updateContext(preload);
try {
const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);
this.drawUi(html);
From 22adac6a7a328a1229b2e375edc98c6190df03e1 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Thu, 16 Mar 2023 21:35:49 +1300
Subject: [PATCH 072/188] Comment out a test of some redundant functionality
---
tests/behat/scratchpad_ui.feature | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/tests/behat/scratchpad_ui.feature b/tests/behat/scratchpad_ui.feature
index 9397521a5..6f5343ab4 100644
--- a/tests/behat/scratchpad_ui.feature
+++ b/tests/behat/scratchpad_ui.feature
@@ -209,18 +209,18 @@ Feature: Test the Scratchpad UI
{"answer_code":[""],"test_code":["print('hello world')"],"show_hide":["1"],"prefix_ans":[""]}
"""
- When I press the CTRL + ALT M key
- Then I wait "2" seconds
- Then I click on "Scratchpad" "button"
-
- Then I wait "2" seconds
- Then I should not see "Run"
-
- When I press the CTRL + ALT M key
- Then I should see in answer field:
- """
- {"answer_code":[""],"test_code":["print('hello world')"],"show_hide":[""],"prefix_ans":[""]}
- """
+ #When I press the CTRL + ALT M key
+ #Then I wait "2" seconds
+ #Then I click on "Scratchpad" "button"
+
+ #Then I wait "2" seconds
+ #Then I should not see "Run"
+
+ #When I press the CTRL + ALT M key
+ #Then I should see in answer field:
+ #"""
+ #{"answer_code":[""],"test_code":["print('hello world')"],"show_hide":[""],"prefix_ans":[""]}
+ #"""
@serial
Scenario: Get UI serialization, while Scratchpad Shown and prefix box NOT ticked
From 6a95604198e99d993282a0c81847e5043f16bce9 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Thu, 16 Mar 2023 21:36:24 +1300
Subject: [PATCH 073/188] Fix error in last commit - prefix_ans was being
negated on reload.
---
amd/build/ui_scratchpad.min.js | 2 +-
amd/build/ui_scratchpad.min.js.map | 2 +-
amd/src/ui_scratchpad.js | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/amd/build/ui_scratchpad.min.js b/amd/build/ui_scratchpad.min.js
index b75962a9b..8c595e960 100644
--- a/amd/build/ui_scratchpad.min.js
+++ b/amd/build/ui_scratchpad.min.js
@@ -1,3 +1,3 @@
-define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_templates,_userinterfacewrapper,_outputdisplayarea){var obj;function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}function _defineProperties(target,props){for(var i=0;iarr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0}))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}},{key:"getElement",value:function(){return this.outerDiv}},{key:"handleRunButtonClick",value:function(){var _this=this;if(null!==this.outputDisplay){this.sync();var preloadString=this.textArea.value,serial=this.readJson(preloadString),escape=function(code){return _this.uiParams.escape?JSON.stringify(code).slice(1,-1):code},code=function(answerCode,testCode,prefixAns,template){var open=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"\\(",close=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"\\)";template||(template="".concat(open," ANSWER_CODE ").concat(close,"\n")+"".concat(open," SCRATCHPAD_CODE ").concat(close)),prefixAns||(answerCode="");var escOpen=escapeRegExp(open),escClose=escapeRegExp(close),answerRegex=new RegExp("".concat(escOpen,"\\s*ANSWER_CODE\\s*").concat(escClose),"g"),scratchpadRegex=new RegExp("".concat(escOpen,"\\s*SCRATCHPAD_CODE\\s*").concat(escClose),"g");return(template=template.replaceAll(answerRegex,answerCode)).replaceAll(scratchpadRegex,testCode)}(escape(serial.answer_code[0]),escape(serial.test_code[0]),serial.prefix_ans[0],this.runWrapper,this.uiParams.open_delimiter,this.uiParams.close_delimiter);this.outputDisplay.runCode(code,"",!0)}}},{key:"updateContext",value:function(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}},{key:"readJson",value:function(preloadString){var serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch(_unused){serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}},{key:"reload",value:(fn=regeneratorRuntime.mark((function _callee(){var _yield$Templates$rend,html;return regeneratorRuntime.wrap((function(_context){for(;;)switch(_context.prev=_context.next){case 0:return _context.prev=0,_context.next=3,_templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);case 3:_yield$Templates$rend=_context.sent,html=_yield$Templates$rend.html,this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners(),_context.next=15;break;case 11:_context.prev=11,_context.t0=_context.catch(0),this.fail=!0,this.failString="scratchpad_ui_templateloadfail";case 15:case"end":return _context.stop()}}),_callee,this,[[0,11]])})),_reload=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(){return _reload.apply(this,arguments)})},{key:"drawUi",value:function(html){var wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}},{key:"addAceUis",value:function(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}},{key:"addEventListeners",value:function(){var _this2=this,runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(function(){return _this2.handleRunButtonClick()}))}},{key:"resize",value:function(){}},{key:"hasFocus",value:function(){var _this$answerCodeUi,_this$testCodeUi,focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}},{key:"destroy",value:function(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}],protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),ScratchpadUi}();_exports.Constructor=ScratchpadUi}));
+define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_templates,_userinterfacewrapper,_outputdisplayarea){var obj;function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}function _defineProperties(target,props){for(var i=0;iarr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0}))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}},{key:"getElement",value:function(){return this.outerDiv}},{key:"handleRunButtonClick",value:function(){var _this=this;if(null!==this.outputDisplay){this.sync();var preloadString=this.textArea.value,serial=this.readJson(preloadString),escape=function(code){return _this.uiParams.escape?JSON.stringify(code).slice(1,-1):code},code=function(answerCode,testCode,prefixAns,template){var open=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"\\(",close=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"\\)";template||(template="".concat(open," ANSWER_CODE ").concat(close,"\n")+"".concat(open," SCRATCHPAD_CODE ").concat(close)),prefixAns||(answerCode="");var escOpen=escapeRegExp(open),escClose=escapeRegExp(close),answerRegex=new RegExp("".concat(escOpen,"\\s*ANSWER_CODE\\s*").concat(escClose),"g"),scratchpadRegex=new RegExp("".concat(escOpen,"\\s*SCRATCHPAD_CODE\\s*").concat(escClose),"g");return(template=template.replaceAll(answerRegex,answerCode)).replaceAll(scratchpadRegex,testCode)}(escape(serial.answer_code[0]),escape(serial.test_code[0]),serial.prefix_ans[0],this.runWrapper,this.uiParams.open_delimiter,this.uiParams.close_delimiter);this.outputDisplay.runCode(code,"",!0)}}},{key:"updateContext",value:function(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}},{key:"readJson",value:function(preloadString){var serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch(_unused){serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}},{key:"reload",value:(fn=regeneratorRuntime.mark((function _callee(){var _yield$Templates$rend,html;return regeneratorRuntime.wrap((function(_context){for(;;)switch(_context.prev=_context.next){case 0:return _context.prev=0,_context.next=3,_templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);case 3:_yield$Templates$rend=_context.sent,html=_yield$Templates$rend.html,this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners(),_context.next=15;break;case 11:_context.prev=11,_context.t0=_context.catch(0),this.fail=!0,this.failString="scratchpad_ui_templateloadfail";case 15:case"end":return _context.stop()}}),_callee,this,[[0,11]])})),_reload=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(){return _reload.apply(this,arguments)})},{key:"drawUi",value:function(html){var wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}},{key:"addAceUis",value:function(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}},{key:"addEventListeners",value:function(){var _this2=this,runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(function(){return _this2.handleRunButtonClick()}))}},{key:"resize",value:function(){}},{key:"hasFocus",value:function(){var _this$answerCodeUi,_this$testCodeUi,focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}},{key:"destroy",value:function(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}],protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),ScratchpadUi}();_exports.Constructor=ScratchpadUi}));
//# sourceMappingURL=ui_scratchpad.min.js.map
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js.map b/amd/build/ui_scratchpad.min.js.map
index 9498a8dc4..36e1a822b 100644
--- a/amd/build/ui_scratchpad.min.js.map
+++ b/amd/build/ui_scratchpad.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text from first editor.\n * @param {string} testCode text from second editor.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @param {string} open delimiter to look for, e.g. '[['\n * @param {string} close delimiter to look for, e.g. ']]'\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template, open = '\\\\(', close = '\\\\)') => {\n if (!template) {\n template = `${open} ANSWER_CODE ${close}\\n` +\n `${open} SCRATCHPAD_CODE ${close}`;\n }\n if (!prefixAns) {\n answerCode = '';\n }\n const escOpen = escapeRegExp(open);\n const escClose = escapeRegExp(close);\n const answerRegex = new RegExp(`${escOpen}\\\\s*ANSWER_CODE\\\\s*${escClose}`, 'g');\n const scratchpadRegex = new RegExp(`${escOpen}\\\\s*SCRATCHPAD_CODE\\\\s*${escClose}`, 'g');\n template = template.replaceAll(answerRegex, answerCode);\n template = template.replaceAll(scratchpadRegex, testCode);\n return template;\n};\n\n/**\n * Escapes a string for use in regex.\n * @param {string} string to escape.\n * @returns {string} RegEx escaped string\n */\nconst escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"); // $& means the whole matched string\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the default value.\n * Does not add keys/values to the result if that key is not in defaults.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!(el.classList.contains('collapse') || el.classList.contains('collapsing'))) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null,\n open_delimiter: '{|',\n close_delimiter: '|}',\n escape: false\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n let serialisation = {\n answer_code: [this.context.answer_code.text],\n test_code: [this.context.test_code.text],\n show_hide: [this.context.show_hide.show],\n prefix_ans: [this.context.prefix_ans.checked]\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const escape = (code) => this.uiParams.escape ? JSON.stringify(code).slice(1, -1) : code;\n const answerCode = escape(serial.answer_code[0]);\n const testCode = escape(serial.test_code[0]);\n const code = fillWrapper(\n answerCode,\n testCode,\n serial.prefix_ans[0],\n this.runWrapper,\n this.uiParams.open_delimiter,\n this.uiParams.close_delimiter\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","escapeRegExp","string","replace","overwriteValues","defaults","prescribed","overwritten","Object","entries","key","value","ScratchpadUi","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","open_delimiter","close_delimiter","escape","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","preload","preloadString","readJson","error","failString","updateContext","reload","wrapperSrc","dataset","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","text","test_code","show","checked","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","values","some","val","length","JSON","stringify","sync","serial","code","_this","slice","answerCode","testCode","template","open","close","escOpen","escClose","answerRegex","RegExp","scratchpadRegex","replaceAll","fillWrapper","runCode","render","CSS","parse","hasOwnProperty","TypeError","Templates","renderForPromise","html","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","_this2","handleRunButtonClick","focused","_this$answerCodeUi","uiInstance","hasFocus","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text from first editor.\n * @param {string} testCode text from second editor.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @param {string} open delimiter to look for, e.g. '[['\n * @param {string} close delimiter to look for, e.g. ']]'\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template, open = '\\\\(', close = '\\\\)') => {\n if (!template) {\n template = `${open} ANSWER_CODE ${close}\\n` +\n `${open} SCRATCHPAD_CODE ${close}`;\n }\n if (!prefixAns) {\n answerCode = '';\n }\n const escOpen = escapeRegExp(open);\n const escClose = escapeRegExp(close);\n const answerRegex = new RegExp(`${escOpen}\\\\s*ANSWER_CODE\\\\s*${escClose}`, 'g');\n const scratchpadRegex = new RegExp(`${escOpen}\\\\s*SCRATCHPAD_CODE\\\\s*${escClose}`, 'g');\n template = template.replaceAll(answerRegex, answerCode);\n template = template.replaceAll(scratchpadRegex, testCode);\n return template;\n};\n\n/**\n * Escapes a string for use in regex.\n * @param {string} string to escape.\n * @returns {string} RegEx escaped string\n */\nconst escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"); // $& means the whole matched string\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the default value.\n * Does not add keys/values to the result if that key is not in defaults.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!(el.classList.contains('collapse') || el.classList.contains('collapsing'))) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null,\n open_delimiter: '{|',\n close_delimiter: '|}',\n escape: false\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n let serialisation = {\n answer_code: [this.context.answer_code.text],\n test_code: [this.context.test_code.text],\n show_hide: [this.context.show_hide.show],\n prefix_ans: [!this.context.prefix_ans.checked]\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const escape = (code) => this.uiParams.escape ? JSON.stringify(code).slice(1, -1) : code;\n const answerCode = escape(serial.answer_code[0]);\n const testCode = escape(serial.test_code[0]);\n const code = fillWrapper(\n answerCode,\n testCode,\n serial.prefix_ans[0],\n this.runWrapper,\n this.uiParams.open_delimiter,\n this.uiParams.close_delimiter\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","escapeRegExp","string","replace","overwriteValues","defaults","prescribed","overwritten","Object","entries","key","value","ScratchpadUi","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","open_delimiter","close_delimiter","escape","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","preload","preloadString","readJson","error","failString","updateContext","reload","wrapperSrc","dataset","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","text","test_code","show","checked","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","values","some","val","length","JSON","stringify","sync","serial","code","_this","slice","answerCode","testCode","template","open","close","escOpen","escClose","answerRegex","RegExp","scratchpadRegex","replaceAll","fillWrapper","runCode","render","CSS","parse","hasOwnProperty","TypeError","Templates","renderForPromise","html","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","_this2","handleRunButtonClick","focused","_this$answerCodeUi","uiInstance","hasFocus","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/ui_scratchpad.js b/amd/src/ui_scratchpad.js
index 800a30fc7..cd5bdfd18 100644
--- a/amd/src/ui_scratchpad.js
+++ b/amd/src/ui_scratchpad.js
@@ -234,7 +234,7 @@ class ScratchpadUi {
answer_code: [this.context.answer_code.text],
test_code: [this.context.test_code.text],
show_hide: [this.context.show_hide.show],
- prefix_ans: [this.context.prefix_ans.checked]
+ prefix_ans: [!this.context.prefix_ans.checked]
};
if (this.answerTextarea) {
serialisation.answer_code = [this.answerTextarea.value];
From 8c874a169e90c2d5671f4363d86214e7fbc86574 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Fri, 17 Mar 2023 11:53:21 +1300
Subject: [PATCH 074/188] Further tweaking of ui_scratchpad to handle some edge
cases not handled by recent refactoring.
---
amd/build/ui_scratchpad.min.js | 2 +-
amd/build/ui_scratchpad.min.js.map | 2 +-
amd/src/ui_scratchpad.js | 8 +++++++-
tests/behat/scratchpad_ui.feature | 24 ++++++++++++------------
4 files changed, 21 insertions(+), 15 deletions(-)
diff --git a/amd/build/ui_scratchpad.min.js b/amd/build/ui_scratchpad.min.js
index 8c595e960..ecae9f4c4 100644
--- a/amd/build/ui_scratchpad.min.js
+++ b/amd/build/ui_scratchpad.min.js
@@ -1,3 +1,3 @@
-define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_templates,_userinterfacewrapper,_outputdisplayarea){var obj;function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}function _defineProperties(target,props){for(var i=0;iarr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0}))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}},{key:"getElement",value:function(){return this.outerDiv}},{key:"handleRunButtonClick",value:function(){var _this=this;if(null!==this.outputDisplay){this.sync();var preloadString=this.textArea.value,serial=this.readJson(preloadString),escape=function(code){return _this.uiParams.escape?JSON.stringify(code).slice(1,-1):code},code=function(answerCode,testCode,prefixAns,template){var open=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"\\(",close=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"\\)";template||(template="".concat(open," ANSWER_CODE ").concat(close,"\n")+"".concat(open," SCRATCHPAD_CODE ").concat(close)),prefixAns||(answerCode="");var escOpen=escapeRegExp(open),escClose=escapeRegExp(close),answerRegex=new RegExp("".concat(escOpen,"\\s*ANSWER_CODE\\s*").concat(escClose),"g"),scratchpadRegex=new RegExp("".concat(escOpen,"\\s*SCRATCHPAD_CODE\\s*").concat(escClose),"g");return(template=template.replaceAll(answerRegex,answerCode)).replaceAll(scratchpadRegex,testCode)}(escape(serial.answer_code[0]),escape(serial.test_code[0]),serial.prefix_ans[0],this.runWrapper,this.uiParams.open_delimiter,this.uiParams.close_delimiter);this.outputDisplay.runCode(code,"",!0)}}},{key:"updateContext",value:function(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}},{key:"readJson",value:function(preloadString){var serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch(_unused){serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}},{key:"reload",value:(fn=regeneratorRuntime.mark((function _callee(){var _yield$Templates$rend,html;return regeneratorRuntime.wrap((function(_context){for(;;)switch(_context.prev=_context.next){case 0:return _context.prev=0,_context.next=3,_templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);case 3:_yield$Templates$rend=_context.sent,html=_yield$Templates$rend.html,this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners(),_context.next=15;break;case 11:_context.prev=11,_context.t0=_context.catch(0),this.fail=!0,this.failString="scratchpad_ui_templateloadfail";case 15:case"end":return _context.stop()}}),_callee,this,[[0,11]])})),_reload=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(){return _reload.apply(this,arguments)})},{key:"drawUi",value:function(html){var wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}},{key:"addAceUis",value:function(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}},{key:"addEventListeners",value:function(){var _this2=this,runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(function(){return _this2.handleRunButtonClick()}))}},{key:"resize",value:function(){}},{key:"hasFocus",value:function(){var _this$answerCodeUi,_this$testCodeUi,focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}},{key:"destroy",value:function(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}],protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),ScratchpadUi}();_exports.Constructor=ScratchpadUi}));
+define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coderunner/userinterfacewrapper","qtype_coderunner/outputdisplayarea"],(function(_exports,_templates,_userinterfacewrapper,_outputdisplayarea){var obj;function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}function _defineProperties(target,props){for(var i=0;iarr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0}))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}},{key:"getElement",value:function(){return this.outerDiv}},{key:"handleRunButtonClick",value:function(){var _this=this;if(null!==this.outputDisplay){this.sync();var preloadString=this.textArea.value,serial=this.readJson(preloadString),escape=function(code){return _this.uiParams.escape?JSON.stringify(code).slice(1,-1):code},code=function(answerCode,testCode,prefixAns,template){var open=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"\\(",close=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"\\)";template||(template="".concat(open," ANSWER_CODE ").concat(close,"\n")+"".concat(open," SCRATCHPAD_CODE ").concat(close)),prefixAns||(answerCode="");var escOpen=escapeRegExp(open),escClose=escapeRegExp(close),answerRegex=new RegExp("".concat(escOpen,"\\s*ANSWER_CODE\\s*").concat(escClose),"g"),scratchpadRegex=new RegExp("".concat(escOpen,"\\s*SCRATCHPAD_CODE\\s*").concat(escClose),"g");return(template=template.replaceAll(answerRegex,answerCode)).replaceAll(scratchpadRegex,testCode)}(escape(serial.answer_code[0]),escape(serial.test_code[0]),serial.prefix_ans[0],this.runWrapper,this.uiParams.open_delimiter,this.uiParams.close_delimiter);this.outputDisplay.runCode(code,"",!0)}}},{key:"updateContext",value:function(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}},{key:"readJson",value:function(preloadString){var serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch(_unused){serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}},{key:"reload",value:(fn=regeneratorRuntime.mark((function _callee(){var _yield$Templates$rend,html;return regeneratorRuntime.wrap((function(_context){for(;;)switch(_context.prev=_context.next){case 0:return _context.prev=0,_context.next=3,_templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);case 3:_yield$Templates$rend=_context.sent,html=_yield$Templates$rend.html,this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners(),_context.next=15;break;case 11:_context.prev=11,_context.t0=_context.catch(0),this.fail=!0,this.failString="scratchpad_ui_templateloadfail";case 15:case"end":return _context.stop()}}),_callee,this,[[0,11]])})),_reload=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(){return _reload.apply(this,arguments)})},{key:"drawUi",value:function(html){var wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}},{key:"addAceUis",value:function(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}},{key:"addEventListeners",value:function(){var _this2=this,runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(function(){return _this2.handleRunButtonClick()}))}},{key:"resize",value:function(){}},{key:"hasFocus",value:function(){var _this$answerCodeUi,_this$testCodeUi,focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}},{key:"destroy",value:function(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}],protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Object.defineProperty(Constructor,"prototype",{writable:!1}),ScratchpadUi}();_exports.Constructor=ScratchpadUi}));
//# sourceMappingURL=ui_scratchpad.min.js.map
\ No newline at end of file
diff --git a/amd/build/ui_scratchpad.min.js.map b/amd/build/ui_scratchpad.min.js.map
index 36e1a822b..1199f8f58 100644
--- a/amd/build/ui_scratchpad.min.js.map
+++ b/amd/build/ui_scratchpad.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text from first editor.\n * @param {string} testCode text from second editor.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @param {string} open delimiter to look for, e.g. '[['\n * @param {string} close delimiter to look for, e.g. ']]'\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template, open = '\\\\(', close = '\\\\)') => {\n if (!template) {\n template = `${open} ANSWER_CODE ${close}\\n` +\n `${open} SCRATCHPAD_CODE ${close}`;\n }\n if (!prefixAns) {\n answerCode = '';\n }\n const escOpen = escapeRegExp(open);\n const escClose = escapeRegExp(close);\n const answerRegex = new RegExp(`${escOpen}\\\\s*ANSWER_CODE\\\\s*${escClose}`, 'g');\n const scratchpadRegex = new RegExp(`${escOpen}\\\\s*SCRATCHPAD_CODE\\\\s*${escClose}`, 'g');\n template = template.replaceAll(answerRegex, answerCode);\n template = template.replaceAll(scratchpadRegex, testCode);\n return template;\n};\n\n/**\n * Escapes a string for use in regex.\n * @param {string} string to escape.\n * @returns {string} RegEx escaped string\n */\nconst escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"); // $& means the whole matched string\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the default value.\n * Does not add keys/values to the result if that key is not in defaults.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!(el.classList.contains('collapse') || el.classList.contains('collapsing'))) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null,\n open_delimiter: '{|',\n close_delimiter: '|}',\n escape: false\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n let serialisation = {\n answer_code: [this.context.answer_code.text],\n test_code: [this.context.test_code.text],\n show_hide: [this.context.show_hide.show],\n prefix_ans: [!this.context.prefix_ans.checked]\n };\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const escape = (code) => this.uiParams.escape ? JSON.stringify(code).slice(1, -1) : code;\n const answerCode = escape(serial.answer_code[0]);\n const testCode = escape(serial.test_code[0]);\n const code = fillWrapper(\n answerCode,\n testCode,\n serial.prefix_ans[0],\n this.runWrapper,\n this.uiParams.open_delimiter,\n this.uiParams.close_delimiter\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","escapeRegExp","string","replace","overwriteValues","defaults","prescribed","overwritten","Object","entries","key","value","ScratchpadUi","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","open_delimiter","close_delimiter","escape","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","preload","preloadString","readJson","error","failString","updateContext","reload","wrapperSrc","dataset","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","text","test_code","show","checked","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","values","some","val","length","JSON","stringify","sync","serial","code","_this","slice","answerCode","testCode","template","open","close","escOpen","escClose","answerRegex","RegExp","scratchpadRegex","replaceAll","fillWrapper","runCode","render","CSS","parse","hasOwnProperty","TypeError","Templates","renderForPromise","html","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","_this2","handleRunButtonClick","focused","_this$answerCodeUi","uiInstance","hasFocus","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.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 util.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 * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text from first editor.\n * @param {string} testCode text from second editor.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @param {string} open delimiter to look for, e.g. '[['\n * @param {string} close delimiter to look for, e.g. ']]'\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template, open = '\\\\(', close = '\\\\)') => {\n if (!template) {\n template = `${open} ANSWER_CODE ${close}\\n` +\n `${open} SCRATCHPAD_CODE ${close}`;\n }\n if (!prefixAns) {\n answerCode = '';\n }\n const escOpen = escapeRegExp(open);\n const escClose = escapeRegExp(close);\n const answerRegex = new RegExp(`${escOpen}\\\\s*ANSWER_CODE\\\\s*${escClose}`, 'g');\n const scratchpadRegex = new RegExp(`${escOpen}\\\\s*SCRATCHPAD_CODE\\\\s*${escClose}`, 'g');\n template = template.replaceAll(answerRegex, answerCode);\n template = template.replaceAll(scratchpadRegex, testCode);\n return template;\n};\n\n/**\n * Escapes a string for use in regex.\n * @param {string} string to escape.\n * @returns {string} RegEx escaped string\n */\nconst escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"); // $& means the whole matched string\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the default value.\n * Does not add keys/values to the result if that key is not in defaults.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!(el.classList.contains('collapse') || el.classList.contains('collapsing'))) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null,\n open_delimiter: '{|',\n close_delimiter: '|}',\n escape: false\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n // Initialise using the JSON string from the server.\n let serialisation = {\n answer_code: [this.context.answer_code.text],\n test_code: [this.context.test_code.text],\n show_hide: [this.context.show_hide.show],\n prefix_ans: [invertSerial(this.context.prefix_ans.checked)]\n };\n // If the UI is up and running, update elements from the UI.\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n } else {\n serialisation.show_hide = [''];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n } else {\n serialisation.prefix_ans = [''];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const escape = (code) => this.uiParams.escape ? JSON.stringify(code).slice(1, -1) : code;\n const answerCode = escape(serial.answer_code[0]);\n const testCode = escape(serial.test_code[0]);\n const code = fillWrapper(\n answerCode,\n testCode,\n serial.prefix_ans[0],\n this.runWrapper,\n this.uiParams.open_delimiter,\n this.uiParams.close_delimiter\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","escapeRegExp","string","replace","overwriteValues","defaults","prescribed","overwritten","Object","entries","key","value","ScratchpadUi","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","open_delimiter","close_delimiter","escape","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","preload","preloadString","readJson","error","failString","updateContext","reload","wrapperSrc","dataset","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","text","test_code","show","checked","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","values","some","val","length","JSON","stringify","sync","serial","code","_this","slice","answerCode","testCode","template","open","close","escOpen","escClose","answerRegex","RegExp","scratchpadRegex","replaceAll","fillWrapper","runCode","render","CSS","parse","hasOwnProperty","TypeError","Templates","renderForPromise","html","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","_this2","handleRunButtonClick","focused","_this$answerCodeUi","uiInstance","hasFocus","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/ui_scratchpad.js b/amd/src/ui_scratchpad.js
index cd5bdfd18..7268abb91 100644
--- a/amd/src/ui_scratchpad.js
+++ b/amd/src/ui_scratchpad.js
@@ -230,12 +230,14 @@ class ScratchpadUi {
getSerialisation() {
const prefixAns = document.getElementById(this.context.prefix_ans.id);
const showHide = document.getElementById(this.context.show_hide.id);
+ // Initialise using the JSON string from the server.
let serialisation = {
answer_code: [this.context.answer_code.text],
test_code: [this.context.test_code.text],
show_hide: [this.context.show_hide.show],
- prefix_ans: [!this.context.prefix_ans.checked]
+ prefix_ans: [invertSerial(this.context.prefix_ans.checked)]
};
+ // If the UI is up and running, update elements from the UI.
if (this.answerTextarea) {
serialisation.answer_code = [this.answerTextarea.value];
}
@@ -244,9 +246,13 @@ class ScratchpadUi {
}
if (showHide && !isCollapsed(showHide)) {
serialisation.show_hide = ['1'];
+ } else {
+ serialisation.show_hide = [''];
}
if (prefixAns?.checked || this.context.disable_scratchpad) {
serialisation.prefix_ans = ['1'];
+ } else {
+ serialisation.prefix_ans = [''];
}
if (this.invertPreload) {
serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);
diff --git a/tests/behat/scratchpad_ui.feature b/tests/behat/scratchpad_ui.feature
index 6f5343ab4..9397521a5 100644
--- a/tests/behat/scratchpad_ui.feature
+++ b/tests/behat/scratchpad_ui.feature
@@ -209,18 +209,18 @@ Feature: Test the Scratchpad UI
{"answer_code":[""],"test_code":["print('hello world')"],"show_hide":["1"],"prefix_ans":[""]}
"""
- #When I press the CTRL + ALT M key
- #Then I wait "2" seconds
- #Then I click on "Scratchpad" "button"
-
- #Then I wait "2" seconds
- #Then I should not see "Run"
-
- #When I press the CTRL + ALT M key
- #Then I should see in answer field:
- #"""
- #{"answer_code":[""],"test_code":["print('hello world')"],"show_hide":[""],"prefix_ans":[""]}
- #"""
+ When I press the CTRL + ALT M key
+ Then I wait "2" seconds
+ Then I click on "Scratchpad" "button"
+
+ Then I wait "2" seconds
+ Then I should not see "Run"
+
+ When I press the CTRL + ALT M key
+ Then I should see in answer field:
+ """
+ {"answer_code":[""],"test_code":["print('hello world')"],"show_hide":[""],"prefix_ans":[""]}
+ """
@serial
Scenario: Get UI serialization, while Scratchpad Shown and prefix box NOT ticked
From 8a31488b52d840c98bc1ed907adf9482bd931ae4 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Mon, 20 Mar 2023 14:45:23 +1300
Subject: [PATCH 075/188] Try to reduce the probability of loss of data from
the Ace UI if the Ace editor load fails. Also reduce the sync timeout from 5
secs to 2 secs to reduce the risk of data loss when a quiz attempt times out.
---
amd/src/ui_ace_gapfiller.js | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/amd/src/ui_ace_gapfiller.js b/amd/src/ui_ace_gapfiller.js
index f81753994..c1923358b 100644
--- a/amd/src/ui_ace_gapfiller.js
+++ b/amd/src/ui_ace_gapfiller.js
@@ -385,6 +385,9 @@ define(['jquery'], function($) {
// Sync to TextArea
AceGapfillerUi.prototype.sync = function() {
+ if (this.fail) {
+ return; // Leave the text area alone if Ace load failed.
+ }
let serialisation = []; // A list of field values.
let empty = true;
@@ -403,6 +406,10 @@ define(['jquery'], function($) {
}
};
+ // Sync every 2 seconds in case quiz closes automatically without user
+ // action.
+ AceGapfillerUi.prototype.syncIntervalSecs = (() => 2);
+
// Reload the HTML fields from the given serialisation.
AceGapfillerUi.prototype.reload = function() {
let content = this.textArea.val();
From 9ab70998052e8c8610ad01e101527c38e78498aa Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Thu, 6 Apr 2023 21:39:03 +1200
Subject: [PATCH 076/188] Bug fix: calls to get the best sandbox (e.g. by
filter_ace_inline) failed if multiple Jobe servers were configured.
---
classes/jobesandbox.php | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/classes/jobesandbox.php b/classes/jobesandbox.php
index 712582c42..bcb5706c7 100644
--- a/classes/jobesandbox.php
+++ b/classes/jobesandbox.php
@@ -209,7 +209,7 @@ public function execute($sourcecode, $language, $input, $files=null, $params=nul
$httpcode = $this->submit($postbody);
}
}
-
+
$runresult = array();
$runresult['sandboxinfo'] = array(
'jobeserver' => $this->jobeserver,
@@ -313,10 +313,16 @@ private function put_file($contents) {
*/
private function get_jobe_connection_info($resource) {
$jobe = $this->jobeserver;
- if (!empty($this->currentjobid) && strpos($jobe, ';') !== false) {
+ if (strpos($jobe, ';') !== false) {
// Support multiple servers - thanks Khang Pham Nguyen KHANG: 2021/10/18.
$servers = array_values(array_filter(array_map('trim', explode(';', $jobe)), 'strlen'));
- $jobe = $servers[intval($this->currentjobid, 16) % count($servers)];
+ if ($this->currentjobid) {
+ // Make sure to use the same jobe server when files are involved.
+ $rand = intval($this->currentjobid, 16);
+ } else {
+ $rand = mt_rand();
+ }
+ $jobe = $servers[$rand % count($servers)];
}
$jobe = trim($jobe); // Remove leading or trailing extra whitespace from the settings.
$protocol = 'http://';
From 5bf53be2d617e21412ada3d48551fa1b9ea4bbe8 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Fri, 7 Apr 2023 11:08:24 +1200
Subject: [PATCH 077/188] Updated minimised js
---
amd/build/ui_ace_gapfiller.min.js | 2 +-
amd/build/ui_ace_gapfiller.min.js.map | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/amd/build/ui_ace_gapfiller.min.js b/amd/build/ui_ace_gapfiller.min.js
index 4bf3d7434..a8d9e03f2 100644
--- a/amd/build/ui_ace_gapfiller.min.js
+++ b/amd/build/ui_ace_gapfiller.min.js
@@ -38,6 +38,6 @@
* @copyright Matthew Toohey, 2021, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_ace_gapfiller",["jquery"],(function($){var Range,validChars=/[ !"#$%&'()*+,`\-./0-9:;<=>?@A-Z\[\]\\^_a-z{}|~]/;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this,code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){var cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){var nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){var char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){var _char=e.args;validChars.test(_char)&&gap.insertChar(t.gaps,selectionRange.start,_char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){var maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+1=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(var i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(var i=start;i?@A-Z\[\]\\^_a-z{}|~]/;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this,code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$(""),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){var cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){var nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){var char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){var _char=e.args;validChars.test(_char)&&gap.insertChar(t.gaps,selectionRange.start,_char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){var cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){var maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+1=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(var i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(var i=start;i.\n\n/**\n * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Use the uiParams theme if provided else use light.\n if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","theme","setTheme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ui_ace_gapfiller.min.js","sources":["../src/ui_ace_gapfiller.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 util.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 * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9:;<=>?@A-Z\\[\\]\\\\^_a-z{}|~]/;\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Use the uiParams theme if provided else use light.\n if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n if (this.fail) {\n return; // Leave the text area alone if Ace load failed.\n }\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Sync every 2 seconds in case quiz closes automatically without user\n // action.\n AceGapfillerUi.prototype.syncIntervalSecs = (() => 2);\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","theme","setTheme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","syncIntervalSecs","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"}
\ No newline at end of file
From f82f2259cc4538bf9a7138e81ec472e5253b1c93 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Sat, 6 May 2023 14:19:05 +1200
Subject: [PATCH 078/188] Allow 0 marks for test cases.
---
edit_coderunner_form.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/edit_coderunner_form.php b/edit_coderunner_form.php
index f2ad0ba3a..019c1a03a 100644
--- a/edit_coderunner_form.php
+++ b/edit_coderunner_form.php
@@ -1192,7 +1192,7 @@ private function validate_test_cases($data) {
if ($mark != '') {
if (!is_numeric($mark)) {
$errors["testcode[$i]"] = get_string('nonnumericmark', 'qtype_coderunner');
- } else if (floatval($mark) <= 0) {
+ } else if (floatval($mark) < 0) {
$errors["testcode[$i]"] = get_string('negativeorzeromark', 'qtype_coderunner');
}
}
From d5b31319deec7370a6030278349cc063c11cd64a Mon Sep 17 00:00:00 2001
From: Anupama Sarjoshi <81178902+AnupamaSarjoshi@users.noreply.github.com>
Date: Tue, 9 May 2023 22:20:41 +0100
Subject: [PATCH 079/188] Fix PHP8.1 deprecated error for trim function (#169)
---
questiontype.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/questiontype.php b/questiontype.php
index d0a5cdf89..078d4c3cb 100644
--- a/questiontype.php
+++ b/questiontype.php
@@ -358,7 +358,7 @@ public function clean_question_form($question, $isvalidation=false) {
}
}
- if (trim($question->sandbox) === 'DEFAULT') {
+ if (trim($question->sandbox ?? '') === 'DEFAULT') {
$question->sandbox = null;
}
From ed3afc725bdce28debb50ee66ecbbf6afeca7227 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Mon, 15 May 2023 09:44:57 +1200
Subject: [PATCH 080/188] Set specific column widths for compatibility with
newer versions of sqlite3.
---
samples/sqlexamples.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/samples/sqlexamples.xml b/samples/sqlexamples.xml
index 28b724326..556c9fa63 100644
--- a/samples/sqlexamples.xml
+++ b/samples/sqlexamples.xml
@@ -66,7 +66,7 @@ order by order_num]]>
-
+
From 5290ccddbebd87ff5d70bc684c21b07725c52c02 Mon Sep 17 00:00:00 2001
From: Richard Lobb
Date: Tue, 16 May 2023 21:15:24 +1200
Subject: [PATCH 081/188] Use input elements in the Table UI rather than
textareas when there is only one row per cell. Reduces confusion when student
hits Enter key.
---
amd/build/ui_table.min.js | 7 +++--
amd/build/ui_table.min.js.map | 2 +-
amd/src/ui_table.js | 58 ++++++++++++++++++++++++-----------
3 files changed, 46 insertions(+), 21 deletions(-)
diff --git a/amd/build/ui_table.min.js b/amd/build/ui_table.min.js
index 5f33da58d..0aee7d268 100644
--- a/amd/build/ui_table.min.js
+++ b/amd/build/ui_table.min.js
@@ -18,8 +18,11 @@
* 5. width_percents: a list of the percentages of the width occupied
* by each column. This list must include a value for the row labels, if present.
*
+ * Individual cells are textareas except when the number of rows per cell is set to
+ * 1, in which case input elements are used instead.
+ *
* The serialisation of the table, which is what is essentially copied back
- * into the textarea for submissions as the answer, is a JSON array. Each
+ * into the original answer box textarea for submissions as the answer, is a JSON array. Each
* element in the array is itself an array containing the values of one row
* of the table. Empty cells are empty strings. The table header row and row
* label columns are not provided in the serialisation.
@@ -37,6 +40,6 @@
* @copyright Richard Lobb, 2018, The University of Canterbury
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("qtype_coderunner/ui_table",["jquery"],(function($){function TableUi(textareaId,width,height,uiParams){if(this.textArea=$(document.getElementById(textareaId)),this.readOnly=this.textArea.prop("readonly"),this.tableDiv=null,this.uiParams=uiParams,!uiParams.num_columns||!uiParams.num_rows)return this.fail=!0,void(this.failString="table_ui_missingparams");this.fail=!1,this.lockedCells=uiParams.locked_cells||[],this.hasHeader=!!(uiParams.column_headers&&uiParams.column_headers.length>0),this.hasRowLabels=!!(uiParams.row_labels&&uiParams.row_labels.length>0),this.numDataColumns=uiParams.num_columns,this.rowsPerCell=uiParams.lines_per_cell||2,this.totNumColumns=this.numDataColumns+(this.hasRowLabels?1:0),this.columnWidths=this.computeColumnWidths(),this.reload()}return TableUi.prototype.computeColumnWidths=function(){var defaultWidth=Math.trunc(100/this.totNumColumns),columnWidths=[];if(this.uiParams.column_width_percents&&this.uiParams.column_width_percents.length>0)return this.uiParams.column_width_percents;if(Array.prototype.fill)return new Array(this.totNumColumns).fill(defaultWidth);for(var i=0;i",iRow");for(var iCol=0;iCol",html+='