/** * 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 */ 'use strict'; // Note the VLE specific include of logic. /* All the IFRAMES have unique identifiers that they give in their * messages. But we only work with those that have been created by * our logic and are found from this map. */ let IFRAMES = {}; /* For event handling, lists of IFRAMES listening particular inputs. */ let INPUTS = {}; /* For event handling, lists of IFRAMES listening particular inputs * and their input events. By default we only listen to changes. * We report input events as changes to the other side. */ let INPUTS_INPUT_EVENT = {}; /* A flag to disable certain things. */ let DISABLE_CHANGES = false; /** * Returns an element with a given id, if an only if that element exists * inside a portion of DOM that represents a question. * * If not found or exists outside the restricted area then returns `null`. * * @param {String} id the identifier of the element we want. */ function vle_get_element(id) { /* In the case of Moodle we are happy as long as the element is inside something with the `formulation`-class. */ 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; } /** * Returns an input element with a given name, if and only if that element * exists inside a portion of DOM that represents a question. * * Note that, the input element may have a name that multiple questions * use and to pick the preferred element one needs to pick the one * within the same question as the IFRAME. * * Note that the input can also be a select. In the case of radio buttons * returning one of the possible buttons is enough. * * If not found or exists outside the restricted area then returns `null`. * * @param {String} name the name of the input we want * @param {String} srciframe the identifier of the iframe wanting it */ function vle_get_input_element(name, srciframe) { /* In the case of Moodle we are happy as long as the element is inside something with the `formulation`-class. */ let initialcandidate = document.getElementById(srciframe); let iter = initialcandidate; while (iter && !iter.classList.contains('formulation')) { iter = iter.parentElement; } if (iter && iter.classList.contains('formulation')) { // iter now represents the borders of the question containing // this IFRAME. let possible = iter.querySelector('input[id$="_' + name + '"]'); if (possible !== null) { return possible; } // Radios have interesting ids, but the name makes sense possible = iter.querySelector('input[id$="_' + name + '_1"][type=radio]'); if (possible !== null) { return possible; } possible = iter.querySelector('select[id$="_' + name + '"]'); if (possible !== null) { return possible; } } // If none found within the question itself, search everywhere. let possible = document.querySelector('.formulation input[id$="_' + name + '"]'); if (possible !== null) { return possible; } // Radios have interesting ids, but the name makes sense possible = document.querySelector('.formulation input[id$="_' + name + '_1"][type=radio]'); if (possible !== null) { return possible; } possible = document.querySelector('.formulation select[id$="_' + name + '"]'); return possible; } /** * Triggers any VLE specific scripting related to updates of the given * input element. * * @param {HTMLElement} inputelement the input element that has changed */ function vle_update_input(inputelement) { // Triggering a change event may be necessary. const c = new Event('change'); inputelement.dispatchEvent(c); // Also there are those that listen to input events. const i = new Event('input'); inputelement.dispatchEvent(i); } /** * Triggers any VLE specific scripting related to DOM updates. * * @param {HTMLElement} modifiedsubtreerootelement element under which changes may have happened. */ function vle_update_dom(modifiedsubtreerootelement) { CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement); } /** * Does HTML-string cleaning, i.e., removes any script payload. Returns * a DOM version of the given input string. * * This is used when receiving replacement content for a div. * * @param {String} src a raw string to sanitise */ function vle_html_sanitize(src) { // This can be implemented with many libraries or by custom code // however as this is typically a thing that a VLE might already have // tools for we have it at this level so that the VLE can use its own // tools that do things that the VLE developpers consider safe. // As Moodle does not currently seem to have such a sanitizer in // the core libraries, here is one implementation that shows what we // are looking for. // TO-DO: look into replacing this with DOMPurify or some such. let parser = new DOMParser(); let doc = parser.parseFromString(src, "text/html"); // First remove all