diff --git a/amd/build/stackjsvle.min.js b/amd/build/stackjsvle.min.js index 1a0b7f669bfacff7c8a4c35836eadc38fd07fb69..ebe37d668d50df6f7baa627a8fd5a7caa38f0873 100644 --- a/amd/build/stackjsvle.min.js +++ b/amd/build/stackjsvle.min.js @@ -31,6 +31,6 @@ * @copyright 2023 Aalto University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("qtype_stack/stackjsvle",["core_filters/events"],(function(CustomEvents){let IFRAMES={},INPUTS={},INPUTS_INPUT_EVENT={},DISABLE_CHANGES=!1;function vle_get_element(id){let candidate=document.getElementById(id),iter=candidate;for(;iter&&!iter.classList.contains("formulation")&&!iter.classList.contains("outcome");)iter=iter.parentElement;return iter&&(iter.classList.contains("formulation")||iter.classList.contains("outcome"))?candidate:null}function vle_get_input_element(name,srciframe,outside){void 0===outside&&(outside=!0);let iter=document.getElementById(srciframe);for(;iter&&!iter.classList.contains("formulation")&&!iter.classList.contains("outcome");)iter=iter.parentElement;if(iter&&(iter.classList.contains("formulation")||iter.classList.contains("outcome"))){let possible=iter.querySelector('input[id$="_'+name+'"]');if(null!==possible)return possible;if(possible=iter.querySelector('textarea[id$="_'+name+'"]'),null!==possible)return possible;if(possible=iter.querySelector('input[id$="_'+name+'_1"][type=radio]'),null!==possible)return possible;if(possible=iter.querySelector('input[id$="_'+name+'_1"][type=checkbox]'),null!==possible)return possible;if(possible=iter.querySelector('select[id$="_'+name+'"]'),null!==possible)return possible}if(!outside)return null;let possible=document.querySelector('.formulation input[id$="_'+name+'"]');return null!==possible?possible:(possible=document.querySelector('.formulation textarea[id$="_'+name+'"]'),null!==possible?possible:(possible=document.querySelector('.formulation input[id$="_'+name+'_1"][type=radio]'),null!==possible?possible:(possible=document.querySelector('.formulation input[id$="_'+name+'_1"][type=checkbox]'),null!==possible?possible:(possible=document.querySelector('.formulation select[id$="_'+name+'"]'),null!==possible?possible:(possible=document.querySelector('.outcome input[id$="_'+name+'"]'),null!==possible?possible:(possible=document.querySelector('.outcome textarea[id$="_'+name+'"]'),null!==possible||(possible=document.querySelector('.outcome select[id$="_'+name+'"]')),possible))))))}function vle_get_others_of_same_input_group(input){return"radio"===input.type?document.querySelectorAll(".formulation input[name="+CSS.escape(input.name)+"]"):input.name.startsWith("q")&&input.name.indexOf(":")>-1&&input.name.endsWith("_1")?document.querySelectorAll(".formulation input[name^="+CSS.escape(input.name.substring(0,input.name.length-1))+"]"):document.querySelectorAll(".formulation input[name="+CSS.escape(input.name)+"]")}function vle_get_submit_button(srciframe){let iter=document.getElementById(srciframe);for(;iter&&!iter.classList.contains("formulation");)iter=iter.parentElement;if(iter&&iter.classList.contains("formulation")){return iter.querySelector('input[id$="-submit"][type=submit]')}return null}function vle_update_input(inputelement){const c=new Event("change");inputelement.dispatchEvent(c);const i=new Event("input");if(inputelement.dispatchEvent(i),"radio"===inputelement.type||"checkbox"===inputelement.type){const k=new Event("click");inputelement.dispatchEvent(k)}}function vle_update_dom(modifiedsubtreerootelement){CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement)}function is_evil_attribute(name,value){const lcname=name.toLowerCase();if(lcname.startsWith("on"))return!0;if("src"===lcname||lcname.endsWith("href")){const lcvalue=value.replace(/\s+/g,"").toLowerCase();if(lcvalue.includes("javascript:")||lcvalue.includes("data:text"))return!0}return!1}return window.addEventListener("message",(e=>{if(!("string"==typeof e.data||e.data instanceof String))return;let msg=null;try{msg=JSON.parse(e.data)}catch(e){return}if(!("version"in msg)||!msg.version.startsWith("STACK-JS"))return;if(!("src"in msg&&"type"in msg&&msg.src in IFRAMES))return;let element=null,input=null,response={version:"STACK-JS:1.3.0"};switch(msg.type){case"register-input-listener":if(input=vle_get_input_element(msg.name,msg.src,!msg["limit-to-question"]),null===input)return response.type="error",response.msg='Failed to connect to input: "'+msg.name+'"',response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");if(response.type="initial-input",response.name=msg.name,response.tgt=msg.src,"select"===input.nodeName.toLowerCase()?(response.value=input.value,response["input-type"]="select",response["input-readonly"]=input.hasAttribute("disabled")):"textarea"===input.nodeName.toLowerCase()?(response.value=input.value,response["input-type"]="textarea",response["input-readonly"]=input.hasAttribute("disabled")):"checkbox"===input.type?(response.value=input.checked,response["input-type"]="checkbox",response["input-readonly"]=input.hasAttribute("disabled")):(response.value=input.value,response["input-type"]=input.type,response["input-readonly"]=input.hasAttribute("readonly")),"radio"===input.type){response["input-readonly"]=input.hasAttribute("disabled"),response.value="";for(let inp of document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]"))inp.checked&&(response.value=inp.value)}if(input.id in INPUTS){if(msg.src in INPUTS[input.id])return;if("radio"!==input.type)INPUTS[input.id].push(msg.src);else{let radgroup=document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]");for(let inp of radgroup)INPUTS[inp.id].push(msg.src)}}else{if("radio"!==input.type)INPUTS[input.id]=[msg.src];else{let radgroup=document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]");for(let inp of radgroup)INPUTS[inp.id]=[msg.src]}if("radio"!==input.type)input.addEventListener("change",(()=>{if(DISABLE_CHANGES)return;let resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};"checkbox"===input.type?resp.value=input.checked:resp.value=input.value;for(let tgt of INPUTS[input.id])resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}));else{document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]").forEach((inp=>{inp.addEventListener("change",(()=>{if(DISABLE_CHANGES)return;let resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};if(inp.checked){resp.value=inp.value;for(let tgt of INPUTS[inp.id])resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}}))}))}}if("track-input"in msg&&msg["track-input"]&&"radio"!==input.type)if(input.id in INPUTS_INPUT_EVENT){if(msg.src in INPUTS_INPUT_EVENT[input.id])return;INPUTS_INPUT_EVENT[input.id].push(msg.src)}else INPUTS_INPUT_EVENT[input.id]=[msg.src],input.addEventListener("input",(()=>{if(DISABLE_CHANGES)return;let resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};"checkbox"===input.type?resp.value=input.checked:resp.value=input.value;for(let tgt of INPUTS_INPUT_EVENT[input.id])resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}));msg.src in INPUTS[input.id]||IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");break;case"changed-input":if(input=vle_get_input_element(msg.name,msg.src),null===input){const ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to modify input: "'+msg.name+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}DISABLE_CHANGES=!0,"checkbox"===input.type?input.checked=msg.value:input.value=msg.value,vle_update_input(input),DISABLE_CHANGES=!1,response.type="changed-input",response.name=msg.name,response.value=msg.value;for(let tgt of INPUTS[input.id])tgt!==msg.src&&(response.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(response),"*"));break;case"clear-input":if(input=vle_get_input_element(msg.name,msg.src),"select"===input.nodeName.toLowerCase()){-1!==input.selectedIndex&&(input.selectedIndex=-1,vle_update_input(input));for(var i=0;i<input.options.length;i++)input.options[i].hasAttribute("selected")&&(input.options[i].removeAttribute("selected"),vle_update_input(input)),""===input.options[i].value&&(input.options[i].selected=!0,vle_update_input(input))}else if("textarea"===input.nodeName.toLowerCase())""!==input.value&&(input.value="",vle_update_input(input));else if("checkbox"===input.type)for(let inp of vle_get_others_of_same_input_group(input))inp.checked=!1,vle_update_input(inp);else if("radio"===input.type)for(let inp of vle_get_others_of_same_input_group(input))inp.checked=""===inp.value,vle_update_input(inp);else""!==input.value&&(input.value="",vle_update_input(input));vle_update_input(input);break;case"register-button-listener":if(element=vle_get_element(msg.target),null===element){const ret={version:"STACK-JS:1.2.0",type:"error",msg:'Failed to find element: "'+msg.target+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}element.addEventListener("click",(event=>{let resp={version:"STACK-JS:1.2.0",type:"button-click",name:msg.target,tgt:msg.src};IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(resp),"*"),event.preventDefault()}));break;case"toggle-visibility":if(element=vle_get_element(msg.target),null===element){const ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to find element: "'+msg.target+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}"show"===msg.set?(element.style.display="block",vle_update_dom(element)):"hide"===msg.set&&(element.style.display="none");break;case"change-content":if(element=vle_get_element(msg.target),null===element)return response.type="error",response.msg='Failed to find element: "'+msg.target+'"',response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");element.innerHTML=function(src){let doc=(new DOMParser).parseFromString(src,"text/html");for(let el of doc.querySelectorAll("script, style"))el.remove();for(let el of doc.querySelectorAll("*"))for(let{name:name,value:value}of el.attributes)is_evil_attribute(name,value)&&el.removeAttribute(name);return doc.body}(msg.content).innerHTML,vle_update_dom(element);break;case"get-content":element=vle_get_element(msg.target),response.type="xfer-content",response.tgt=msg.src,response.target=msg.target,response.content=null,null!==element&&(response.content=element.innerHTML),IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");break;case"resize-frame":element=IFRAMES[msg.src].parentElement,element.style.width=msg.width,element.style.height=msg.height,IFRAMES[msg.src].style.width="100%",IFRAMES[msg.src].style.height="100%",vle_update_dom(element);break;case"ping":return response.type="ping",response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");case"query-submit-button":return response.type="submit-button-info",response.tgt=msg.src,input=vle_get_submit_button(msg.src),null===input||input.hasAttribute("hidden")?response.value=null:response.value=input.value,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");case"enable-submit-button":return input=vle_get_submit_button(msg.src),void(null!==input?msg.enabled?input.removeAttribute("disabled"):input.disabled=!0:(response.type="error",response.msg="Could not find matching submit button for this question.",response.tgt=msg.src,IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*")));case"relabel-submit-button":return input=vle_get_submit_button(msg.src),void(null!==input?input.value=msg.name:(response.type="error",response.msg="Could not find matching submit button for this question.",response.tgt=msg.src,IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*")));case"submit-button-info":case"initial-input":case"error":break;default:response.type="error",response.msg='Unknown message-type: "'+msg.type+'"',response.tgt=msg.src,IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*")}})),{create_iframe(iframeid,content,targetdivid,title,scrolling,evil){const frm=document.createElement("iframe");frm.id=iframeid,frm.style.width="100%",frm.style.height="100%",frm.style.border=0,!1===scrolling?(frm.scrolling="no",frm.style.overflow="hidden"):frm.scrolling="yes",frm.title=title,frm.referrerpolicy="no-referrer",evil||(frm.sandbox="allow-scripts allow-downloads"),frm.srcdoc=content,document.getElementById(targetdivid).replaceChildren(frm),IFRAMES[iframeid]=frm}}})); +define("qtype_stack/stackjsvle",["core_filters/events"],(function(CustomEvents){let IFRAMES={},INPUTS={},INPUTS_INPUT_EVENT={},DISABLE_CHANGES=!1;function vle_get_element(id){let candidate=document.getElementById(id),iter=candidate;for(;iter&&!iter.classList.contains("formulation")&&!iter.classList.contains("outcome");)iter=iter.parentElement;return iter&&(iter.classList.contains("formulation")||iter.classList.contains("outcome"))?candidate:null}function vle_get_input_element(name,srciframe,outside){void 0===outside&&(outside=!0);let iter=document.getElementById(srciframe);for(;iter&&!iter.classList.contains("formulation")&&!iter.classList.contains("outcome");)iter=iter.parentElement;if(iter&&(iter.classList.contains("formulation")||iter.classList.contains("outcome"))){let possible=iter.querySelector('input[id$="_'+name+'"]');if(null!==possible)return possible;if(possible=iter.querySelector('textarea[id$="_'+name+'"]'),null!==possible)return possible;if(possible=iter.querySelector('input[id$="_'+name+'_1"][type=radio]'),null!==possible)return possible;if(possible=iter.querySelector('input[id$="_'+name+'_1"][type=checkbox]'),null!==possible)return possible;if(possible=iter.querySelector('select[id$="_'+name+'"]'),null!==possible)return possible}if(!outside)return null;let possible=document.querySelector('.formulation input[id$="_'+name+'"]');return null!==possible?possible:(possible=document.querySelector('.formulation textarea[id$="_'+name+'"]'),null!==possible?possible:(possible=document.querySelector('.formulation input[id$="_'+name+'_1"][type=radio]'),null!==possible?possible:(possible=document.querySelector('.formulation input[id$="_'+name+'_1"][type=checkbox]'),null!==possible?possible:(possible=document.querySelector('.formulation select[id$="_'+name+'"]'),null!==possible?possible:(possible=document.querySelector('.outcome input[id$="_'+name+'"]'),null!==possible?possible:(possible=document.querySelector('.outcome textarea[id$="_'+name+'"]'),null!==possible||(possible=document.querySelector('.outcome select[id$="_'+name+'"]')),possible))))))}function vle_get_others_of_same_input_group(input){return"radio"===input.type?document.querySelectorAll(".formulation input[name="+CSS.escape(input.name)+"]"):input.name.startsWith("q")&&input.name.indexOf(":")>-1&&input.name.endsWith("_1")?document.querySelectorAll(".formulation input[name^="+CSS.escape(input.name.substring(0,input.name.length-1))+"]"):document.querySelectorAll(".formulation input[name="+CSS.escape(input.name)+"]")}function vle_get_submit_button(srciframe){let iter=document.getElementById(srciframe);for(;iter&&!iter.classList.contains("formulation");)iter=iter.parentElement;if(iter&&iter.classList.contains("formulation")){return iter.querySelector('.im-controls *[id$="-submit"][type=submit]')}return null}function vle_update_input(inputelement){const c=new Event("change");inputelement.dispatchEvent(c);const i=new Event("input");if(inputelement.dispatchEvent(i),"radio"===inputelement.type||"checkbox"===inputelement.type){const k=new Event("click");inputelement.dispatchEvent(k)}}function vle_update_dom(modifiedsubtreerootelement){CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement)}function is_evil_attribute(name,value){const lcname=name.toLowerCase();if(lcname.startsWith("on"))return!0;if("src"===lcname||lcname.endsWith("href")){const lcvalue=value.replace(/\s+/g,"").toLowerCase();if(lcvalue.includes("javascript:")||lcvalue.includes("data:text"))return!0}return!1}return window.addEventListener("message",(e=>{if(!("string"==typeof e.data||e.data instanceof String))return;let msg=null;try{msg=JSON.parse(e.data)}catch(e){return}if(!("version"in msg)||!msg.version.startsWith("STACK-JS"))return;if(!("src"in msg&&"type"in msg&&msg.src in IFRAMES))return;let element=null,input=null,response={version:"STACK-JS:1.3.0"};switch(msg.type){case"register-input-listener":if(input=vle_get_input_element(msg.name,msg.src,!msg["limit-to-question"]),null===input)return response.type="error",response.msg='Failed to connect to input: "'+msg.name+'"',response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");if(response.type="initial-input",response.name=msg.name,response.tgt=msg.src,"select"===input.nodeName.toLowerCase()?(response.value=input.value,response["input-type"]="select",response["input-readonly"]=input.hasAttribute("disabled")):"textarea"===input.nodeName.toLowerCase()?(response.value=input.value,response["input-type"]="textarea",response["input-readonly"]=input.hasAttribute("disabled")):"checkbox"===input.type?(response.value=input.checked,response["input-type"]="checkbox",response["input-readonly"]=input.hasAttribute("disabled")):(response.value=input.value,response["input-type"]=input.type,response["input-readonly"]=input.hasAttribute("readonly")),"radio"===input.type){response["input-readonly"]=input.hasAttribute("disabled"),response.value="";for(let inp of document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]"))inp.checked&&(response.value=inp.value)}if(input.id in INPUTS){if(msg.src in INPUTS[input.id])return;if("radio"!==input.type)INPUTS[input.id].push(msg.src);else{let radgroup=document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]");for(let inp of radgroup)INPUTS[inp.id].push(msg.src)}}else{if("radio"!==input.type)INPUTS[input.id]=[msg.src];else{let radgroup=document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]");for(let inp of radgroup)INPUTS[inp.id]=[msg.src]}if("radio"!==input.type)input.addEventListener("change",(()=>{if(DISABLE_CHANGES)return;let resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};"checkbox"===input.type?resp.value=input.checked:resp.value=input.value;for(let tgt of INPUTS[input.id])resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}));else{document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]").forEach((inp=>{inp.addEventListener("change",(()=>{if(DISABLE_CHANGES)return;let resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};if(inp.checked){resp.value=inp.value;for(let tgt of INPUTS[inp.id])resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}}))}))}}if("track-input"in msg&&msg["track-input"]&&"radio"!==input.type)if(input.id in INPUTS_INPUT_EVENT){if(msg.src in INPUTS_INPUT_EVENT[input.id])return;INPUTS_INPUT_EVENT[input.id].push(msg.src)}else INPUTS_INPUT_EVENT[input.id]=[msg.src],input.addEventListener("input",(()=>{if(DISABLE_CHANGES)return;let resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};"checkbox"===input.type?resp.value=input.checked:resp.value=input.value;for(let tgt of INPUTS_INPUT_EVENT[input.id])resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}));msg.src in INPUTS[input.id]||IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");break;case"changed-input":if(input=vle_get_input_element(msg.name,msg.src),null===input){const ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to modify input: "'+msg.name+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}DISABLE_CHANGES=!0,"checkbox"===input.type?input.checked=msg.value:input.value=msg.value,vle_update_input(input),DISABLE_CHANGES=!1,response.type="changed-input",response.name=msg.name,response.value=msg.value;for(let tgt of INPUTS[input.id])tgt!==msg.src&&(response.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(response),"*"));break;case"clear-input":if(input=vle_get_input_element(msg.name,msg.src),"select"===input.nodeName.toLowerCase()){-1!==input.selectedIndex&&(input.selectedIndex=-1,vle_update_input(input));for(var i=0;i<input.options.length;i++)input.options[i].hasAttribute("selected")&&(input.options[i].removeAttribute("selected"),vle_update_input(input)),""===input.options[i].value&&(input.options[i].selected=!0,vle_update_input(input))}else if("textarea"===input.nodeName.toLowerCase())""!==input.value&&(input.value="",vle_update_input(input));else if("checkbox"===input.type)for(let inp of vle_get_others_of_same_input_group(input))inp.checked=!1,vle_update_input(inp);else if("radio"===input.type)for(let inp of vle_get_others_of_same_input_group(input))inp.checked=""===inp.value,vle_update_input(inp);else""!==input.value&&(input.value="",vle_update_input(input));vle_update_input(input);break;case"register-button-listener":if(element=vle_get_element(msg.target),null===element){const ret={version:"STACK-JS:1.2.0",type:"error",msg:'Failed to find element: "'+msg.target+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}element.addEventListener("click",(event=>{let resp={version:"STACK-JS:1.2.0",type:"button-click",name:msg.target,tgt:msg.src};IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(resp),"*"),event.preventDefault()}));break;case"toggle-visibility":if(element=vle_get_element(msg.target),null===element){const ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to find element: "'+msg.target+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}"show"===msg.set?(element.style.display="block",vle_update_dom(element)):"hide"===msg.set&&(element.style.display="none");break;case"change-content":if(element=vle_get_element(msg.target),null===element)return response.type="error",response.msg='Failed to find element: "'+msg.target+'"',response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");element.innerHTML=function(src){let doc=(new DOMParser).parseFromString(src,"text/html");for(let el of doc.querySelectorAll("script, style"))el.remove();for(let el of doc.querySelectorAll("*"))for(let{name:name,value:value}of el.attributes)is_evil_attribute(name,value)&&el.removeAttribute(name);return doc.body}(msg.content).innerHTML,vle_update_dom(element);break;case"get-content":element=vle_get_element(msg.target),response.type="xfer-content",response.tgt=msg.src,response.target=msg.target,response.content=null,null!==element&&(response.content=element.innerHTML),IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");break;case"resize-frame":element=IFRAMES[msg.src].parentElement,element.style.width=msg.width,element.style.height=msg.height,IFRAMES[msg.src].style.width="100%",IFRAMES[msg.src].style.height="100%",vle_update_dom(element);break;case"ping":return response.type="ping",response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");case"query-submit-button":return response.type="submit-button-info",response.tgt=msg.src,input=vle_get_submit_button(msg.src),null===input||input.hasAttribute("hidden")?response.value=null:response.value=input.value,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");case"enable-submit-button":return input=vle_get_submit_button(msg.src),void(null!==input?msg.enabled?input.removeAttribute("disabled"):input.disabled=!0:(response.type="error",response.msg="Could not find matching submit button for this question.",response.tgt=msg.src,IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*")));case"relabel-submit-button":return input=vle_get_submit_button(msg.src),void(null!==input?(input.childNodes.length>1?input.childNodes.forEach((n=>{"#text"==n.nodeName&&(n.textContent=msg.name)})):input.innerHTML=msg.name,input.value=msg.name):(response.type="error",response.msg="Could not find matching submit button for this question.",response.tgt=msg.src,IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*")));case"submit-button-info":case"initial-input":case"error":break;default:response.type="error",response.msg='Unknown message-type: "'+msg.type+'"',response.tgt=msg.src,IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*")}})),{create_iframe(iframeid,content,targetdivid,title,scrolling,evil){const frm=document.createElement("iframe");frm.id=iframeid,frm.style.width="100%",frm.style.height="100%",frm.style.border=0,!1===scrolling?(frm.scrolling="no",frm.style.overflow="hidden"):frm.scrolling="yes",frm.title=title,frm.referrerpolicy="no-referrer",evil||(frm.sandbox="allow-scripts allow-downloads"),frm.srcdoc=content,document.getElementById(targetdivid).replaceChildren(frm),IFRAMES[iframeid]=frm}}})); //# sourceMappingURL=stackjsvle.min.js.map \ No newline at end of file diff --git a/amd/build/stackjsvle.min.js.map b/amd/build/stackjsvle.min.js.map index ee6944be67f78147bf0ad18ad7c568b3847a668e..c39554420f8ad12efde50a207e8bfc0388dae470 100644 --- a/amd/build/stackjsvle.min.js.map +++ b/amd/build/stackjsvle.min.js.map @@ -1 +1 @@ -{"version":3,"file":"stackjsvle.min.js","sources":["../src/stackjsvle.js"],"sourcesContent":["/**\n * A javascript module to handle separation of author sourced scripts into\n * IFRAMES. All such scripts will have limited access to the actual document\n * on the VLE side and this script represents the VLE side endpoint for\n * message handling needed to give that access. When porting STACK onto VLEs\n * one needs to map this script to do the following:\n *\n * 1. Ensure that searches for target elements/inputs are limited to questions\n * or their feedback and do not return any elements outside them.\n *\n * 2. Map any identifiers needed to identify inputs by name.\n *\n * 3. Any change handling related to input value modifications through this\n * logic gets connected to any such handling on the VLE side.\n *\n *\n * This script is intenttionally ordered so that the VLE specific bits should\n * be at the top.\n *\n *\n * This script assumes the following:\n *\n * 1. Each relevant IFRAME has an `id`-attribute that will be told to this\n * script.\n *\n * 2. Each such IFRAME exists within the question content itself, so that\n * one can traverse up the DOM tree from that IFRAME to find the border of\n * the question.\n *\n * @module qtype_stack/stackjsvle\n * @copyright 2023 Aalto University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'core_filters/events'\n], function(\n CustomEvents\n) {\n 'use strict';\n // Note the VLE specific include of logic.\n\n /* All the IFRAMES have unique identifiers that they give in their\n * messages. But we only work with those that have been created by\n * our logic and are found from this map.\n */\n let IFRAMES = {};\n\n /* For event handling, lists of IFRAMES listening particular inputs.\n */\n let INPUTS = {};\n\n /* For event handling, lists of IFRAMES listening particular inputs\n * and their input events. By default we only listen to changes.\n * We report input events as changes to the other side.\n */\n let INPUTS_INPUT_EVENT = {};\n\n /* A flag to disable certain things. */\n let DISABLE_CHANGES = false;\n\n\n /**\n * Returns an element with a given id, if an only if that element exists\n * inside a portion of DOM that represents a question or its feedback.\n *\n * If not found or exists outside the restricted area then returns `null`.\n *\n * @param {String} id the identifier of the element we want.\n */\n function vle_get_element(id) {\n /* In the case of Moodle we are happy as long as the element is inside\n something with the `formulation`-class. */\n let candidate = document.getElementById(id);\n let iter = candidate;\n while (iter && !iter.classList.contains('formulation') &&\n !iter.classList.contains('outcome')) {\n iter = iter.parentElement;\n }\n if (iter && (iter.classList.contains('formulation') ||\n iter.classList.contains('outcome'))) {\n return candidate;\n }\n\n return null;\n }\n\n /**\n * Returns an input element with a given name, if and only if that element\n * exists inside a portion of DOM that represents a question or its feedback.\n *\n * Note that, the input element may have a name that multiple questions\n * use and to pick the preferred element one needs to pick the one\n * within the same question as the IFRAME.\n *\n * Note that the input can also be a select. In the case of radio buttons\n * returning one of the possible buttons is enough.\n *\n * If not found or exists outside the restricted area then returns `null`.\n *\n * @param {String} name the name of the input we want\n * @param {String} srciframe the identifier of the iframe wanting it\n * @param {boolean} outside do we expand the search beyound the src question?\n */\n function vle_get_input_element(name, srciframe, outside) {\n /* In the case of Moodle we are happy as long as the element is inside\n something with the `formulation`-class. */\n if (outside === undefined) {\n // Old default was to search beyoudn the question.\n outside = true;\n }\n let initialcandidate = document.getElementById(srciframe);\n let iter = initialcandidate;\n while (iter && !iter.classList.contains('formulation') &&\n !iter.classList.contains('outcome')) {\n iter = iter.parentElement;\n }\n if (iter && (iter.classList.contains('formulation') ||\n iter.classList.contains('outcome'))) {\n // iter now represents the borders of the question containing\n // this IFRAME.\n let possible = iter.querySelector('input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n possible = iter.querySelector('textarea[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n // Radios have interesting ids, but the name makes sense\n possible = iter.querySelector('input[id$=\"_' + name + '_1\"][type=radio]');\n if (possible !== null) {\n return possible;\n }\n // Same for checkboxes, ntoe that non STACK checkbox can be targetted by\n // just the id using the topmost case here.\n possible = iter.querySelector('input[id$=\"_' + name + '_1\"][type=checkbox]');\n if (possible !== null) {\n return possible;\n }\n possible = iter.querySelector('select[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n }\n if (!outside) {\n return null;\n }\n // If none found within the question itself, search everywhere.\n let possible = document.querySelector('.formulation input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.formulation textarea[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n // Radios have interesting ids, but the name makes sense\n possible = document.querySelector('.formulation input[id$=\"_' + name + '_1\"][type=radio]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.formulation input[id$=\"_' + name + '_1\"][type=checkbox]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.formulation select[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n\n // Also search from within the feedback and other \"outcome\".\n // Note that we do not search for STACK sourced checkboxes from the feedback,\n // they do not exist there so simply finding them with the id is enough.\n possible = document.querySelector('.outcome input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.outcome textarea[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.outcome select[id$=\"_' + name + '\"]');\n return possible;\n }\n\n /**\n * Returns a list of input elements targetting the same thing.\n *\n * Note that STACK checkboxes have interesting naming for this.\n * And we assume we are getting the ones that `vle_get_input_element` would return.\n *\n * @param {element} input element of type=radio or type=checkbox\n */\n function vle_get_others_of_same_input_group(input) {\n if (input.type === 'radio') {\n return document.querySelectorAll('.formulation input[name=' + CSS.escape(input.name) + ']');\n }\n // Is it a Moodle input or a fake? If Moodle then assume STACK and its pattern.\n if (input.name.startsWith('q') && input.name.indexOf(':') > -1 && input.name.endsWith('_1')) {\n return document.querySelectorAll('.formulation input[name^=' +\n CSS.escape(input.name.substring(0, input.name.length - 1)) + ']');\n }\n return document.querySelectorAll('.formulation input[name=' + CSS.escape(input.name) + ']');\n }\n\n\n /**\n * Returns the input element or null for a question level submit button.\n * Basically, the \"Check\" button that behaviours like adaptive-mode in Moodle have.\n * Not all questions have such buttons, and the behaviour will affect that.\n *\n * Will only return the button of the question containing that iframe.\n *\n * @param {String} srciframe the identifier of the iframe wanting it\n */\n function vle_get_submit_button(srciframe) {\n let initialcandidate = document.getElementById(srciframe);\n let iter = initialcandidate;\n // Note the submit button is most definitely not within \"outcome\".\n while (iter && !iter.classList.contains('formulation')) {\n iter = iter.parentElement;\n }\n if (iter && iter.classList.contains('formulation')) {\n // iter now represents the borders of the question containing\n // this IFRAME.\n // In Moodle inputs that are behaviour variables use `-` as a separator\n // for the name and usage id.\n let possible = iter.querySelector('input[id$=\"-submit\"][type=submit]');\n return possible;\n }\n return null;\n }\n\n /**\n * Triggers any VLE specific scripting related to updates of the given\n * input element.\n *\n * @param {HTMLElement} inputelement the input element that has changed\n */\n function vle_update_input(inputelement) {\n // Triggering a change event may be necessary.\n const c = new Event('change');\n inputelement.dispatchEvent(c);\n // Also there are those that listen to input events.\n const i = new Event('input');\n inputelement.dispatchEvent(i);\n if (inputelement.type === 'radio' || inputelement.type === 'checkbox') {\n const k = new Event('click');\n inputelement.dispatchEvent(k);\n }\n }\n\n /**\n * Triggers any VLE specific scripting related to DOM updates.\n *\n * @param {HTMLElement} modifiedsubtreerootelement element under which changes may have happened.\n */\n function vle_update_dom(modifiedsubtreerootelement) {\n CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement);\n }\n\n /**\n * Does HTML-string cleaning, i.e., removes any script payload. Returns\n * a DOM version of the given input string. The DOM version returned is\n * an element of some sort containing the contents, possibly a `body`.\n *\n * This is used when receiving replacement content for a div.\n *\n * @param {String} src a raw string to sanitise\n */\n function vle_html_sanitize(src) {\n // This can be implemented with many libraries or by custom code\n // however as this is typically a thing that a VLE might already have\n // tools for we have it at this level so that the VLE can use its own\n // tools that do things that the VLE developpers consider safe.\n\n // As Moodle does not currently seem to have such a sanitizer in\n // the core libraries, here is one implementation that shows what we\n // are looking for.\n\n // TO-DO: look into replacing this with DOMPurify or some such.\n\n let parser = new DOMParser();\n let doc = parser.parseFromString(src, \"text/html\");\n\n // First remove all <script> tags. Also <style> as we do not want\n // to include too much style.\n for (let el of doc.querySelectorAll('script, style')) {\n el.remove();\n }\n\n // Check all elements for attributes.\n for (let el of doc.querySelectorAll('*')) {\n for (let {name, value} of el.attributes) {\n if (is_evil_attribute(name, value)) {\n el.removeAttribute(name);\n }\n }\n }\n\n return doc.body;\n }\n\n /**\n * Utility function trying to determine if a given attribute is evil\n * when sanitizing HTML-fragments.\n *\n * @param {String} name the name of an attribute.\n * @param {String} value the value of an attribute.\n */\n function is_evil_attribute(name, value) {\n const lcname = name.toLowerCase();\n if (lcname.startsWith('on')) {\n // We do not allow event listeners to be defined.\n return true;\n }\n if (lcname === 'src' || lcname.endsWith('href')) {\n // Do not allow certain things in the urls.\n const lcvalue = value.replace(/\\s+/g, '').toLowerCase();\n // Ignore es-lint false positive.\n /* eslint-disable no-script-url */\n if (lcvalue.includes('javascript:') || lcvalue.includes('data:text')) {\n return true;\n }\n }\n\n return false;\n }\n\n\n /*************************************************************************\n * Above this are the bits that one would probably tune when porting.\n *\n * Below is the actuall message handling and it should be left alone.\n */\n window.addEventListener(\"message\", (e) => {\n // NOTE! We do not check the source or origin of the message in\n // the normal way. All actions that can bypass our filters to trigger\n // something are largely irrelevant and all traffic will be kept\n // \"safe\" as anyone could be listening.\n\n // All messages we receive are strings, anything else is for someone\n // else and will be ignored.\n if (!(typeof e.data === 'string' || e.data instanceof String)) {\n return;\n }\n\n // That string is a JSON encoded dictionary.\n let msg = null;\n try {\n msg = JSON.parse(e.data);\n } catch (e) {\n // Only JSON objects that are parseable will work.\n return;\n }\n\n // All messages we handle contain a version field with a particular\n // value, for now we leave the possibility open for that value to have\n // an actual version number suffix...\n if (!(('version' in msg) && msg.version.startsWith('STACK-JS'))) {\n return;\n }\n\n // All messages we handle must have a source and a type,\n // and that source must be one of the registered ones.\n if (!(('src' in msg) && ('type' in msg) && (msg.src in IFRAMES))) {\n return;\n }\n let element = null;\n let input = null;\n\n let response = {\n version: 'STACK-JS:1.3.0'\n };\n\n switch (msg.type) {\n case 'register-input-listener':\n // 1. Find the input.\n input = vle_get_input_element(msg.name, msg.src, !msg['limit-to-question']);\n\n if (input === null) {\n // Requested something that is not available.\n response.type = 'error';\n response.msg = 'Failed to connect to input: \"' + msg.name + '\"';\n response.tgt = msg.src;\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n return;\n }\n\n response.type = 'initial-input';\n response.name = msg.name;\n response.tgt = msg.src;\n\n // 2. What type of an input is this? Note that we do not\n // currently support all types in sensible ways. In particular,\n // anything with multiple values will be a problem.\n if (input.nodeName.toLowerCase() === 'select') {\n response.value = input.value;\n response['input-type'] = 'select';\n response['input-readonly'] = input.hasAttribute('disabled');\n } else if (input.nodeName.toLowerCase() === 'textarea') {\n response.value = input.value;\n response['input-type'] = 'textarea';\n response['input-readonly'] = input.hasAttribute('disabled');\n } else if (input.type === 'checkbox') {\n response.value = input.checked;\n response['input-type'] = 'checkbox';\n response['input-readonly'] = input.hasAttribute('disabled');\n } else {\n response.value = input.value;\n response['input-type'] = input.type;\n response['input-readonly'] = input.hasAttribute('readonly');\n }\n if (input.type === 'radio') {\n response['input-readonly'] = input.hasAttribute('disabled');\n response.value = '';\n for (let inp of document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']')) {\n if (inp.checked) {\n response.value = inp.value;\n }\n }\n }\n\n // 3. Add listener for changes of this input.\n if (input.id in INPUTS) {\n if (msg.src in INPUTS[input.id]) {\n // DO NOT BIND TWICE!\n return;\n }\n if (input.type !== 'radio') {\n INPUTS[input.id].push(msg.src);\n } else {\n let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');\n for (let inp of radgroup) {\n INPUTS[inp.id].push(msg.src);\n }\n }\n } else {\n if (input.type !== 'radio') {\n INPUTS[input.id] = [msg.src];\n } else {\n let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');\n for (let inp of radgroup) {\n INPUTS[inp.id] = [msg.src];\n }\n }\n if (input.type !== 'radio') {\n input.addEventListener('change', () => {\n if (DISABLE_CHANGES) {\n return;\n }\n let resp = {\n version: 'STACK-JS:1.0.0',\n type: 'changed-input',\n name: msg.name\n };\n if (input.type === 'checkbox') {\n resp['value'] = input.checked;\n } else {\n resp['value'] = input.value;\n }\n for (let tgt of INPUTS[input.id]) {\n resp['tgt'] = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');\n }\n });\n } else {\n // Assume that if we received a radio button that is safe\n // then all its friends are also safe.\n let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');\n radgroup.forEach((inp) => {\n inp.addEventListener('change', () => {\n if (DISABLE_CHANGES) {\n return;\n }\n let resp = {\n version: 'STACK-JS:1.0.0',\n type: 'changed-input',\n name: msg.name\n };\n if (inp.checked) {\n resp.value = inp.value;\n } else {\n // What about unsetting?\n return;\n }\n for (let tgt of INPUTS[inp.id]) {\n resp['tgt'] = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');\n }\n });\n });\n }\n }\n\n if (('track-input' in msg) && msg['track-input'] && input.type !== 'radio') {\n if (input.id in INPUTS_INPUT_EVENT) {\n if (msg.src in INPUTS_INPUT_EVENT[input.id]) {\n // DO NOT BIND TWICE!\n return;\n }\n INPUTS_INPUT_EVENT[input.id].push(msg.src);\n } else {\n INPUTS_INPUT_EVENT[input.id] = [msg.src];\n\n input.addEventListener('input', () => {\n if (DISABLE_CHANGES) {\n return;\n }\n let resp = {\n version: 'STACK-JS:1.0.0',\n type: 'changed-input',\n name: msg.name\n };\n if (input.type === 'checkbox') {\n resp['value'] = input.checked;\n } else {\n resp['value'] = input.value;\n }\n for (let tgt of INPUTS_INPUT_EVENT[input.id]) {\n resp['tgt'] = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');\n }\n });\n }\n }\n\n // 4. Let the requester know that we have bound things\n // and let it know the initial value.\n if (!(msg.src in INPUTS[input.id])) {\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n\n break;\n case 'changed-input':\n // 1. Find the input.\n input = vle_get_input_element(msg.name, msg.src);\n\n if (input === null) {\n // Requested something that is not available.\n const ret = {\n version: 'STACK-JS:1.0.0',\n type: 'error',\n msg: 'Failed to modify input: \"' + msg.name + '\"',\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');\n return;\n }\n\n // Disable change events.\n DISABLE_CHANGES = true;\n\n // TO-DO: Radio buttons should we check that value is possible?\n if (input.type === 'checkbox') {\n input.checked = msg.value;\n } else {\n input.value = msg.value;\n }\n\n // Trigger VLE side actions.\n vle_update_input(input);\n\n // Enable change tracking.\n DISABLE_CHANGES = false;\n\n // Tell all other frames, that care, about this.\n response.type = 'changed-input';\n response.name = msg.name;\n response.value = msg.value;\n\n for (let tgt of INPUTS[input.id]) {\n if (tgt !== msg.src) {\n response.tgt = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n }\n break;\n case 'clear-input':\n // 1. Find the input.\n input = vle_get_input_element(msg.name, msg.src);\n\n if (input.nodeName.toLowerCase() === 'select') {\n if (input.selectedIndex !== -1) {\n input.selectedIndex = -1;\n vle_update_input(input);\n }\n for(var i = 0; i < input.options.length; i++) {\n if (input.options[i].hasAttribute('selected')) {\n input.options[i].removeAttribute('selected');\n vle_update_input(input);\n }\n if (input.options[i].value === '') {\n // If we have the clear input option select that.\n input.options[i].selected = true;\n vle_update_input(input);\n }\n }\n } else if (input.nodeName.toLowerCase() === 'textarea') {\n if (input.value !== '') {\n input.value = '';\n vle_update_input(input);\n }\n } else if (input.type === 'checkbox') {\n for (let inp of vle_get_others_of_same_input_group(input)) {\n inp.checked = false;\n vle_update_input(inp);\n }\n } else if (input.type === 'radio') {\n for (let inp of vle_get_others_of_same_input_group(input)) {\n // If we have the clear value option select that.\n inp.checked = inp.value === '';\n vle_update_input(inp);\n }\n } else {\n if (input.value !== '') {\n input.value = '';\n vle_update_input(input);\n }\n }\n\n vle_update_input(input);\n break;\n case 'register-button-listener':\n // 1. Find the element.\n element = vle_get_element(msg.target);\n\n if (element === null) {\n // Requested something that is not available.\n const ret = {\n version: 'STACK-JS:1.2.0',\n type: 'error',\n msg: 'Failed to find element: \"' + msg.target + '\"',\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');\n return;\n }\n\n // 2. Add a listener, no need to do anything more\n // complicated than this.\n element.addEventListener('click', (event) => {\n let resp = {\n version: 'STACK-JS:1.2.0',\n type: 'button-click',\n name: msg.target,\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(resp), '*');\n // These listeners will stop the submissions of buttons which might be a problem.\n event.preventDefault();\n });\n\n break;\n case 'toggle-visibility':\n // 1. Find the element.\n element = vle_get_element(msg.target);\n\n if (element === null) {\n // Requested something that is not available.\n const ret = {\n version: 'STACK-JS:1.0.0',\n type: 'error',\n msg: 'Failed to find element: \"' + msg.target + '\"',\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');\n return;\n }\n\n // 2. Toggle display setting.\n if (msg.set === 'show') {\n element.style.display = 'block';\n // If we make something visible we should let the VLE know about it.\n vle_update_dom(element);\n } else if (msg.set === 'hide') {\n element.style.display = 'none';\n }\n\n break;\n case 'change-content':\n // 1. Find the element.\n element = vle_get_element(msg.target);\n\n if (element === null) {\n // Requested something that is not available.\n response.type = 'error';\n response.msg = 'Failed to find element: \"' + msg.target + '\"';\n response.tgt = msg.src;\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n return;\n }\n\n // 2. Secure content.\n // 3. Switch the content. Note the contents coming from `vle_html_sanitize`\n // are wrapped in an element possibly `<body>` and will need to be unwrapped.\n // We can simply use innerHTML here to also disconnect the content from\n // whatever it was before being sanitized.\n element.innerHTML = vle_html_sanitize(msg.content).innerHTML;\n // If we tune something we should let the VLE know about it.\n vle_update_dom(element);\n\n break;\n case 'get-content':\n // 1. Find the element.\n element = vle_get_element(msg.target);\n // 2. Build the message.\n response.type = 'xfer-content';\n response.tgt = msg.src;\n response.target = msg.target;\n response.content = null;\n if (element !== null) {\n // TO-DO: Should we sanitise the content? Probably not as using\n // this to interrogate neighbouring questions only allows\n // messing with the other questions and not anything outside\n // them. If we do not sanitise it we allow some interesting\n // question-analytics tooling, and if we do we really don't\n // gain anything sensible.\n // Matti's opinnion is to not sanitise at this point as\n // interraction between questions is not inherently evil\n // and could be of use even at the level of reading code from\n // from other questions.\n response.content = element.innerHTML;\n }\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n break;\n case 'resize-frame':\n // 1. Find the frames wrapper div.\n element = IFRAMES[msg.src].parentElement;\n\n // 2. Set the wrapper size.\n element.style.width = msg.width;\n element.style.height = msg.height;\n\n // 3. Reset the frame size.\n IFRAMES[msg.src].style.width = '100%';\n IFRAMES[msg.src].style.height = '100%';\n\n // Only touching the size but still let the VLE know.\n vle_update_dom(element);\n break;\n case 'ping':\n // This is for testing the connection. The other end will\n // send these untill it receives a reply.\n // Part of the logic for startup.\n response.type = 'ping';\n response.tgt = msg.src;\n\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n return;\n case 'query-submit-button':\n response.type = 'submit-button-info';\n response.tgt = msg.src;\n input = vle_get_submit_button(msg.src);\n if (input === null || input.hasAttribute('hidden')) {\n response['value'] = null;\n } else {\n response['value'] = input.value;\n }\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n return;\n case 'enable-submit-button':\n input = vle_get_submit_button(msg.src);\n if (input !== null) {\n if (msg.enabled) {\n input.removeAttribute('disabled');\n } else {\n input.disabled = true;\n }\n } else {\n // We generate this error just to push people to properly check if\n // the button even exists before trying to tune it.\n response.type = 'error';\n response.msg = 'Could not find matching submit button for this question.';\n response.tgt = msg.src;\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n return;\n case 'relabel-submit-button':\n input = vle_get_submit_button(msg.src);\n if (input !== null) {\n input.value = msg.name;\n } else {\n // We generate this error just to push people to properly check if\n // the button even exists before trying to tune it.\n response.type = 'error';\n response.msg = 'Could not find matching submit button for this question.';\n response.tgt = msg.src;\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n return;\n case 'submit-button-info':\n case 'initial-input':\n case 'error':\n // These message types are for the other end.\n break;\n\n default:\n // If we see something unexpected, lets let the other end know\n // and make sure that they know our version. Could be that this\n // end has not been upgraded.\n response.type = 'error';\n response.msg = 'Unknown message-type: \"' + msg.type + '\"';\n response.tgt = msg.src;\n\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n\n });\n\n\n return {\n /* To avoid any logic that forbids IFRAMEs in the VLE output one can\n also create and register that IFRAME through this logic. This\n also ensures that all relevant security settigns for that IFRAME\n have been correctly tuned.\n\n Here the IDs are for the secrect identifier that may be present\n inside the content of that IFRAME and for the question that contains\n it. One also identifies a DIV element that marks the position of\n the IFRAME and limits the size of the IFRAME (all IFRAMEs this\n creates will be 100% x 100%).\n\n @param {String} iframeid the id that the IFRAME has stored inside\n it and uses for communication.\n @param {String} the full HTML content of that IFRAME.\n @param {String} targetdivid the id of the element (div) that will\n hold the IFRAME.\n @param {String} title a descriptive name for the iframe.\n @param {bool} scrolling whether we have overflow:scroll or\n overflow:hidden.\n @param {bool} evil allows certain special cases to act without\n sandboxing, this is a feature that will be removed so do\n not rely on it only use it to test STACK-JS before you get your\n thing to run in a sandbox.\n */\n create_iframe(iframeid, content, targetdivid, title, scrolling, evil) {\n const frm = document.createElement('iframe');\n frm.id = iframeid;\n frm.style.width = '100%';\n frm.style.height = '100%';\n frm.style.border = 0;\n if (scrolling === false) {\n frm.scrolling = 'no';\n frm.style.overflow = 'hidden';\n } else {\n frm.scrolling = 'yes';\n }\n frm.title = title;\n // Somewhat random limitation.\n frm.referrerpolicy = 'no-referrer';\n // We include that allow-downloads as an example of XLS-\n // document building in JS has been seen.\n // UNDER NO CIRCUMSTANCES DO WE ALLOW-SAME-ORIGIN!\n // That would defeat the whole point of this.\n if (!evil) {\n frm.sandbox = 'allow-scripts allow-downloads';\n }\n\n // As the SOP is intentionally broken we need to allow\n // scripts from everywhere.\n\n // NOTE: this bit commented out as long as the csp-attribute\n // is not supported by more browsers.\n // frm.csp = \"script-src: 'unsafe-inline' 'self' '*';\";\n // frm.csp = \"script-src: 'unsafe-inline' 'self' '*';img-src: '*';\";\n\n // Plug the content into the frame.\n frm.srcdoc = content;\n\n // The target DIV will have its children removed.\n // This allows that div to contain some sort of loading\n // indicator until we plug in the frame.\n // Naturally the frame will then start to load itself.\n document.getElementById(targetdivid).replaceChildren(frm);\n IFRAMES[iframeid] = frm;\n }\n\n };\n});"],"names":["define","CustomEvents","IFRAMES","INPUTS","INPUTS_INPUT_EVENT","DISABLE_CHANGES","vle_get_element","id","candidate","document","getElementById","iter","classList","contains","parentElement","vle_get_input_element","name","srciframe","outside","undefined","possible","querySelector","vle_get_others_of_same_input_group","input","type","querySelectorAll","CSS","escape","startsWith","indexOf","endsWith","substring","length","vle_get_submit_button","vle_update_input","inputelement","c","Event","dispatchEvent","i","k","vle_update_dom","modifiedsubtreerootelement","notifyFilterContentUpdated","is_evil_attribute","value","lcname","toLowerCase","lcvalue","replace","includes","window","addEventListener","e","data","String","msg","JSON","parse","version","src","element","response","tgt","contentWindow","postMessage","stringify","nodeName","hasAttribute","checked","inp","push","radgroup","resp","forEach","ret","selectedIndex","options","removeAttribute","selected","target","event","preventDefault","set","style","display","innerHTML","doc","DOMParser","parseFromString","el","remove","attributes","body","vle_html_sanitize","content","width","height","enabled","disabled","create_iframe","iframeid","targetdivid","title","scrolling","evil","frm","createElement","border","overflow","referrerpolicy","sandbox","srcdoc","replaceChildren"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCAA,gCAAO,CACH,wBACD,SACCC,kBASIC,QAAU,GAIVC,OAAS,GAMTC,mBAAqB,GAGrBC,iBAAkB,WAWbC,gBAAgBC,QAGjBC,UAAYC,SAASC,eAAeH,IACpCI,KAAOH,eACJG,OAASA,KAAKC,UAAUC,SAAS,iBAChCF,KAAKC,UAAUC,SAAS,YAC5BF,KAAOA,KAAKG,qBAEZH,OAASA,KAAKC,UAAUC,SAAS,gBACjCF,KAAKC,UAAUC,SAAS,YACjBL,UAGJ,cAoBFO,sBAAsBC,KAAMC,UAAWC,cAG5BC,IAAZD,UAEAA,SAAU,OAGVP,KADmBF,SAASC,eAAeO,gBAExCN,OAASA,KAAKC,UAAUC,SAAS,iBAChCF,KAAKC,UAAUC,SAAS,YAC5BF,KAAOA,KAAKG,iBAEZH,OAASA,KAAKC,UAAUC,SAAS,gBACjCF,KAAKC,UAAUC,SAAS,YAAa,KAGjCO,SAAWT,KAAKU,cAAc,eAAiBL,KAAO,SACzC,OAAbI,gBACOA,YAEXA,SAAWT,KAAKU,cAAc,kBAAoBL,KAAO,MACxC,OAAbI,gBACOA,YAGXA,SAAWT,KAAKU,cAAc,eAAiBL,KAAO,oBACrC,OAAbI,gBACOA,YAIXA,SAAWT,KAAKU,cAAc,eAAiBL,KAAO,uBACrC,OAAbI,gBACOA,YAEXA,SAAWT,KAAKU,cAAc,gBAAkBL,KAAO,MACtC,OAAbI,gBACOA,aAGVF,eACM,SAGPE,SAAWX,SAASY,cAAc,4BAA8BL,KAAO,aAC1D,OAAbI,SACOA,UAEXA,SAAWX,SAASY,cAAc,+BAAiCL,KAAO,MACzD,OAAbI,SACOA,UAGXA,SAAWX,SAASY,cAAc,4BAA8BL,KAAO,oBACtD,OAAbI,SACOA,UAEXA,SAAWX,SAASY,cAAc,4BAA8BL,KAAO,uBACtD,OAAbI,SACOA,UAEXA,SAAWX,SAASY,cAAc,6BAA+BL,KAAO,MACvD,OAAbI,SACOA,UAMXA,SAAWX,SAASY,cAAc,wBAA0BL,KAAO,MAClD,OAAbI,SACOA,UAEXA,SAAWX,SAASY,cAAc,2BAA6BL,KAAO,MACrD,OAAbI,WAGJA,SAAWX,SAASY,cAAc,yBAA2BL,KAAO,OAFzDI,wBAcNE,mCAAmCC,aACrB,UAAfA,MAAMC,KACCf,SAASgB,iBAAiB,2BAA6BC,IAAIC,OAAOJ,MAAMP,MAAQ,KAGvFO,MAAMP,KAAKY,WAAW,MAAQL,MAAMP,KAAKa,QAAQ,MAAQ,GAAKN,MAAMP,KAAKc,SAAS,MAC3ErB,SAASgB,iBAAiB,4BAC7BC,IAAIC,OAAOJ,MAAMP,KAAKe,UAAU,EAAGR,MAAMP,KAAKgB,OAAS,IAAM,KAE9DvB,SAASgB,iBAAiB,2BAA6BC,IAAIC,OAAOJ,MAAMP,MAAQ,cAalFiB,sBAAsBhB,eAEvBN,KADmBF,SAASC,eAAeO,gBAGxCN,OAASA,KAAKC,UAAUC,SAAS,gBACpCF,KAAOA,KAAKG,iBAEZH,MAAQA,KAAKC,UAAUC,SAAS,eAAgB,QAKjCF,KAAKU,cAAc,4CAG/B,cASFa,iBAAiBC,oBAEhBC,EAAI,IAAIC,MAAM,UACpBF,aAAaG,cAAcF,SAErBG,EAAI,IAAIF,MAAM,YACpBF,aAAaG,cAAcC,GACD,UAAtBJ,aAAaX,MAA0C,aAAtBW,aAAaX,KAAqB,OAC7DgB,EAAI,IAAIH,MAAM,SACpBF,aAAaG,cAAcE,aAS1BC,eAAeC,4BACpBzC,aAAa0C,2BAA2BD,qCAoDnCE,kBAAkB5B,KAAM6B,aACvBC,OAAS9B,KAAK+B,iBAChBD,OAAOlB,WAAW,aAEX,KAEI,QAAXkB,QAAoBA,OAAOhB,SAAS,QAAS,OAEvCkB,QAAUH,MAAMI,QAAQ,OAAQ,IAAIF,iBAGtCC,QAAQE,SAAS,gBAAkBF,QAAQE,SAAS,oBAC7C,SAIR,SASXC,OAAOC,iBAAiB,WAAYC,SAQR,iBAAXA,EAAEC,MAAqBD,EAAEC,gBAAgBC,mBAKlDC,IAAM,SAENA,IAAMC,KAAKC,MAAML,EAAEC,MACrB,MAAOD,eAQF,YAAaG,OAAQA,IAAIG,QAAQ/B,WAAW,wBAM5C,QAAS4B,KAAS,SAAUA,KAASA,IAAII,OAAO1D,oBAGnD2D,QAAU,KACVtC,MAAQ,KAERuC,SAAW,CACXH,QAAS,yBAGLH,IAAIhC,UACP,6BAEDD,MAAQR,sBAAsByC,IAAIxC,KAAMwC,IAAII,KAAMJ,IAAI,sBAExC,OAAVjC,aAEAuC,SAAStC,KAAO,QAChBsC,SAASN,IAAM,gCAAkCA,IAAIxC,KAAO,IAC5D8C,SAASC,IAAMP,IAAII,SACnB1D,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUJ,UAAW,QAIzEA,SAAStC,KAAO,gBAChBsC,SAAS9C,KAAOwC,IAAIxC,KACpB8C,SAASC,IAAMP,IAAII,IAKkB,WAAjCrC,MAAM4C,SAASpB,eACfe,SAASjB,MAAQtB,MAAMsB,MACvBiB,SAAS,cAAgB,SACzBA,SAAS,kBAAoBvC,MAAM6C,aAAa,aACR,aAAjC7C,MAAM4C,SAASpB,eACtBe,SAASjB,MAAQtB,MAAMsB,MACvBiB,SAAS,cAAgB,WACzBA,SAAS,kBAAoBvC,MAAM6C,aAAa,aAC1B,aAAf7C,MAAMC,MACbsC,SAASjB,MAAQtB,MAAM8C,QACvBP,SAAS,cAAgB,WACzBA,SAAS,kBAAoBvC,MAAM6C,aAAa,cAEhDN,SAASjB,MAAQtB,MAAMsB,MACvBiB,SAAS,cAAgBvC,MAAMC,KAC/BsC,SAAS,kBAAoBvC,MAAM6C,aAAa,aAEjC,UAAf7C,MAAMC,KAAkB,CACxBsC,SAAS,kBAAoBvC,MAAM6C,aAAa,YAChDN,SAASjB,MAAQ,OACZ,IAAIyB,OAAO7D,SAASgB,iBAAiB,0BAA4BC,IAAIC,OAAOJ,MAAMP,MAAQ,KACvFsD,IAAID,UACJP,SAASjB,MAAQyB,IAAIzB,UAM7BtB,MAAMhB,MAAMJ,OAAQ,IAChBqD,IAAII,OAAOzD,OAAOoB,MAAMhB,cAIT,UAAfgB,MAAMC,KACNrB,OAAOoB,MAAMhB,IAAIgE,KAAKf,IAAII,SACvB,KACCY,SAAW/D,SAASgB,iBAAiB,0BAA4BC,IAAIC,OAAOJ,MAAMP,MAAQ,SACzF,IAAIsD,OAAOE,SACZrE,OAAOmE,IAAI/D,IAAIgE,KAAKf,IAAII,UAG7B,IACgB,UAAfrC,MAAMC,KACNrB,OAAOoB,MAAMhB,IAAM,CAACiD,IAAII,SACrB,KACCY,SAAW/D,SAASgB,iBAAiB,0BAA4BC,IAAIC,OAAOJ,MAAMP,MAAQ,SACzF,IAAIsD,OAAOE,SACZrE,OAAOmE,IAAI/D,IAAM,CAACiD,IAAII,QAGX,UAAfrC,MAAMC,KACND,MAAM6B,iBAAiB,UAAU,QACzB/C,2BAGAoE,KAAO,CACPd,QAAS,iBACTnC,KAAM,gBACNR,KAAMwC,IAAIxC,MAEK,aAAfO,MAAMC,KACNiD,KAAI,MAAYlD,MAAM8C,QAEtBI,KAAI,MAAYlD,MAAMsB,UAErB,IAAIkB,OAAO5D,OAAOoB,MAAMhB,IACzBkE,KAAI,IAAUV,IACd7D,QAAQ6D,KAAKC,cAAcC,YAAYR,KAAKS,UAAUO,MAAO,YAGlE,CAGYhE,SAASgB,iBAAiB,0BAA4BC,IAAIC,OAAOJ,MAAMP,MAAQ,KACrF0D,SAASJ,MACdA,IAAIlB,iBAAiB,UAAU,QACvB/C,2BAGAoE,KAAO,CACPd,QAAS,iBACTnC,KAAM,gBACNR,KAAMwC,IAAIxC,SAEVsD,IAAID,SACJI,KAAK5B,MAAQyB,IAAIzB,UAKhB,IAAIkB,OAAO5D,OAAOmE,IAAI/D,IACvBkE,KAAI,IAAUV,IACd7D,QAAQ6D,KAAKC,cAAcC,YAAYR,KAAKS,UAAUO,MAAO,gBAO5E,gBAAiBjB,KAAQA,IAAI,gBAAiC,UAAfjC,MAAMC,QAClDD,MAAMhB,MAAMH,mBAAoB,IAC5BoD,IAAII,OAAOxD,mBAAmBmB,MAAMhB,WAIxCH,mBAAmBmB,MAAMhB,IAAIgE,KAAKf,IAAII,UAEtCxD,mBAAmBmB,MAAMhB,IAAM,CAACiD,IAAII,KAEpCrC,MAAM6B,iBAAiB,SAAS,QACxB/C,2BAGAoE,KAAO,CACPd,QAAS,iBACTnC,KAAM,gBACNR,KAAMwC,IAAIxC,MAEK,aAAfO,MAAMC,KACNiD,KAAI,MAAYlD,MAAM8C,QAEtBI,KAAI,MAAYlD,MAAMsB,UAErB,IAAIkB,OAAO3D,mBAAmBmB,MAAMhB,IACrCkE,KAAI,IAAUV,IACd7D,QAAQ6D,KAAKC,cAAcC,YAAYR,KAAKS,UAAUO,MAAO,QAQvEjB,IAAII,OAAOzD,OAAOoB,MAAMhB,KAC1BL,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUJ,UAAW,eAIxE,mBAEDvC,MAAQR,sBAAsByC,IAAIxC,KAAMwC,IAAII,KAE9B,OAAVrC,MAAgB,OAEVoD,IAAM,CACRhB,QAAS,iBACTnC,KAAM,QACNgC,IAAK,4BAA8BA,IAAIxC,KAAO,IAC9C+C,IAAKP,IAAII,iBAEb1D,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUS,KAAM,KAKpEtE,iBAAkB,EAGC,aAAfkB,MAAMC,KACND,MAAM8C,QAAUb,IAAIX,MAEpBtB,MAAMsB,MAAQW,IAAIX,MAItBX,iBAAiBX,OAGjBlB,iBAAkB,EAGlByD,SAAStC,KAAO,gBAChBsC,SAAS9C,KAAOwC,IAAIxC,KACpB8C,SAASjB,MAAQW,IAAIX,UAEhB,IAAIkB,OAAO5D,OAAOoB,MAAMhB,IACrBwD,MAAQP,IAAII,MACZE,SAASC,IAAMA,IACf7D,QAAQ6D,KAAKC,cAAcC,YAAYR,KAAKS,UAAUJ,UAAW,gBAIxE,iBAEDvC,MAAQR,sBAAsByC,IAAIxC,KAAMwC,IAAII,KAEP,WAAjCrC,MAAM4C,SAASpB,cAA4B,EACd,IAAzBxB,MAAMqD,gBACNrD,MAAMqD,eAAiB,EACvB1C,iBAAiBX,YAEjB,IAAIgB,EAAI,EAAGA,EAAIhB,MAAMsD,QAAQ7C,OAAQO,IACjChB,MAAMsD,QAAQtC,GAAG6B,aAAa,cAC9B7C,MAAMsD,QAAQtC,GAAGuC,gBAAgB,YACjC5C,iBAAiBX,QAEU,KAA3BA,MAAMsD,QAAQtC,GAAGM,QAEjBtB,MAAMsD,QAAQtC,GAAGwC,UAAW,EAC5B7C,iBAAiBX,aAGtB,GAAqC,aAAjCA,MAAM4C,SAASpB,cACF,KAAhBxB,MAAMsB,QACNtB,MAAMsB,MAAQ,GACdX,iBAAiBX,aAElB,GAAmB,aAAfA,MAAMC,SACR,IAAI8C,OAAOhD,mCAAmCC,OAC/C+C,IAAID,SAAU,EACdnC,iBAAiBoC,UAElB,GAAmB,UAAf/C,MAAMC,SACR,IAAI8C,OAAOhD,mCAAmCC,OAE/C+C,IAAID,QAAwB,KAAdC,IAAIzB,MAClBX,iBAAiBoC,SAGD,KAAhB/C,MAAMsB,QACNtB,MAAMsB,MAAQ,GACdX,iBAAiBX,QAIzBW,iBAAiBX,iBAEhB,8BAEDsC,QAAUvD,gBAAgBkD,IAAIwB,QAEd,OAAZnB,QAAkB,OAEZc,IAAM,CACRhB,QAAS,iBACTnC,KAAM,QACNgC,IAAK,4BAA8BA,IAAIwB,OAAS,IAChDjB,IAAKP,IAAII,iBAEb1D,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUS,KAAM,KAMpEd,QAAQT,iBAAiB,SAAU6B,YAC3BR,KAAO,CACPd,QAAS,iBACTnC,KAAM,eACNR,KAAMwC,IAAIwB,OACVjB,IAAKP,IAAII,KAEb1D,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUO,MAAO,KAEjEQ,MAAMC,8BAIT,uBAEDrB,QAAUvD,gBAAgBkD,IAAIwB,QAEd,OAAZnB,QAAkB,OAEZc,IAAM,CACRhB,QAAS,iBACTnC,KAAM,QACNgC,IAAK,4BAA8BA,IAAIwB,OAAS,IAChDjB,IAAKP,IAAII,iBAEb1D,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUS,KAAM,KAKpD,SAAZnB,IAAI2B,KACJtB,QAAQuB,MAAMC,QAAU,QAExB5C,eAAeoB,UACI,SAAZL,IAAI2B,MACXtB,QAAQuB,MAAMC,QAAU,kBAI3B,oBAEDxB,QAAUvD,gBAAgBkD,IAAIwB,QAEd,OAAZnB,eAEAC,SAAStC,KAAO,QAChBsC,SAASN,IAAM,4BAA8BA,IAAIwB,OAAS,IAC1DlB,SAASC,IAAMP,IAAII,SACnB1D,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUJ,UAAW,KASzED,QAAQyB,mBA5aW1B,SAanB2B,KADS,IAAIC,WACAC,gBAAgB7B,IAAK,iBAIjC,IAAI8B,MAAMH,IAAI9D,iBAAiB,iBAChCiE,GAAGC,aAIF,IAAID,MAAMH,IAAI9D,iBAAiB,SAC3B,IAAIT,KAACA,KAAD6B,MAAOA,SAAU6C,GAAGE,WACrBhD,kBAAkB5B,KAAM6B,QACxB6C,GAAGZ,gBAAgB9D,aAKxBuE,IAAIM,KA8YaC,CAAkBtC,IAAIuC,SAAST,UAEnD7C,eAAeoB,mBAGd,cAEDA,QAAUvD,gBAAgBkD,IAAIwB,QAE9BlB,SAAStC,KAAO,eAChBsC,SAASC,IAAMP,IAAII,IACnBE,SAASkB,OAASxB,IAAIwB,OACtBlB,SAASiC,QAAU,KACH,OAAZlC,UAWAC,SAASiC,QAAUlC,QAAQyB,WAE/BpF,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUJ,UAAW,eAEpE,eAEDD,QAAU3D,QAAQsD,IAAII,KAAK9C,cAG3B+C,QAAQuB,MAAMY,MAAQxC,IAAIwC,MAC1BnC,QAAQuB,MAAMa,OAASzC,IAAIyC,OAG3B/F,QAAQsD,IAAII,KAAKwB,MAAMY,MAAQ,OAC/B9F,QAAQsD,IAAII,KAAKwB,MAAMa,OAAS,OAGhCxD,eAAeoB,mBAEd,cAIDC,SAAStC,KAAO,OAChBsC,SAASC,IAAMP,IAAII,SAEnB1D,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUJ,UAAW,SAEpE,6BACDA,SAAStC,KAAO,qBAChBsC,SAASC,IAAMP,IAAII,IACnBrC,MAAQU,sBAAsBuB,IAAII,KACpB,OAAVrC,OAAkBA,MAAM6C,aAAa,UACrCN,SAAQ,MAAY,KAEpBA,SAAQ,MAAYvC,MAAMsB,WAE9B3C,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUJ,UAAW,SAEpE,8BACDvC,MAAQU,sBAAsBuB,IAAII,UACpB,OAAVrC,MACIiC,IAAI0C,QACJ3E,MAAMuD,gBAAgB,YAEtBvD,MAAM4E,UAAW,GAKrBrC,SAAStC,KAAO,QAChBsC,SAASN,IAAM,2DACfM,SAASC,IAAMP,IAAII,IACnB1D,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUJ,UAAW,WAGxE,+BACDvC,MAAQU,sBAAsBuB,IAAII,UACpB,OAAVrC,MACAA,MAAMsB,MAAQW,IAAIxC,MAIlB8C,SAAStC,KAAO,QAChBsC,SAASN,IAAM,2DACfM,SAASC,IAAMP,IAAII,IACnB1D,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUJ,UAAW,WAGxE,yBACA,oBACA,sBAQDA,SAAStC,KAAO,QAChBsC,SAASN,IAAM,0BAA4BA,IAAIhC,KAAO,IACtDsC,SAASC,IAAMP,IAAII,IAEnB1D,QAAQsD,IAAII,KAAKI,cAAcC,YAAYR,KAAKS,UAAUJ,UAAW,SAMtE,CAyBHsC,cAAcC,SAAUN,QAASO,YAAaC,MAAOC,UAAWC,YACtDC,IAAMjG,SAASkG,cAAc,UACnCD,IAAInG,GAAK8F,SACTK,IAAItB,MAAMY,MAAQ,OAClBU,IAAItB,MAAMa,OAAS,OACnBS,IAAItB,MAAMwB,OAAS,GACD,IAAdJ,WACAE,IAAIF,UAAY,KAChBE,IAAItB,MAAMyB,SAAW,UAErBH,IAAIF,UAAY,MAEpBE,IAAIH,MAAQA,MAEZG,IAAII,eAAiB,cAKhBL,OACDC,IAAIK,QAAU,iCAYlBL,IAAIM,OAASjB,QAMbtF,SAASC,eAAe4F,aAAaW,gBAAgBP,KACrDxG,QAAQmG,UAAYK"} \ No newline at end of file +{"version":3,"file":"stackjsvle.min.js","sources":["../src/stackjsvle.js"],"sourcesContent":["/**\n * A javascript module to handle separation of author sourced scripts into\n * IFRAMES. All such scripts will have limited access to the actual document\n * on the VLE side and this script represents the VLE side endpoint for\n * message handling needed to give that access. When porting STACK onto VLEs\n * one needs to map this script to do the following:\n *\n * 1. Ensure that searches for target elements/inputs are limited to questions\n * or their feedback and do not return any elements outside them.\n *\n * 2. Map any identifiers needed to identify inputs by name.\n *\n * 3. Any change handling related to input value modifications through this\n * logic gets connected to any such handling on the VLE side.\n *\n *\n * This script is intenttionally ordered so that the VLE specific bits should\n * be at the top.\n *\n *\n * This script assumes the following:\n *\n * 1. Each relevant IFRAME has an `id`-attribute that will be told to this\n * script.\n *\n * 2. Each such IFRAME exists within the question content itself, so that\n * one can traverse up the DOM tree from that IFRAME to find the border of\n * the question.\n *\n * @module qtype_stack/stackjsvle\n * @copyright 2023 Aalto University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'core_filters/events'\n], function(\n CustomEvents\n) {\n 'use strict';\n // Note the VLE specific include of logic.\n\n /* All the IFRAMES have unique identifiers that they give in their\n * messages. But we only work with those that have been created by\n * our logic and are found from this map.\n */\n let IFRAMES = {};\n\n /* For event handling, lists of IFRAMES listening particular inputs.\n */\n let INPUTS = {};\n\n /* For event handling, lists of IFRAMES listening particular inputs\n * and their input events. By default we only listen to changes.\n * We report input events as changes to the other side.\n */\n let INPUTS_INPUT_EVENT = {};\n\n /* A flag to disable certain things. */\n let DISABLE_CHANGES = false;\n\n\n /**\n * Returns an element with a given id, if an only if that element exists\n * inside a portion of DOM that represents a question or its feedback.\n *\n * If not found or exists outside the restricted area then returns `null`.\n *\n * @param {String} id the identifier of the element we want.\n */\n function vle_get_element(id) {\n /* In the case of Moodle we are happy as long as the element is inside\n something with the `formulation`-class. */\n let candidate = document.getElementById(id);\n let iter = candidate;\n while (iter && !iter.classList.contains('formulation') &&\n !iter.classList.contains('outcome')) {\n iter = iter.parentElement;\n }\n if (iter && (iter.classList.contains('formulation') ||\n iter.classList.contains('outcome'))) {\n return candidate;\n }\n\n return null;\n }\n\n /**\n * Returns an input element with a given name, if and only if that element\n * exists inside a portion of DOM that represents a question or its feedback.\n *\n * Note that, the input element may have a name that multiple questions\n * use and to pick the preferred element one needs to pick the one\n * within the same question as the IFRAME.\n *\n * Note that the input can also be a select. In the case of radio buttons\n * returning one of the possible buttons is enough.\n *\n * If not found or exists outside the restricted area then returns `null`.\n *\n * @param {String} name the name of the input we want\n * @param {String} srciframe the identifier of the iframe wanting it\n * @param {boolean} outside do we expand the search beyound the src question?\n */\n function vle_get_input_element(name, srciframe, outside) {\n /* In the case of Moodle we are happy as long as the element is inside\n something with the `formulation`-class. */\n if (outside === undefined) {\n // Old default was to search beyoudn the question.\n outside = true;\n }\n let initialcandidate = document.getElementById(srciframe);\n let iter = initialcandidate;\n while (iter && !iter.classList.contains('formulation') &&\n !iter.classList.contains('outcome')) {\n iter = iter.parentElement;\n }\n if (iter && (iter.classList.contains('formulation') ||\n iter.classList.contains('outcome'))) {\n // iter now represents the borders of the question containing\n // this IFRAME.\n let possible = iter.querySelector('input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n possible = iter.querySelector('textarea[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n // Radios have interesting ids, but the name makes sense\n possible = iter.querySelector('input[id$=\"_' + name + '_1\"][type=radio]');\n if (possible !== null) {\n return possible;\n }\n // Same for checkboxes, ntoe that non STACK checkbox can be targetted by\n // just the id using the topmost case here.\n possible = iter.querySelector('input[id$=\"_' + name + '_1\"][type=checkbox]');\n if (possible !== null) {\n return possible;\n }\n possible = iter.querySelector('select[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n }\n if (!outside) {\n return null;\n }\n // If none found within the question itself, search everywhere.\n let possible = document.querySelector('.formulation input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.formulation textarea[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n // Radios have interesting ids, but the name makes sense\n possible = document.querySelector('.formulation input[id$=\"_' + name + '_1\"][type=radio]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.formulation input[id$=\"_' + name + '_1\"][type=checkbox]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.formulation select[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n\n // Also search from within the feedback and other \"outcome\".\n // Note that we do not search for STACK sourced checkboxes from the feedback,\n // they do not exist there so simply finding them with the id is enough.\n possible = document.querySelector('.outcome input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.outcome textarea[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.outcome select[id$=\"_' + name + '\"]');\n return possible;\n }\n\n /**\n * Returns a list of input elements targetting the same thing.\n *\n * Note that STACK checkboxes have interesting naming for this.\n * And we assume we are getting the ones that `vle_get_input_element` would return.\n *\n * @param {element} input element of type=radio or type=checkbox\n */\n function vle_get_others_of_same_input_group(input) {\n if (input.type === 'radio') {\n return document.querySelectorAll('.formulation input[name=' + CSS.escape(input.name) + ']');\n }\n // Is it a Moodle input or a fake? If Moodle then assume STACK and its pattern.\n if (input.name.startsWith('q') && input.name.indexOf(':') > -1 && input.name.endsWith('_1')) {\n return document.querySelectorAll('.formulation input[name^=' +\n CSS.escape(input.name.substring(0, input.name.length - 1)) + ']');\n }\n return document.querySelectorAll('.formulation input[name=' + CSS.escape(input.name) + ']');\n }\n\n\n /**\n * Returns the input element or null for a question level submit button.\n * Basically, the \"Check\" button that behaviours like adaptive-mode in Moodle have.\n * Not all questions have such buttons, and the behaviour will affect that.\n *\n * Will only return the button of the question containing that iframe.\n *\n * @param {String} srciframe the identifier of the iframe wanting it\n */\n function vle_get_submit_button(srciframe) {\n let initialcandidate = document.getElementById(srciframe);\n let iter = initialcandidate;\n // Note the submit button is most definitely not within \"outcome\".\n while (iter && !iter.classList.contains('formulation')) {\n iter = iter.parentElement;\n }\n if (iter && iter.classList.contains('formulation')) {\n // iter now represents the borders of the question containing\n // this IFRAME.\n // In Moodle inputs that are behaviour variables use `-` as a separator\n // for the name and usage id.\n let possible = iter.querySelector('.im-controls *[id$=\"-submit\"][type=submit]');\n return possible;\n }\n return null;\n }\n\n /**\n * Triggers any VLE specific scripting related to updates of the given\n * input element.\n *\n * @param {HTMLElement} inputelement the input element that has changed\n */\n function vle_update_input(inputelement) {\n // Triggering a change event may be necessary.\n const c = new Event('change');\n inputelement.dispatchEvent(c);\n // Also there are those that listen to input events.\n const i = new Event('input');\n inputelement.dispatchEvent(i);\n if (inputelement.type === 'radio' || inputelement.type === 'checkbox') {\n const k = new Event('click');\n inputelement.dispatchEvent(k);\n }\n }\n\n /**\n * Triggers any VLE specific scripting related to DOM updates.\n *\n * @param {HTMLElement} modifiedsubtreerootelement element under which changes may have happened.\n */\n function vle_update_dom(modifiedsubtreerootelement) {\n CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement);\n }\n\n /**\n * Does HTML-string cleaning, i.e., removes any script payload. Returns\n * a DOM version of the given input string. The DOM version returned is\n * an element of some sort containing the contents, possibly a `body`.\n *\n * This is used when receiving replacement content for a div.\n *\n * @param {String} src a raw string to sanitise\n */\n function vle_html_sanitize(src) {\n // This can be implemented with many libraries or by custom code\n // however as this is typically a thing that a VLE might already have\n // tools for we have it at this level so that the VLE can use its own\n // tools that do things that the VLE developpers consider safe.\n\n // As Moodle does not currently seem to have such a sanitizer in\n // the core libraries, here is one implementation that shows what we\n // are looking for.\n\n // TO-DO: look into replacing this with DOMPurify or some such.\n\n let parser = new DOMParser();\n let doc = parser.parseFromString(src, \"text/html\");\n\n // First remove all <script> tags. Also <style> as we do not want\n // to include too much style.\n for (let el of doc.querySelectorAll('script, style')) {\n el.remove();\n }\n\n // Check all elements for attributes.\n for (let el of doc.querySelectorAll('*')) {\n for (let {name, value} of el.attributes) {\n if (is_evil_attribute(name, value)) {\n el.removeAttribute(name);\n }\n }\n }\n\n return doc.body;\n }\n\n /**\n * Utility function trying to determine if a given attribute is evil\n * when sanitizing HTML-fragments.\n *\n * @param {String} name the name of an attribute.\n * @param {String} value the value of an attribute.\n */\n function is_evil_attribute(name, value) {\n const lcname = name.toLowerCase();\n if (lcname.startsWith('on')) {\n // We do not allow event listeners to be defined.\n return true;\n }\n if (lcname === 'src' || lcname.endsWith('href')) {\n // Do not allow certain things in the urls.\n const lcvalue = value.replace(/\\s+/g, '').toLowerCase();\n // Ignore es-lint false positive.\n /* eslint-disable no-script-url */\n if (lcvalue.includes('javascript:') || lcvalue.includes('data:text')) {\n return true;\n }\n }\n\n return false;\n }\n\n\n /*************************************************************************\n * Above this are the bits that one would probably tune when porting.\n *\n * Below is the actuall message handling and it should be left alone.\n */\n window.addEventListener(\"message\", (e) => {\n // NOTE! We do not check the source or origin of the message in\n // the normal way. All actions that can bypass our filters to trigger\n // something are largely irrelevant and all traffic will be kept\n // \"safe\" as anyone could be listening.\n\n // All messages we receive are strings, anything else is for someone\n // else and will be ignored.\n if (!(typeof e.data === 'string' || e.data instanceof String)) {\n return;\n }\n\n // That string is a JSON encoded dictionary.\n let msg = null;\n try {\n msg = JSON.parse(e.data);\n } catch (e) {\n // Only JSON objects that are parseable will work.\n return;\n }\n\n // All messages we handle contain a version field with a particular\n // value, for now we leave the possibility open for that value to have\n // an actual version number suffix...\n if (!(('version' in msg) && msg.version.startsWith('STACK-JS'))) {\n return;\n }\n\n // All messages we handle must have a source and a type,\n // and that source must be one of the registered ones.\n if (!(('src' in msg) && ('type' in msg) && (msg.src in IFRAMES))) {\n return;\n }\n let element = null;\n let input = null;\n\n let response = {\n version: 'STACK-JS:1.3.0'\n };\n\n switch (msg.type) {\n case 'register-input-listener':\n // 1. Find the input.\n input = vle_get_input_element(msg.name, msg.src, !msg['limit-to-question']);\n\n if (input === null) {\n // Requested something that is not available.\n response.type = 'error';\n response.msg = 'Failed to connect to input: \"' + msg.name + '\"';\n response.tgt = msg.src;\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n return;\n }\n\n response.type = 'initial-input';\n response.name = msg.name;\n response.tgt = msg.src;\n\n // 2. What type of an input is this? Note that we do not\n // currently support all types in sensible ways. In particular,\n // anything with multiple values will be a problem.\n if (input.nodeName.toLowerCase() === 'select') {\n response.value = input.value;\n response['input-type'] = 'select';\n response['input-readonly'] = input.hasAttribute('disabled');\n } else if (input.nodeName.toLowerCase() === 'textarea') {\n response.value = input.value;\n response['input-type'] = 'textarea';\n response['input-readonly'] = input.hasAttribute('disabled');\n } else if (input.type === 'checkbox') {\n response.value = input.checked;\n response['input-type'] = 'checkbox';\n response['input-readonly'] = input.hasAttribute('disabled');\n } else {\n response.value = input.value;\n response['input-type'] = input.type;\n response['input-readonly'] = input.hasAttribute('readonly');\n }\n if (input.type === 'radio') {\n response['input-readonly'] = input.hasAttribute('disabled');\n response.value = '';\n for (let inp of document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']')) {\n if (inp.checked) {\n response.value = inp.value;\n }\n }\n }\n\n // 3. Add listener for changes of this input.\n if (input.id in INPUTS) {\n if (msg.src in INPUTS[input.id]) {\n // DO NOT BIND TWICE!\n return;\n }\n if (input.type !== 'radio') {\n INPUTS[input.id].push(msg.src);\n } else {\n let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');\n for (let inp of radgroup) {\n INPUTS[inp.id].push(msg.src);\n }\n }\n } else {\n if (input.type !== 'radio') {\n INPUTS[input.id] = [msg.src];\n } else {\n let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');\n for (let inp of radgroup) {\n INPUTS[inp.id] = [msg.src];\n }\n }\n if (input.type !== 'radio') {\n input.addEventListener('change', () => {\n if (DISABLE_CHANGES) {\n return;\n }\n let resp = {\n version: 'STACK-JS:1.0.0',\n type: 'changed-input',\n name: msg.name\n };\n if (input.type === 'checkbox') {\n resp['value'] = input.checked;\n } else {\n resp['value'] = input.value;\n }\n for (let tgt of INPUTS[input.id]) {\n resp['tgt'] = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');\n }\n });\n } else {\n // Assume that if we received a radio button that is safe\n // then all its friends are also safe.\n let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');\n radgroup.forEach((inp) => {\n inp.addEventListener('change', () => {\n if (DISABLE_CHANGES) {\n return;\n }\n let resp = {\n version: 'STACK-JS:1.0.0',\n type: 'changed-input',\n name: msg.name\n };\n if (inp.checked) {\n resp.value = inp.value;\n } else {\n // What about unsetting?\n return;\n }\n for (let tgt of INPUTS[inp.id]) {\n resp['tgt'] = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');\n }\n });\n });\n }\n }\n\n if (('track-input' in msg) && msg['track-input'] && input.type !== 'radio') {\n if (input.id in INPUTS_INPUT_EVENT) {\n if (msg.src in INPUTS_INPUT_EVENT[input.id]) {\n // DO NOT BIND TWICE!\n return;\n }\n INPUTS_INPUT_EVENT[input.id].push(msg.src);\n } else {\n INPUTS_INPUT_EVENT[input.id] = [msg.src];\n\n input.addEventListener('input', () => {\n if (DISABLE_CHANGES) {\n return;\n }\n let resp = {\n version: 'STACK-JS:1.0.0',\n type: 'changed-input',\n name: msg.name\n };\n if (input.type === 'checkbox') {\n resp['value'] = input.checked;\n } else {\n resp['value'] = input.value;\n }\n for (let tgt of INPUTS_INPUT_EVENT[input.id]) {\n resp['tgt'] = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');\n }\n });\n }\n }\n\n // 4. Let the requester know that we have bound things\n // and let it know the initial value.\n if (!(msg.src in INPUTS[input.id])) {\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n\n break;\n case 'changed-input':\n // 1. Find the input.\n input = vle_get_input_element(msg.name, msg.src);\n\n if (input === null) {\n // Requested something that is not available.\n const ret = {\n version: 'STACK-JS:1.0.0',\n type: 'error',\n msg: 'Failed to modify input: \"' + msg.name + '\"',\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');\n return;\n }\n\n // Disable change events.\n DISABLE_CHANGES = true;\n\n // TO-DO: Radio buttons should we check that value is possible?\n if (input.type === 'checkbox') {\n input.checked = msg.value;\n } else {\n input.value = msg.value;\n }\n\n // Trigger VLE side actions.\n vle_update_input(input);\n\n // Enable change tracking.\n DISABLE_CHANGES = false;\n\n // Tell all other frames, that care, about this.\n response.type = 'changed-input';\n response.name = msg.name;\n response.value = msg.value;\n\n for (let tgt of INPUTS[input.id]) {\n if (tgt !== msg.src) {\n response.tgt = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n }\n break;\n case 'clear-input':\n // 1. Find the input.\n input = vle_get_input_element(msg.name, msg.src);\n\n if (input.nodeName.toLowerCase() === 'select') {\n if (input.selectedIndex !== -1) {\n input.selectedIndex = -1;\n vle_update_input(input);\n }\n for(var i = 0; i < input.options.length; i++) {\n if (input.options[i].hasAttribute('selected')) {\n input.options[i].removeAttribute('selected');\n vle_update_input(input);\n }\n if (input.options[i].value === '') {\n // If we have the clear input option select that.\n input.options[i].selected = true;\n vle_update_input(input);\n }\n }\n } else if (input.nodeName.toLowerCase() === 'textarea') {\n if (input.value !== '') {\n input.value = '';\n vle_update_input(input);\n }\n } else if (input.type === 'checkbox') {\n for (let inp of vle_get_others_of_same_input_group(input)) {\n inp.checked = false;\n vle_update_input(inp);\n }\n } else if (input.type === 'radio') {\n for (let inp of vle_get_others_of_same_input_group(input)) {\n // If we have the clear value option select that.\n inp.checked = inp.value === '';\n vle_update_input(inp);\n }\n } else {\n if (input.value !== '') {\n input.value = '';\n vle_update_input(input);\n }\n }\n\n vle_update_input(input);\n break;\n case 'register-button-listener':\n // 1. Find the element.\n element = vle_get_element(msg.target);\n\n if (element === null) {\n // Requested something that is not available.\n const ret = {\n version: 'STACK-JS:1.2.0',\n type: 'error',\n msg: 'Failed to find element: \"' + msg.target + '\"',\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');\n return;\n }\n\n // 2. Add a listener, no need to do anything more\n // complicated than this.\n element.addEventListener('click', (event) => {\n let resp = {\n version: 'STACK-JS:1.2.0',\n type: 'button-click',\n name: msg.target,\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(resp), '*');\n // These listeners will stop the submissions of buttons which might be a problem.\n event.preventDefault();\n });\n\n break;\n case 'toggle-visibility':\n // 1. Find the element.\n element = vle_get_element(msg.target);\n\n if (element === null) {\n // Requested something that is not available.\n const ret = {\n version: 'STACK-JS:1.0.0',\n type: 'error',\n msg: 'Failed to find element: \"' + msg.target + '\"',\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');\n return;\n }\n\n // 2. Toggle display setting.\n if (msg.set === 'show') {\n element.style.display = 'block';\n // If we make something visible we should let the VLE know about it.\n vle_update_dom(element);\n } else if (msg.set === 'hide') {\n element.style.display = 'none';\n }\n\n break;\n case 'change-content':\n // 1. Find the element.\n element = vle_get_element(msg.target);\n\n if (element === null) {\n // Requested something that is not available.\n response.type = 'error';\n response.msg = 'Failed to find element: \"' + msg.target + '\"';\n response.tgt = msg.src;\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n return;\n }\n\n // 2. Secure content.\n // 3. Switch the content. Note the contents coming from `vle_html_sanitize`\n // are wrapped in an element possibly `<body>` and will need to be unwrapped.\n // We can simply use innerHTML here to also disconnect the content from\n // whatever it was before being sanitized.\n element.innerHTML = vle_html_sanitize(msg.content).innerHTML;\n // If we tune something we should let the VLE know about it.\n vle_update_dom(element);\n\n break;\n case 'get-content':\n // 1. Find the element.\n element = vle_get_element(msg.target);\n // 2. Build the message.\n response.type = 'xfer-content';\n response.tgt = msg.src;\n response.target = msg.target;\n response.content = null;\n if (element !== null) {\n // TO-DO: Should we sanitise the content? Probably not as using\n // this to interrogate neighbouring questions only allows\n // messing with the other questions and not anything outside\n // them. If we do not sanitise it we allow some interesting\n // question-analytics tooling, and if we do we really don't\n // gain anything sensible.\n // Matti's opinnion is to not sanitise at this point as\n // interraction between questions is not inherently evil\n // and could be of use even at the level of reading code from\n // from other questions.\n response.content = element.innerHTML;\n }\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n break;\n case 'resize-frame':\n // 1. Find the frames wrapper div.\n element = IFRAMES[msg.src].parentElement;\n\n // 2. Set the wrapper size.\n element.style.width = msg.width;\n element.style.height = msg.height;\n\n // 3. Reset the frame size.\n IFRAMES[msg.src].style.width = '100%';\n IFRAMES[msg.src].style.height = '100%';\n\n // Only touching the size but still let the VLE know.\n vle_update_dom(element);\n break;\n case 'ping':\n // This is for testing the connection. The other end will\n // send these untill it receives a reply.\n // Part of the logic for startup.\n response.type = 'ping';\n response.tgt = msg.src;\n\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n return;\n case 'query-submit-button':\n response.type = 'submit-button-info';\n response.tgt = msg.src;\n input = vle_get_submit_button(msg.src);\n if (input === null || input.hasAttribute('hidden')) {\n response['value'] = null;\n } else {\n response['value'] = input.value;\n }\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n return;\n case 'enable-submit-button':\n input = vle_get_submit_button(msg.src);\n if (input !== null) {\n if (msg.enabled) {\n input.removeAttribute('disabled');\n } else {\n input.disabled = true;\n }\n } else {\n // We generate this error just to push people to properly check if\n // the button even exists before trying to tune it.\n response.type = 'error';\n response.msg = 'Could not find matching submit button for this question.';\n response.tgt = msg.src;\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n return;\n case 'relabel-submit-button':\n input = vle_get_submit_button(msg.src);\n if (input !== null) {\n if (input.childNodes.length > 1) {\n // If we happen to have some extra SR stuff...\n input.childNodes.forEach((n) => {\n if (n.nodeName == '#text') {\n n.textContent = msg.name;\n }\n });\n } else {\n input.innerHTML = msg.name;\n }\n input.value = msg.name;\n } else {\n // We generate this error just to push people to properly check if\n // the button even exists before trying to tune it.\n response.type = 'error';\n response.msg = 'Could not find matching submit button for this question.';\n response.tgt = msg.src;\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n return;\n case 'submit-button-info':\n case 'initial-input':\n case 'error':\n // These message types are for the other end.\n break;\n\n default:\n // If we see something unexpected, lets let the other end know\n // and make sure that they know our version. Could be that this\n // end has not been upgraded.\n response.type = 'error';\n response.msg = 'Unknown message-type: \"' + msg.type + '\"';\n response.tgt = msg.src;\n\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n\n });\n\n\n return {\n /* To avoid any logic that forbids IFRAMEs in the VLE output one can\n also create and register that IFRAME through this logic. This\n also ensures that all relevant security settigns for that IFRAME\n have been correctly tuned.\n\n Here the IDs are for the secrect identifier that may be present\n inside the content of that IFRAME and for the question that contains\n it. One also identifies a DIV element that marks the position of\n the IFRAME and limits the size of the IFRAME (all IFRAMEs this\n creates will be 100% x 100%).\n\n @param {String} iframeid the id that the IFRAME has stored inside\n it and uses for communication.\n @param {String} the full HTML content of that IFRAME.\n @param {String} targetdivid the id of the element (div) that will\n hold the IFRAME.\n @param {String} title a descriptive name for the iframe.\n @param {bool} scrolling whether we have overflow:scroll or\n overflow:hidden.\n @param {bool} evil allows certain special cases to act without\n sandboxing, this is a feature that will be removed so do\n not rely on it only use it to test STACK-JS before you get your\n thing to run in a sandbox.\n */\n create_iframe(iframeid, content, targetdivid, title, scrolling, evil) {\n const frm = document.createElement('iframe');\n frm.id = iframeid;\n frm.style.width = '100%';\n frm.style.height = '100%';\n frm.style.border = 0;\n if (scrolling === false) {\n frm.scrolling = 'no';\n frm.style.overflow = 'hidden';\n } else {\n frm.scrolling = 'yes';\n }\n frm.title = title;\n // Somewhat random limitation.\n frm.referrerpolicy = 'no-referrer';\n // We include that allow-downloads as an example of XLS-\n // document building in JS has been seen.\n // UNDER NO CIRCUMSTANCES DO WE ALLOW-SAME-ORIGIN!\n // That would defeat the whole point of this.\n if (!evil) {\n frm.sandbox = 'allow-scripts allow-downloads';\n }\n\n // As the SOP is intentionally broken we need to allow\n // scripts from everywhere.\n\n // NOTE: this bit commented out as long as the csp-attribute\n // is not supported by more browsers.\n // frm.csp = \"script-src: 'unsafe-inline' 'self' '*';\";\n // frm.csp = \"script-src: 'unsafe-inline' 'self' '*';img-src: '*';\";\n\n // Plug the content into the frame.\n frm.srcdoc = content;\n\n // The target DIV will have its children removed.\n // This allows that div to contain some sort of loading\n // indicator until we plug in the frame.\n // Naturally the frame will then start to load itself.\n document.getElementById(targetdivid).replaceChildren(frm);\n IFRAMES[iframeid] = frm;\n }\n\n };\n});"],"names":["define","CustomEvents","IFRAMES","INPUTS","INPUTS_INPUT_EVENT","DISABLE_CHANGES","vle_get_element","id","candidate","document","getElementById","iter","classList","contains","parentElement","vle_get_input_element","name","srciframe","outside","undefined","possible","querySelector","vle_get_others_of_same_input_group","input","type","querySelectorAll","CSS","escape","startsWith","indexOf","endsWith","substring","length","vle_get_submit_button","vle_update_input","inputelement","c","Event","dispatchEvent","i","k","vle_update_dom","modifiedsubtreerootelement","notifyFilterContentUpdated","is_evil_attribute","value","lcname","toLowerCase","lcvalue","replace","includes","window","addEventListener","e","data","String","msg","JSON","parse","version","src","element","response","tgt","contentWindow","postMessage","stringify","nodeName","hasAttribute","checked","inp","push","radgroup","resp","forEach","ret","selectedIndex","options","removeAttribute","selected","target","event","preventDefault","set","style","display","innerHTML","doc","DOMParser","parseFromString","el","remove","attributes","body","vle_html_sanitize","content","width","height","enabled","disabled","childNodes","n","textContent","create_iframe","iframeid","targetdivid","title","scrolling","evil","frm","createElement","border","overflow","referrerpolicy","sandbox","srcdoc","replaceChildren"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/stackjsvle.js b/amd/src/stackjsvle.js index 17dcb38e58122e95c1518473e6b3c65aead4821d..e31bc15dce413e6d344e889030777116b1f6695a 100644 --- a/amd/src/stackjsvle.js +++ b/amd/src/stackjsvle.js @@ -225,7 +225,7 @@ define([ // this IFRAME. // In Moodle inputs that are behaviour variables use `-` as a separator // for the name and usage id. - let possible = iter.querySelector('input[id$="-submit"][type=submit]'); + let possible = iter.querySelector('.im-controls *[id$="-submit"][type=submit]'); return possible; } return null; @@ -779,6 +779,16 @@ define([ case 'relabel-submit-button': input = vle_get_submit_button(msg.src); if (input !== null) { + if (input.childNodes.length > 1) { + // If we happen to have some extra SR stuff... + input.childNodes.forEach((n) => { + if (n.nodeName == '#text') { + n.textContent = msg.name; + } + }); + } else { + input.innerHTML = msg.name; + } input.value = msg.name; } else { // We generate this error just to push people to properly check if