Latest build deployed.
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
const timeOutHandler = new Object();
|
||||
const inputPrefix = "stackapi_input_";
|
||||
const feedbackPrefix = "stackapi_fb_";
|
||||
const validationPrefix = "stackapi_val_";
|
||||
const stackstring = {
|
||||
"teacheranswershow_mcq": "A correct answer is: {$a->display}",
|
||||
"api_which_typed": "which can be typed as follows",
|
||||
"api_valid_all_parts": "Please enter valid answers for all parts of the question.",
|
||||
"api_out_of": "out of",
|
||||
"api_marks_sub": "Marks for this submission",
|
||||
"api_submit": "Submit Answers",
|
||||
"generalfeedback": "General feedback",
|
||||
"score": "Score",
|
||||
"api_response": "Response summary",
|
||||
"api_correct": "Correct answers"
|
||||
};
|
||||
function wrap_math(content) {
|
||||
content = content.replace(/(?<!\\)(\\\(.*?(?<!\\)\\\))/gs, '<span class="process-math">$1</span>');
|
||||
return content.replace(/(?<!\\)(\\\[.*?(?<!\\)\\\])/gs, '<span class="process-math">$1</span>');
|
||||
}
|
||||
async function collectData(qfile, qname, qprefix) {
|
||||
let res = "";
|
||||
await getQuestionFile(qfile, qname).then((response) => {
|
||||
if (response.questionxml != "<quiz>\nnull\n</quiz>") {
|
||||
res = {
|
||||
questionDefinition: response.questionxml,
|
||||
answers: collectAnswer(qprefix),
|
||||
seed: response.seed,
|
||||
renderInputs: qprefix + inputPrefix,
|
||||
readOnly: false
|
||||
};
|
||||
}
|
||||
;
|
||||
});
|
||||
return res;
|
||||
}
|
||||
function collectAnswer(qprefix) {
|
||||
const inputs = document.getElementsByTagName("input");
|
||||
const textareas = document.getElementsByTagName("textarea");
|
||||
const selects = document.getElementsByTagName("select");
|
||||
let res = {};
|
||||
res = processNodes(res, inputs, qprefix);
|
||||
res = processNodes(res, textareas, qprefix);
|
||||
res = processNodes(res, selects, qprefix);
|
||||
return res;
|
||||
}
|
||||
function processNodes(res, nodes, qprefix) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const element = nodes[i];
|
||||
if (element.name.indexOf(qprefix + inputPrefix) === 0 && element.name.indexOf("_val") === -1) {
|
||||
if (element.type === "checkbox" || element.type === "radio") {
|
||||
if (element.checked) {
|
||||
res[element.name.slice((qprefix + inputPrefix).length)] = element.value;
|
||||
}
|
||||
} else {
|
||||
res[element.name.slice((qprefix + inputPrefix).length)] = element.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function send(qfile, qname, qprefix) {
|
||||
const http = new XMLHttpRequest();
|
||||
const url = stack_api_url + "/render";
|
||||
http.open("POST", url, true);
|
||||
http.setRequestHeader("Content-Type", "application/json");
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
try {
|
||||
const json = JSON.parse(http.responseText);
|
||||
if (json.message) {
|
||||
console.log(json);
|
||||
document.getElementById(`${qprefix + "errors"}`).innerText = json.message;
|
||||
return;
|
||||
} else {
|
||||
document.getElementById(`${qprefix + "errors"}`).innerText = "";
|
||||
}
|
||||
renameIframeHolders();
|
||||
let question = json.questionrender;
|
||||
const inputs = json.questioninputs;
|
||||
const seed = json.questionseed;
|
||||
let correctAnswers = "";
|
||||
for (const [name, input] of Object.entries(inputs)) {
|
||||
question = question.replace(`[[input:${name}]]`, input.render);
|
||||
question = question.replace(`[[validation:${name}]]`, `<span name='${qprefix + validationPrefix + name}'></span>`);
|
||||
question = question.replace(/javascript:download\(([^,]+?),([^,]+?)\)/, `javascript:download($1,$2, '${qfile}', '${qname}', '${qprefix}', ${seed})`);
|
||||
question = wrap_math(question);
|
||||
if (input.samplesolutionrender && name !== "remember") {
|
||||
correctAnswers += `<p>
|
||||
${stackstring["teacheranswershow_mcq"]} \\[{${input.samplesolutionrender}}\\],
|
||||
${stackstring["api_which_typed"]}: `;
|
||||
for (const [name2, solution] of Object.entries(input.samplesolution)) {
|
||||
if (name2.indexOf("_val") === -1) {
|
||||
correctAnswers += `<span class='correct-answer'>${wrap_math(solution)}</span>`;
|
||||
}
|
||||
}
|
||||
correctAnswers += ".</p>";
|
||||
} else if (name !== "remember") {
|
||||
for (const solution of Object.values(input.samplesolution)) {
|
||||
if (input.configuration.options) {
|
||||
correctAnswers += `<p class='correct-answer'>${input.configuration.options[solution]}</p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [name, file] of Object.entries(json.questionassets)) {
|
||||
const plotUrl = getPlotUrl(file);
|
||||
question = question.replace(name, plotUrl);
|
||||
json.questionsamplesolutiontext = json.questionsamplesolutiontext.replace(name, plotUrl);
|
||||
correctAnswers = correctAnswers.replace(name, plotUrl);
|
||||
}
|
||||
question = replaceFeedbackTags(question, qprefix);
|
||||
qoutput = document.getElementById(`${qprefix + "output"}`);
|
||||
qoutput.innerHTML = question;
|
||||
document.getElementById(`${qprefix + "stackapi_qtext"}`).style.display = "block";
|
||||
document.getElementById(`${qprefix + "stackapi_correct"}`).style.display = "block";
|
||||
for (const inputName of Object.keys(inputs)) {
|
||||
const inputElements = document.querySelectorAll(`[name^=${qprefix + inputPrefix + inputName}]`);
|
||||
for (const inputElement of Object.values(inputElements)) {
|
||||
inputElement.oninput = (event) => {
|
||||
const currentTimeout = timeOutHandler[event.target.id];
|
||||
if (currentTimeout) {
|
||||
window.clearTimeout(currentTimeout);
|
||||
}
|
||||
timeOutHandler[event.target.id] = window.setTimeout(validate.bind(null, event.target, qfile, qname, qprefix), 1e3);
|
||||
};
|
||||
}
|
||||
}
|
||||
let sampleText = json.questionsamplesolutiontext;
|
||||
if (sampleText) {
|
||||
sampleText = replaceFeedbackTags(sampleText, qprefix);
|
||||
document.getElementById(`${qprefix + "generalfeedback"}`).innerHTML = wrap_math(sampleText);
|
||||
document.getElementById(`${qprefix + "stackapi_generalfeedback"}`).style.display = "block";
|
||||
} else {
|
||||
document.getElementById(`${qprefix + "stackapi_generalfeedback"}`).style.display = "none";
|
||||
}
|
||||
document.getElementById(`${qprefix + "stackapi_score"}`).style.display = "none";
|
||||
document.getElementById(`${qprefix + "stackapi_validity"}`).innerText = "";
|
||||
const innerFeedback = document.getElementById(`${qprefix + "specificfeedback"}`);
|
||||
innerFeedback.innerHTML = "";
|
||||
innerFeedback.classList.remove("feedback");
|
||||
document.getElementById(`${qprefix + "formatcorrectresponse"}`).innerHTML = correctAnswers;
|
||||
document.getElementById(`${qprefix + "stackapi_generalfeedback"}`).style.display = "none";
|
||||
document.getElementById(`${qprefix + "stackapi_correct"}`).style.display = "none";
|
||||
createIframes(json.iframes);
|
||||
MathJax.typeset();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
document.getElementById(`${qprefix + "errors"}`).innerText = http.responseText;
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
collectData(qfile, qname, qprefix).then((data) => {
|
||||
let submitbutton = document.getElementById(`${qprefix + "stackapi_qtext"}`).querySelector('input[type="button"]');
|
||||
submitbutton.addEventListener("click", function() {
|
||||
answer(qfile, qname, qprefix, data.seed);
|
||||
});
|
||||
http.send(JSON.stringify(data));
|
||||
let questioncontainer = document.getElementById(`${qprefix + "stack"}`).parentElement;
|
||||
if (questioncontainer.getBoundingClientRect().top < 0) {
|
||||
questioncontainer.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
;
|
||||
});
|
||||
}
|
||||
function validate(element, qfile, qname, qprefix) {
|
||||
const http = new XMLHttpRequest();
|
||||
const url = stack_api_url + "/validate";
|
||||
http.open("POST", url, true);
|
||||
const answerNamePrefixTrim = (qprefix + inputPrefix).length;
|
||||
const answerName = element.name.slice(answerNamePrefixTrim).split("_", 1)[0];
|
||||
http.setRequestHeader("Content-Type", "application/json");
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
try {
|
||||
const json = JSON.parse(http.responseText);
|
||||
if (json.message) {
|
||||
document.getElementById(`${qprefix + "errors"}`).innerText = json.message;
|
||||
return;
|
||||
} else {
|
||||
document.getElementById(`${qprefix + "errors"}`).innerText = "";
|
||||
}
|
||||
renameIframeHolders();
|
||||
const validationHTML = json.validation;
|
||||
const element2 = document.getElementsByName(`${qprefix + validationPrefix + answerName}`)[0];
|
||||
element2.innerHTML = wrap_math(validationHTML);
|
||||
if (validationHTML) {
|
||||
element2.classList.add("validation");
|
||||
} else {
|
||||
element2.classList.remove("validation");
|
||||
}
|
||||
createIframes(json.iframes);
|
||||
MathJax.typeset();
|
||||
} catch (e) {
|
||||
document.getElementById(`${qprefix + "errors"}`).innerText = http.responseText;
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
collectData(qfile, qname, qprefix).then((data) => {
|
||||
data.inputName = answerName;
|
||||
http.send(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
function answer(qfile, qname, qprefix, seed) {
|
||||
const http = new XMLHttpRequest();
|
||||
const url = stack_api_url + "/grade";
|
||||
http.open("POST", url, true);
|
||||
if (!document.getElementById(`${qprefix + "output"}`).innerText) {
|
||||
return;
|
||||
}
|
||||
http.setRequestHeader("Content-Type", "application/json");
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
try {
|
||||
const json = JSON.parse(http.responseText);
|
||||
if (json.message) {
|
||||
document.getElementById(`${qprefix + "errors"}`).innerText = json.message;
|
||||
return;
|
||||
} else {
|
||||
document.getElementById(`${qprefix + "errors"}`).innerText = "";
|
||||
}
|
||||
if (!json.isgradable) {
|
||||
document.getElementById(`${qprefix + "stackapi_validity"}`).innerText = " " + stackstring["api_valid_all_parts"];
|
||||
return;
|
||||
}
|
||||
renameIframeHolders();
|
||||
document.getElementById(`${qprefix + "score"}`).innerText = (json.score * json.scoreweights.total).toFixed(2) + " " + stackstring["api_out_of"] + " " + json.scoreweights.total;
|
||||
document.getElementById(`${qprefix + "stackapi_score"}`).style.display = "block";
|
||||
document.getElementById(`${qprefix + "response_summary"}`).innerText = json.responsesummary;
|
||||
document.getElementById(`${qprefix + "stackapi_generalfeedback"}`).style.display = "block";
|
||||
document.getElementById(`${qprefix + "stackapi_summary"}`).style.display = "none";
|
||||
const feedback = json.prts;
|
||||
const specificFeedbackElement2 = document.getElementById(`${qprefix + "specificfeedback"}`);
|
||||
if (json.specificfeedback) {
|
||||
for (const [name, file] of Object.entries(json.gradingassets)) {
|
||||
json.specificfeedback = json.specificfeedback.replace(name, getPlotUrl(file));
|
||||
}
|
||||
json.specificfeedback = replaceFeedbackTags(json.specificfeedback, qprefix);
|
||||
specificFeedbackElement2.innerHTML = wrap_math(json.specificfeedback);
|
||||
specificFeedbackElement2.classList.add("feedback");
|
||||
} else {
|
||||
specificFeedbackElement2.classList.remove("feedback");
|
||||
}
|
||||
for (let [name, fb] of Object.entries(feedback)) {
|
||||
for (const [name2, file] of Object.entries(json.gradingassets)) {
|
||||
fb = fb.replace(name2, getPlotUrl(file));
|
||||
}
|
||||
const elements = document.getElementsByName(`${qprefix + feedbackPrefix + name}`);
|
||||
if (elements.length > 0) {
|
||||
const element = elements[0];
|
||||
if (json.scores[name] !== void 0) {
|
||||
fb = fb + `<div>${stackstring["api_marks_sub"]}:
|
||||
${(json.scores[name] * json.scoreweights[name] * json.scoreweights.total).toFixed(2)}
|
||||
/ ${(json.scoreweights[name] * json.scoreweights.total).toFixed(2)}.</div>`;
|
||||
}
|
||||
element.innerHTML = wrap_math(fb);
|
||||
}
|
||||
}
|
||||
createIframes(json.iframes);
|
||||
MathJax.typeset();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
document.getElementById(`${qprefix + "errors"}`).innerText = http.responseText;
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
const specificFeedbackElement = document.getElementById(`${qprefix + "specificfeedback"}`);
|
||||
specificFeedbackElement.innerHTML = "";
|
||||
specificFeedbackElement.classList.remove("feedback");
|
||||
document.getElementById(`${qprefix + "response_summary"}`).innerText = "";
|
||||
document.getElementById(`${qprefix + "stackapi_summary"}`).style.display = "none";
|
||||
const inputElements = document.querySelectorAll(`[name^=${qprefix + feedbackPrefix}]`);
|
||||
for (const inputElement of Object.values(inputElements)) {
|
||||
inputElement.innerHTML = "";
|
||||
inputElement.classList.remove("feedback");
|
||||
}
|
||||
document.getElementById(`${qprefix + "stackapi_score"}`).style.display = "none";
|
||||
document.getElementById(`${qprefix + "stackapi_validity"}`).innerText = "";
|
||||
collectData(qfile, qname, qprefix).then((data) => {
|
||||
data.seed = seed;
|
||||
http.send(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
function download(filename, fileid, qfile, qname, qprefix, seed) {
|
||||
const http = new XMLHttpRequest();
|
||||
const url = stack_api_url + "/download";
|
||||
http.open("POST", url, true);
|
||||
http.setRequestHeader("Content-Type", "application/json");
|
||||
http.filename = filename;
|
||||
http.fileid = fileid;
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
try {
|
||||
const blob = new Blob([http.responseText], { type: "application/octet-binary", endings: "native" });
|
||||
const selector = CSS.escape(`javascript:download('${http.filename}', ${http.fileid}, '${qfile}', '${qname}', '${qprefix}', ${seed})`);
|
||||
const linkElements = document.querySelectorAll(`a[href^=${selector}]`);
|
||||
const link = linkElements[0];
|
||||
link.setAttribute("href", URL.createObjectURL(blob));
|
||||
link.setAttribute("download", filename);
|
||||
link.click();
|
||||
} catch (e) {
|
||||
document.getElementById("errors").innerText = http.responseText;
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
collectData(qfile, qname, qprefix).then((data) => {
|
||||
data.filename = filename;
|
||||
data.fileid = fileid;
|
||||
data.seed = seed;
|
||||
http.send(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
function saveState(key, value) {
|
||||
if (typeof Storage !== "undefined") {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
}
|
||||
function loadState(key) {
|
||||
if (typeof Storage !== "undefined") {
|
||||
return localStorage.getItem(key) || "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function renameIframeHolders() {
|
||||
for (const iframe of document.querySelectorAll(`[id^=stack-iframe-holder]:not([id$=old]`)) {
|
||||
iframe.id = iframe.id + "_old";
|
||||
}
|
||||
}
|
||||
function createIframes(iframes) {
|
||||
const corsFragment = "/cors.php?name=";
|
||||
for (const iframe of iframes) {
|
||||
create_iframe(
|
||||
iframe[0],
|
||||
iframe[1].replaceAll(corsFragment, stack_api_url + corsFragment),
|
||||
...iframe.slice(2)
|
||||
);
|
||||
}
|
||||
}
|
||||
function replaceFeedbackTags(text, qprefix) {
|
||||
let result = text;
|
||||
const feedbackTags = text.match(/\[\[feedback:.*?\]\]/g);
|
||||
if (feedbackTags) {
|
||||
for (const tag of feedbackTags) {
|
||||
result = result.replace(tag, `<div name='${qprefix + feedbackPrefix + tag.slice(11, -2)}'></div>`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async function getQuestionFile(questionURL, questionName) {
|
||||
let res = "";
|
||||
if (questionURL) {
|
||||
await fetch(questionURL).then((result) => result.text()).then((result) => {
|
||||
res = loadQuestionFromFile(result, questionName);
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function loadQuestionFromFile(fileContents, questionName) {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(fileContents, "text/xml");
|
||||
let thequestion = null;
|
||||
let randSeed = "";
|
||||
for (const question of xmlDoc.getElementsByTagName("question")) {
|
||||
if (question.getAttribute("type").toLowerCase() === "stack" && (!questionName || question.querySelectorAll("name text")[0].textContent === questionName)) {
|
||||
thequestion = question.outerHTML;
|
||||
let seeds = question.querySelectorAll("deployedseed");
|
||||
if (seeds.length) {
|
||||
randSeed = parseInt(seeds[Math.floor(Math.random() * seeds.length)].textContent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { questionxml: setQuestion(thequestion), seed: randSeed };
|
||||
}
|
||||
function setQuestion(question) {
|
||||
return "<quiz>\n" + question + "\n</quiz>";
|
||||
}
|
||||
function createQuestionBlocks() {
|
||||
questionBlocks = document.getElementsByClassName("que stack");
|
||||
let i = 0;
|
||||
for (questionblock of questionBlocks) {
|
||||
i++;
|
||||
let questionPrefix = "q" + i.toString() + "_";
|
||||
var qfile = questionblock.dataset.qfile;
|
||||
var qname = questionblock.dataset.qname || "";
|
||||
questionblock.innerHTML = `
|
||||
<div class="collapsiblecontent" id=${questionPrefix + "stack"}>
|
||||
<div class="vstack gap-3 ms-3 col-lg-8">
|
||||
<div id=${questionPrefix + "errors"}></div>
|
||||
<div id=${questionPrefix + "stackapi_qtext"} class="col-lg-8" style="display: none">
|
||||
<!--<h2>${stackstring["questiontext"]}:</h2>-->
|
||||
<div id=${questionPrefix + "output"} class="formulation"></div>
|
||||
<div id=${questionPrefix + "specificfeedback"}></div>
|
||||
<br>
|
||||
<!-- <input type="button" onclick="answer('${qfile}', '${qname}', '${questionPrefix}')" class="btn btn-primary" value=${stackstring["api_submit"]}/>-->
|
||||
<input type="button" class="btn btn-primary" value=${stackstring["api_submit"]}/>
|
||||
<span id=${questionPrefix + "stackapi_validity"} style="color:darkred"></span>
|
||||
</div>
|
||||
<div id=${questionPrefix + "stackapi_generalfeedback"} class="col-lg-8" style="display: none">
|
||||
<h2>${stackstring["generalfeedback"]}:</h2>
|
||||
<div id=${questionPrefix + "generalfeedback"} class="feedback"></div>
|
||||
</div>
|
||||
<h2 id=${questionPrefix + "stackapi_score"} style="display: none">${stackstring["score"]}: <span id=${questionPrefix + "score"}></span></h2>
|
||||
<div id=${questionPrefix + "stackapi_summary"} class="col-lg-10" style="display: none">
|
||||
<h2>${stackstring["api_response"]}:</h2>
|
||||
<div id=${questionPrefix + "response_summary"} class="feedback"></div>
|
||||
</div>
|
||||
<div id=${questionPrefix + "stackapi_correct"} class="col-lg-10" style="display: none">
|
||||
<h2>${stackstring["api_correct"]}:</h2>
|
||||
<div id=${questionPrefix + "formatcorrectresponse"} class="feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id=${questionPrefix + "newquestionbutton"}>
|
||||
<input type="button" onclick="send('${qfile}', '${qname}', '${questionPrefix}')" class="btn btn-primary" value="Show new example question"/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
function addCollapsibles() {
|
||||
var collapsibles = document.querySelectorAll(".level2>h2, .stack>h2");
|
||||
for (let i = 0; i < collapsibles.length; i++) {
|
||||
collapsibles[i].addEventListener("click", () => collapseFunc(this));
|
||||
}
|
||||
}
|
||||
function collapseFunc(e) {
|
||||
e.classList.toggle("collapsed");
|
||||
}
|
||||
function stackSetup() {
|
||||
createQuestionBlocks();
|
||||
addCollapsibles();
|
||||
}
|
||||
function getPlotUrl(file) {
|
||||
return `${stack_api_url}/plots/${file}`;
|
||||
}
|
||||
//# sourceMappingURL=stackapicalls.js.map
|
||||
+390
@@ -0,0 +1,390 @@
|
||||
"use strict";
|
||||
/**
|
||||
* A javascript module to handle separation of author sourced scripts into
|
||||
* IFRAMES. All such scripts will have limited access to the actual document
|
||||
* on the VLE side and this script represents the VLE side endpoint for
|
||||
* message handling needed to give that access. When porting STACK onto VLEs
|
||||
* one needs to map this script to do the following:
|
||||
*
|
||||
* 1. Ensure that searches for target elements/inputs are limited to questions
|
||||
* and do not return any elements outside them.
|
||||
*
|
||||
* 2. Map any identifiers needed to identify inputs by name.
|
||||
*
|
||||
* 3. Any change handling related to input value modifications through this
|
||||
* logic gets connected to any such handling on the VLE side.
|
||||
*
|
||||
*
|
||||
* This script is intenttionally ordered so that the VLE specific bits should
|
||||
* be at the top.
|
||||
*
|
||||
*
|
||||
* This script assumes the following:
|
||||
*
|
||||
* 1. Each relevant IFRAME has an `id`-attribute that will be told to this
|
||||
* script.
|
||||
*
|
||||
* 2. Each such IFRAME exists within the question itself, so that one can
|
||||
* traverse up the DOM tree from that IFRAME to find the border of
|
||||
* the question.
|
||||
*
|
||||
* @module qtype_stack/stackjsvle
|
||||
* @copyright 2023 Aalto University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
let IFRAMES = {};
|
||||
let INPUTS = {};
|
||||
let INPUTS_INPUT_EVENT = {};
|
||||
let DISABLE_CHANGES = false;
|
||||
function vle_get_element(id) {
|
||||
let candidate = document.getElementById(id);
|
||||
let iter = candidate;
|
||||
while (iter && !iter.classList.contains("formulation")) {
|
||||
iter = iter.parentElement;
|
||||
}
|
||||
if (iter && iter.classList.contains("formulation")) {
|
||||
return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function vle_get_input_element(name, srciframe) {
|
||||
let initialcandidate = document.getElementById(srciframe);
|
||||
let iter = initialcandidate;
|
||||
while (iter && !iter.classList.contains("formulation")) {
|
||||
iter = iter.parentElement;
|
||||
}
|
||||
if (iter && iter.classList.contains("formulation")) {
|
||||
let possible2 = iter.querySelector('input[id$="_' + name + '"]');
|
||||
if (possible2 !== null) {
|
||||
return possible2;
|
||||
}
|
||||
possible2 = iter.querySelector('input[id$="_' + name + '_1"][type=radio]');
|
||||
if (possible2 !== null) {
|
||||
return possible2;
|
||||
}
|
||||
possible2 = iter.querySelector('select[id$="_' + name + '"]');
|
||||
if (possible2 !== null) {
|
||||
return possible2;
|
||||
}
|
||||
}
|
||||
let possible = document.querySelector('.formulation input[id$="_' + name + '"]');
|
||||
if (possible !== null) {
|
||||
return possible;
|
||||
}
|
||||
possible = document.querySelector('.formulation input[id$="_' + name + '_1"][type=radio]');
|
||||
if (possible !== null) {
|
||||
return possible;
|
||||
}
|
||||
possible = document.querySelector('.formulation select[id$="_' + name + '"]');
|
||||
return possible;
|
||||
}
|
||||
function vle_update_input(inputelement) {
|
||||
const c = new Event("change");
|
||||
inputelement.dispatchEvent(c);
|
||||
const i = new Event("input");
|
||||
inputelement.dispatchEvent(i);
|
||||
}
|
||||
function vle_update_dom(modifiedsubtreerootelement) {
|
||||
CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement);
|
||||
}
|
||||
function vle_html_sanitize(src) {
|
||||
let parser = new DOMParser();
|
||||
let doc = parser.parseFromString(src, "text/html");
|
||||
for (let el of doc.querySelectorAll("script, style")) {
|
||||
el.remove();
|
||||
}
|
||||
for (let el of doc.querySelectorAll("*")) {
|
||||
for (let { name, value } of el.attributes) {
|
||||
if (is_evil_attribute(name, value)) {
|
||||
el.removeAttribute(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return doc.body;
|
||||
}
|
||||
function is_evil_attribute(name, value) {
|
||||
const lcname = name.toLowerCase();
|
||||
if (lcname.startsWith("on")) {
|
||||
return true;
|
||||
}
|
||||
if (lcname === "src" || lcname.endsWith("href")) {
|
||||
const lcvalue = value.replace(/\s+/g, "").toLowerCase();
|
||||
if (lcvalue.includes("javascript:") || lcvalue.includes("data:text")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
window.addEventListener("message", (e) => {
|
||||
if (!(typeof e.data === "string" || e.data instanceof String)) {
|
||||
return;
|
||||
}
|
||||
let msg = null;
|
||||
try {
|
||||
msg = JSON.parse(e.data);
|
||||
} catch (e2) {
|
||||
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;
|
||||
let input = null;
|
||||
let response = {
|
||||
version: "STACK-JS:1.1.0"
|
||||
};
|
||||
switch (msg.type) {
|
||||
case "register-input-listener":
|
||||
input = vle_get_input_element(msg.name, msg.src);
|
||||
if (input === null) {
|
||||
response.type = "error";
|
||||
response.msg = 'Failed to connect to input: "' + msg.name + '"';
|
||||
response.tgt = msg.src;
|
||||
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), "*");
|
||||
return;
|
||||
}
|
||||
response.type = "initial-input";
|
||||
response.name = msg.name;
|
||||
response.tgt = msg.src;
|
||||
if (input.nodeName.toLowerCase() === "select") {
|
||||
response.value = input.value;
|
||||
response["input-type"] = "select";
|
||||
response["input-readonly"] = input.hasAttribute("disabled");
|
||||
} else if (input.type === "checkbox") {
|
||||
response.value = input.checked;
|
||||
response["input-type"] = "checkbox";
|
||||
response["input-readonly"] = input.hasAttribute("disabled");
|
||||
} else {
|
||||
response.value = input.value;
|
||||
response["input-type"] = input.type;
|
||||
response["input-readonly"] = input.hasAttribute("readonly");
|
||||
}
|
||||
if (input.type === "radio") {
|
||||
response["input-readonly"] = input.hasAttribute("disabled");
|
||||
response.value = "";
|
||||
for (let inp of document.querySelectorAll("input[type=radio][name=" + CSS.escape(input.name) + "]")) {
|
||||
if (inp.checked) {
|
||||
response.value = inp.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (input.id in INPUTS) {
|
||||
if (msg.src in INPUTS[input.id]) {
|
||||
return;
|
||||
}
|
||||
if (input.type !== "radio") {
|
||||
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 (input.type !== "radio") {
|
||||
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 (input.type !== "radio") {
|
||||
input.addEventListener("change", () => {
|
||||
if (DISABLE_CHANGES) {
|
||||
return;
|
||||
}
|
||||
let resp = {
|
||||
version: "STACK-JS:1.0.0",
|
||||
type: "changed-input",
|
||||
name: msg.name
|
||||
};
|
||||
if (input.type === "checkbox") {
|
||||
resp["value"] = input.checked;
|
||||
} else {
|
||||
resp["value"] = input.value;
|
||||
}
|
||||
for (let tgt of INPUTS[input.id]) {
|
||||
resp["tgt"] = tgt;
|
||||
IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), "*");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let radgroup = document.querySelectorAll("input[type=radio][name=" + CSS.escape(input.name) + "]");
|
||||
radgroup.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;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
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"] && input.type !== "radio") {
|
||||
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
|
||||
};
|
||||
if (input.type === "checkbox") {
|
||||
resp["value"] = input.checked;
|
||||
} else {
|
||||
resp["value"] = input.value;
|
||||
}
|
||||
for (let tgt of INPUTS_INPUT_EVENT[input.id]) {
|
||||
resp["tgt"] = tgt;
|
||||
IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), "*");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!(msg.src in INPUTS[input.id])) {
|
||||
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), "*");
|
||||
}
|
||||
break;
|
||||
case "changed-input":
|
||||
input = vle_get_input_element(msg.name, msg.src);
|
||||
if (input === null) {
|
||||
const ret = {
|
||||
version: "STACK-JS:1.0.0",
|
||||
type: "error",
|
||||
msg: 'Failed to modify input: "' + msg.name + '"',
|
||||
tgt: msg.src
|
||||
};
|
||||
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), "*");
|
||||
return;
|
||||
}
|
||||
DISABLE_CHANGES = true;
|
||||
if (input.type === "checkbox") {
|
||||
input.checked = msg.value;
|
||||
} else {
|
||||
input.value = msg.value;
|
||||
}
|
||||
vle_update_input(input);
|
||||
DISABLE_CHANGES = false;
|
||||
response.type = "changed-input";
|
||||
response.name = msg.name;
|
||||
response.value = msg.value;
|
||||
for (let tgt of INPUTS[input.id]) {
|
||||
if (tgt !== msg.src) {
|
||||
response.tgt = tgt;
|
||||
IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(response), "*");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "toggle-visibility":
|
||||
element = vle_get_element(msg.target);
|
||||
if (element === null) {
|
||||
const ret = {
|
||||
version: "STACK-JS:1.0.0",
|
||||
type: "error",
|
||||
msg: 'Failed to find element: "' + msg.target + '"',
|
||||
tgt: msg.src
|
||||
};
|
||||
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), "*");
|
||||
return;
|
||||
}
|
||||
if (msg.set === "show") {
|
||||
element.style.display = "block";
|
||||
vle_update_dom(element);
|
||||
} else if (msg.set === "hide") {
|
||||
element.style.display = "none";
|
||||
}
|
||||
break;
|
||||
case "change-content":
|
||||
element = vle_get_element(msg.target);
|
||||
if (element === null) {
|
||||
response.type = "error";
|
||||
response.msg = 'Failed to find element: "' + msg.target + '"';
|
||||
response.tgt = msg.src;
|
||||
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), "*");
|
||||
return;
|
||||
}
|
||||
element.replaceChildren(vle_html_sanitize(msg.content));
|
||||
vle_update_dom(element);
|
||||
break;
|
||||
case "get-content":
|
||||
element = vle_get_element(msg.target);
|
||||
response.type = "xfer-content";
|
||||
response.tgt = msg.src;
|
||||
response.target = msg.target;
|
||||
response.content = null;
|
||||
if (element !== null) {
|
||||
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":
|
||||
response.type = "ping";
|
||||
response.tgt = msg.src;
|
||||
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), "*");
|
||||
return;
|
||||
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), "*");
|
||||
}
|
||||
});
|
||||
function 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;
|
||||
if (scrolling === false) {
|
||||
frm.scrolling = "no";
|
||||
frm.style.overflow = "hidden";
|
||||
} else {
|
||||
frm.scrolling = "yes";
|
||||
}
|
||||
frm.title = title;
|
||||
frm.referrerpolicy = "no-referrer";
|
||||
if (!evil) {
|
||||
frm.sandbox = "allow-scripts allow-downloads";
|
||||
}
|
||||
frm.srcdoc = content;
|
||||
document.getElementById(targetdivid).replaceChildren(frm);
|
||||
IFRAMES[iframeid] = frm;
|
||||
}
|
||||
;
|
||||
//# sourceMappingURL=stackjsvle.js.map
|
||||
Reference in New Issue
Block a user