diff --git a/amd/build/userinterfacewrapper.min.js b/amd/build/userinterfacewrapper.min.js
index 86d660ab..f5487bb6 100644
--- a/amd/build/userinterfacewrapper.min.js
+++ b/amd/build/userinterfacewrapper.min.js
@@ -105,6 +105,6 @@
* 'Constructor' that references the constructor (e.g. Graph, AceWrapper etc)
*
*****************************************************************************/
-define("qtype_coderunner/userinterfacewrapper",["jquery","core/templates","core/notification"],(function($,Templates,Notification){function InterfaceWrapper(uiname,textareaId){let t=this;this.GUTTER=14,this.DEFAULT_SYNC_INTERVAL_SECS=5;this.isFullScreenEnable=null,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.setAllowFullScreen=function(enableFullScreen){this.isFullScreenEnable=enableFullScreen},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{var _uiInstance$allowFull;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),(null!==t.isFullScreenEnable?t.isFullScreenEnable:null===(_uiInstance$allowFull=uiInstance.allowFullScreen)||void 0===_uiInstance$allowFull?void 0:_uiInstance$allowFull.call(uiInstance))?t.initFullScreenToggle(t.taId):t.removeFullScreenButton(t.taId)}var langString,errorDiv;t.isLoading=!1})))},InterfaceWrapper.prototype.removeFullScreenButton=function(fieldId){const screenModeButton=document.getElementById("".concat(fieldId,"_wrapper")).parentNode.querySelector(".screen-mode-button");screenModeButton&&screenModeButton.remove()},InterfaceWrapper.prototype.initFullScreenToggle=function(fieldId){const wrapperEditor=document.getElementById("".concat(fieldId,"_wrapper"));function enterFullscreen(fullscreenButton,exitFullscreenButton,e){let t=this;e.preventDefault(),t.wrapperHeight=t.wrapperNode.innerHeight(),t.heightEditNode=t.hLast-t.GUTTER,t.widthEditNode=t.wLast,fullscreenButton.classList.add("d-none"),wrapperEditor.append(exitFullscreenButton),wrapperEditor.addEventListener("fullscreenchange",(()=>{null===document.fullscreenElement?(t.uiInstance.resize(t.widthEditNode,t.heightEditNode),wrapperEditor.style.height=t.wrapperHeight+"px",exitFullscreenButton.classList.add("d-none"),fullscreenButton.classList.remove("d-none")):exitFullscreenButton.classList.remove("d-none")})),wrapperEditor.requestFullscreen().catch(Notification.exception)}function exitFullscreen(e){let t=this;e.preventDefault(),document.exitFullscreen(),wrapperEditor.style.height=t.wrapperHeight+"px",t.uiInstance.resize(t.widthEditNode,t.heightEditNode)}wrapperEditor.parentNode.querySelector(".screen-mode-button")||Templates.renderForPromise("qtype_coderunner/screenmode_button",{}).then((_ref=>{let{html:html}=_ref;const screenModeButton=Templates.appendNodeContents(wrapperEditor,html,"")[0],fullscreenButton=screenModeButton.querySelector(".button-fullscreen"),exitFullscreenButton=screenModeButton.querySelector(".button-exit-fullscreen");fullscreenButton.classList.remove("d-none"),fullscreenButton.addEventListener("click",enterFullscreen.bind(this,fullscreenButton,exitFullscreenButton)),exitFullscreenButton.addEventListener("click",exitFullscreen.bind(this))}))},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","core/templates","core/notification"],(function($,Templates,Notification){function InterfaceWrapper(uiname,textareaId){let t=this;this.GUTTER=16,this.DEFAULT_SYNC_INTERVAL_SECS=5;this.isFullScreenEnable=null,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.setAllowFullScreen=function(enableFullScreen){this.isFullScreenEnable=enableFullScreen},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{var _uiInstance$allowFull;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),(null!==t.isFullScreenEnable?t.isFullScreenEnable:null===(_uiInstance$allowFull=uiInstance.allowFullScreen)||void 0===_uiInstance$allowFull?void 0:_uiInstance$allowFull.call(uiInstance))?t.initFullScreenToggle(t.taId):t.removeFullScreenButton(t.taId)}var langString,errorDiv;t.isLoading=!1})))},InterfaceWrapper.prototype.removeFullScreenButton=function(fieldId){const screenModeButton=document.getElementById("".concat(fieldId,"_wrapper")).parentNode.querySelector(".screen-mode-button");screenModeButton&&screenModeButton.remove()},InterfaceWrapper.prototype.initFullScreenToggle=function(fieldId){const wrapperEditor=document.getElementById("".concat(fieldId,"_wrapper"));function enterFullscreen(fullscreenButton,exitFullscreenButton,e){let t=this;e.preventDefault(),t.wrapperHeight=t.wrapperNode.innerHeight(),t.heightEditNode=t.hLast-t.GUTTER,t.widthEditNode=t.wLast,fullscreenButton.classList.add("d-none"),wrapperEditor.append(exitFullscreenButton),wrapperEditor.addEventListener("fullscreenchange",(()=>{null===document.fullscreenElement?(t.uiInstance.resize(t.widthEditNode,t.heightEditNode),wrapperEditor.style.height=t.wrapperHeight+"px",exitFullscreenButton.classList.add("d-none"),fullscreenButton.classList.remove("d-none")):exitFullscreenButton.classList.remove("d-none")})),wrapperEditor.requestFullscreen().catch(Notification.exception)}function exitFullscreen(e){let t=this;e.preventDefault(),document.exitFullscreen(),wrapperEditor.style.height=t.wrapperHeight+"px",t.uiInstance.resize(t.widthEditNode,t.heightEditNode)}wrapperEditor.parentNode.querySelector(".screen-mode-button")||Templates.renderForPromise("qtype_coderunner/screenmode_button",{}).then((_ref=>{let{html:html}=_ref;const screenModeButton=Templates.appendNodeContents(wrapperEditor,html,"")[0],fullscreenButton=screenModeButton.querySelector(".button-fullscreen"),exitFullscreenButton=screenModeButton.querySelector(".button-exit-fullscreen");fullscreenButton.classList.remove("d-none"),fullscreenButton.addEventListener("click",enterFullscreen.bind(this,fullscreenButton,exitFullscreenButton)),exitFullscreenButton.addEventListener("click",exitFullscreen.bind(this))}))},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 6187422f..f854bb74 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 5).\n *\n * 10. An allowFullScreen() method that returns True if the UI supports\n * use of the full-screen button in the bottom right of the UI wrapper.\n * Defaults to False if not implemented.\n *\n * 11. A setAllowFullScreen(allow) method that takes a boolean parameter that\n * allows or disallows the use of full screening. This overrides the setting\n * from the allowFullScreen() method and is provided to allow parent UIs\n * such as Scratchpad to override the default settings of a child UI.\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', 'core/templates', 'core/notification'], function($, Templates, Notification) {\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 this.isFullScreenEnable = null;\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 * Set the value of the allowFullScreen property.\n * If the value is true, the fullscreen mode will be shown.\n * If the value is false, the fullscreen will be hidden.\n *\n * @param {Boolean} enableFullScreen The value to set.\n */\n InterfaceWrapper.prototype.setAllowFullScreen = function(enableFullScreen) {\n this.isFullScreenEnable = enableFullScreen;\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 let canDoFullScreen = t.isFullScreenEnable !== null ?\n t.isFullScreenEnable : uiInstance.allowFullScreen?.();\n if (canDoFullScreen) {\n t.initFullScreenToggle(t.taId);\n } else {\n t.removeFullScreenButton(t.taId);\n }\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Remove the fullscreen button from the wrapper editor.\n *\n * @param {String} fieldId The id of answer field.\n */\n InterfaceWrapper.prototype.removeFullScreenButton = function(fieldId) {\n const wrapperEditor = document.getElementById(`${fieldId}_wrapper`);\n const screenModeButton = wrapperEditor.parentNode.querySelector('.screen-mode-button');\n if (screenModeButton) {\n screenModeButton.remove();\n }\n };\n\n /**\n * Initialize elements and event listeners for the fullscreen mode.\n *\n * @param {String} fieldId The id of answer field.\n */\n InterfaceWrapper.prototype.initFullScreenToggle = function(fieldId) {\n const wrapperEditor = document.getElementById(`${fieldId}_wrapper`);\n const screenModeButton = wrapperEditor.parentNode.querySelector('.screen-mode-button');\n if (screenModeButton) {\n return;\n }\n\n Templates.renderForPromise('qtype_coderunner/screenmode_button', {}).then(({html}) => {\n const screenModeButton = Templates.appendNodeContents(wrapperEditor, html, '')[0];\n const fullscreenButton = screenModeButton.querySelector('.button-fullscreen');\n const exitFullscreenButton = screenModeButton.querySelector('.button-exit-fullscreen');\n\n // When load successfully, show the fullscreen button.\n fullscreenButton.classList.remove('d-none');\n\n // Add event listeners to the fullscreen/exit-fullscreen button.\n fullscreenButton.addEventListener('click', enterFullscreen.bind(this,\n fullscreenButton, exitFullscreenButton));\n exitFullscreenButton.addEventListener('click', exitFullscreen.bind(this));\n });\n\n /**\n * Make the editor fullscreen.\n *\n * @param {HTMLElement} fullscreenButton The fullscreen button.\n * @param {HTMLElement} exitFullscreenButton The exit fullscreen button.\n * @param {Event} e The click event.\n */\n function enterFullscreen(fullscreenButton, exitFullscreenButton, e) {\n let t = this;\n e.preventDefault();\n // The editor can stretch out.\n // So we need to save the original height and width of the editor before going fullscreen.\n t.wrapperHeight = t.wrapperNode.innerHeight();\n t.heightEditNode = t.hLast - t.GUTTER;\n t.widthEditNode = t.wLast;\n\n fullscreenButton.classList.add('d-none');\n // Append exit fullscreen button to the wrapper editor.\n // So that when in the fullscreen mode, the exit fullscreen button will be in the wrapper editor.\n wrapperEditor.append(exitFullscreenButton);\n\n // Handle fullscreen event.\n wrapperEditor.addEventListener('fullscreenchange', () => {\n if (document.fullscreenElement === null) {\n // When exit fullscreen using ESC key or press exit fullscreen button.\n // We need to reset the editor to the original size.\n t.uiInstance.resize(t.widthEditNode, t.heightEditNode);\n\n // We need to reset the wrapper height to the original height.\n // In fullscreen mode, the wrapper height can change by stretching it out.\n wrapperEditor.style.height = t.wrapperHeight + 'px';\n\n // Add and remove the d-none class to show and hide the buttons.\n exitFullscreenButton.classList.add('d-none');\n fullscreenButton.classList.remove('d-none');\n } else {\n exitFullscreenButton.classList.remove('d-none');\n }\n });\n wrapperEditor.requestFullscreen().catch(Notification.exception);\n }\n\n /**\n * Exit the fullscreen mode.\n *\n * @param {Event} e the click event.\n */\n function exitFullscreen(e) {\n let t = this;\n e.preventDefault();\n document.exitFullscreen();\n\n // Reset the editor to the original size before going fullscreen.\n wrapperEditor.style.height = t.wrapperHeight + 'px';\n t.uiInstance.resize(t.widthEditNode, t.heightEditNode);\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","$","Templates","Notification","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","isFullScreenEnable","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","setAllowFullScreen","enableFullScreen","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","allowFullScreen","_uiInstance$allowFull","initFullScreenToggle","removeFullScreenButton","fieldId","screenModeButton","parentNode","querySelector","remove","wrapperEditor","enterFullscreen","fullscreenButton","exitFullscreenButton","preventDefault","wrapperHeight","heightEditNode","widthEditNode","classList","add","addEventListener","fullscreenElement","style","height","requestFullscreen","catch","exception","exitFullscreen","renderForPromise","then","_ref","appendNodeContents","bind","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","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 5).\n *\n * 10. An allowFullScreen() method that returns True if the UI supports\n * use of the full-screen button in the bottom right of the UI wrapper.\n * Defaults to False if not implemented.\n *\n * 11. A setAllowFullScreen(allow) method that takes a boolean parameter that\n * allows or disallows the use of full screening. This overrides the setting\n * from the allowFullScreen() method and is provided to allow parent UIs\n * such as Scratchpad to override the default settings of a child UI.\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', 'core/templates', 'core/notification'], function($, Templates, Notification) {\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 = 16; // 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 this.isFullScreenEnable = null;\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 * Set the value of the allowFullScreen property.\n * If the value is true, the fullscreen mode will be shown.\n * If the value is false, the fullscreen will be hidden.\n *\n * @param {Boolean} enableFullScreen The value to set.\n */\n InterfaceWrapper.prototype.setAllowFullScreen = function(enableFullScreen) {\n this.isFullScreenEnable = enableFullScreen;\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 let canDoFullScreen = t.isFullScreenEnable !== null ?\n t.isFullScreenEnable : uiInstance.allowFullScreen?.();\n if (canDoFullScreen) {\n t.initFullScreenToggle(t.taId);\n } else {\n t.removeFullScreenButton(t.taId);\n }\n }\n t.isLoading = false;\n });\n }\n };\n\n\n /**\n * Remove the fullscreen button from the wrapper editor.\n *\n * @param {String} fieldId The id of answer field.\n */\n InterfaceWrapper.prototype.removeFullScreenButton = function(fieldId) {\n const wrapperEditor = document.getElementById(`${fieldId}_wrapper`);\n const screenModeButton = wrapperEditor.parentNode.querySelector('.screen-mode-button');\n if (screenModeButton) {\n screenModeButton.remove();\n }\n };\n\n /**\n * Initialize elements and event listeners for the fullscreen mode.\n *\n * @param {String} fieldId The id of answer field.\n */\n InterfaceWrapper.prototype.initFullScreenToggle = function(fieldId) {\n const wrapperEditor = document.getElementById(`${fieldId}_wrapper`);\n const screenModeButton = wrapperEditor.parentNode.querySelector('.screen-mode-button');\n if (screenModeButton) {\n return;\n }\n\n Templates.renderForPromise('qtype_coderunner/screenmode_button', {}).then(({html}) => {\n const screenModeButton = Templates.appendNodeContents(wrapperEditor, html, '')[0];\n const fullscreenButton = screenModeButton.querySelector('.button-fullscreen');\n const exitFullscreenButton = screenModeButton.querySelector('.button-exit-fullscreen');\n\n // When load successfully, show the fullscreen button.\n fullscreenButton.classList.remove('d-none');\n\n // Add event listeners to the fullscreen/exit-fullscreen button.\n fullscreenButton.addEventListener('click', enterFullscreen.bind(this,\n fullscreenButton, exitFullscreenButton));\n exitFullscreenButton.addEventListener('click', exitFullscreen.bind(this));\n });\n\n /**\n * Make the editor fullscreen.\n *\n * @param {HTMLElement} fullscreenButton The fullscreen button.\n * @param {HTMLElement} exitFullscreenButton The exit fullscreen button.\n * @param {Event} e The click event.\n */\n function enterFullscreen(fullscreenButton, exitFullscreenButton, e) {\n let t = this;\n e.preventDefault();\n // The editor can stretch out.\n // So we need to save the original height and width of the editor before going fullscreen.\n t.wrapperHeight = t.wrapperNode.innerHeight();\n t.heightEditNode = t.hLast - t.GUTTER;\n t.widthEditNode = t.wLast;\n\n fullscreenButton.classList.add('d-none');\n // Append exit fullscreen button to the wrapper editor.\n // So that when in the fullscreen mode, the exit fullscreen button will be in the wrapper editor.\n wrapperEditor.append(exitFullscreenButton);\n\n // Handle fullscreen event.\n wrapperEditor.addEventListener('fullscreenchange', () => {\n if (document.fullscreenElement === null) {\n // When exit fullscreen using ESC key or press exit fullscreen button.\n // We need to reset the editor to the original size.\n t.uiInstance.resize(t.widthEditNode, t.heightEditNode);\n\n // We need to reset the wrapper height to the original height.\n // In fullscreen mode, the wrapper height can change by stretching it out.\n wrapperEditor.style.height = t.wrapperHeight + 'px';\n\n // Add and remove the d-none class to show and hide the buttons.\n exitFullscreenButton.classList.add('d-none');\n fullscreenButton.classList.remove('d-none');\n } else {\n exitFullscreenButton.classList.remove('d-none');\n }\n });\n wrapperEditor.requestFullscreen().catch(Notification.exception);\n }\n\n /**\n * Exit the fullscreen mode.\n *\n * @param {Event} e the click event.\n */\n function exitFullscreen(e) {\n let t = this;\n e.preventDefault();\n document.exitFullscreen();\n\n // Reset the editor to the original size before going fullscreen.\n wrapperEditor.style.height = t.wrapperHeight + 'px';\n t.uiInstance.resize(t.widthEditNode, t.heightEditNode);\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","$","Templates","Notification","InterfaceWrapper","uiname","textareaId","t","this","GUTTER","DEFAULT_SYNC_INTERVAL_SECS","isFullScreenEnable","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","setAllowFullScreen","enableFullScreen","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","allowFullScreen","_uiInstance$allowFull","initFullScreenToggle","removeFullScreenButton","fieldId","screenModeButton","parentNode","querySelector","remove","wrapperEditor","enterFullscreen","fullscreenButton","exitFullscreenButton","preventDefault","wrapperHeight","heightEditNode","widthEditNode","classList","add","addEventListener","fullscreenElement","style","height","requestFullscreen","catch","exception","exitFullscreen","renderForPromise","then","_ref","appendNodeContents","bind","timeout","timer","setInterval","stopSyncTimer","clearTimeout","hasFocus","focus","selectionStart","value","removeClass","xLeft","offset","left","maxWidth","hAdjusted","wAdjusted","newUiWrapper"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/userinterfacewrapper.js b/amd/src/userinterfacewrapper.js
index 21a26b95..5e815c68 100644
--- a/amd/src/userinterfacewrapper.js
+++ b/amd/src/userinterfacewrapper.js
@@ -145,7 +145,7 @@ define(['jquery', 'core/templates', 'core/notification'], function($, Templates,
function InterfaceWrapper(uiname, textareaId) {
let t = this; // For use by embedded functions.
- this.GUTTER = 14; // Size of gutter at base of wrapper Node (pixels)
+ this.GUTTER = 16; // Size of gutter at base of wrapper Node (pixels)
this.DEFAULT_SYNC_INTERVAL_SECS = 5;
const PIXELS_PER_ROW = 19; // For estimating height of textareas.
diff --git a/styles.css b/styles.css
index 3007225d..38e5d24f 100644
--- a/styles.css
+++ b/styles.css
@@ -260,16 +260,12 @@ div.coderunner-test-results.partial {
/* Fullscreen and exit fullscreen */
.que.coderunner .button-exit-fullscreen,
body#page-question-type-coderunner .button-exit-fullscreen {
- bottom: -5px;
+ bottom: 0;
}
.que.coderunner .button-fullscreen,
body#page-question-type-coderunner .button-fullscreen {
- bottom: -6px;
-}
-
-body#page-question-type-coderunner .col-md-9 .button-fullscreen {
- margin-right: 15px;
+ bottom: -1px;
}
.que.coderunner .button-fullscreen,
@@ -278,11 +274,23 @@ body#page-question-type-coderunner .button-fullscreen,
body#page-question-type-coderunner .button-exit-fullscreen {
border: none;
background: transparent;
- margin-right: 4px;
- right: 12px;
+ margin: 0 0 2px 0;
+ height: 15px;
+ width: 20px;
+ padding: 0;
+ right: 15px;
z-index: 10;
}
+.que.coderunner .button-fullscreen:focus,
+.que.coderunner .button-exit-fullscreen:focus,
+body#page-question-type-coderunner .button-fullscreen:focus,
+body#page-question-type-coderunner .button-exit-fullscreen:focus {
+ display: flex;
+ justify-content: center;
+ outline: 2px solid black;
+}
+
.que.coderunner .button-fullscreen img.icon:hover,
.que.coderunner .button-exit-fullscreen img.icon:hover,
body#page-question-type-coderunner .button-fullscreen img.icon:hover,
@@ -302,7 +310,7 @@ body#page-question-type-coderunner .button-exit-fullscreen img.icon:hover {
body#page-question-type-coderunner .button-exit-fullscreen img.icon,
body#page-question-type-coderunner .button-fullscreen img.icon {
margin-right: unset;
- margin-bottom: 2px;
+ margin-bottom: 10px;
width: 15px;
height: 15px;
}
diff --git a/templates/screenmode_button.mustache b/templates/screenmode_button.mustache
index 6541267c..78d8450a 100644
--- a/templates/screenmode_button.mustache
+++ b/templates/screenmode_button.mustache
@@ -24,8 +24,8 @@
}
}}
-