diff --git a/lang/en/admin.php b/lang/en/admin.php
index 90243b03e31..1cf92cdf7dc 100644
--- a/lang/en/admin.php
+++ b/lang/en/admin.php
@@ -140,6 +140,7 @@ $string['cliupgradefinished'] = 'Command line upgrade from {$a->oldversion} to {
$string['cliupgradenoneed'] = 'No upgrade needed for the installed version {$a}. Thanks for coming anyway!';
$string['cliupgradepending'] = 'An upgrade is pending';
$string['cliyesnoprompt'] = 'type y (means yes) or n (means no)';
+$string['close'] = 'Close';
$string['commentsperpage'] = 'Comments displayed per page';
$string['commonactivitysettings'] = 'Common activity settings';
$string['commonfiltersettings'] = 'Common filter settings';
diff --git a/lang/en/debug.php b/lang/en/debug.php
index 3e974935889..ef151f0e165 100644
--- a/lang/en/debug.php
+++ b/lang/en/debug.php
@@ -52,6 +52,17 @@ $string['notables'] = 'No tables!';
$string['outputbuffer'] = 'Output buffer';
$string['phpvaroff'] = 'The PHP server variable \'{$a->name}\' should be Off - {$a->link}';
$string['phpvaron'] = 'The PHP server variable \'{$a->name}\' is not turned On - {$a->link}';
+$string['reactive_instances'] = 'Reactive instances:';
+$string['reactive_noinstances'] = 'this page has no reactive instances';
+$string['reactive_pin'] = 'Pin';
+$string['reactive_unpin'] = 'Unpin';
+$string['reactive_highlightoff'] = 'Highlight OFF';
+$string['reactive_highlighton'] = 'Highlight ON';
+$string['reactive_readmodeon'] = 'Read mode ON';
+$string['reactive_readmodeoff'] = 'Read mode OFF';
+$string['reactive_resetpanel'] = 'Reset panel';
+$string['reactive_statedata'] = 'State data';
+$string['reactive_saveingwarning'] = 'Edit the state can cause inexpected results';
$string['sessionmissing'] = '{$a} object missing from session';
$string['sqlrelyonobsoletetable'] = 'This SQL relies on obsolete table(s): {$a}! Your code must be fixed by a developer.';
$string['stacktrace'] = 'Stack trace';
diff --git a/lib/amd/build/local/reactive/debug.min.js b/lib/amd/build/local/reactive/debug.min.js
new file mode 100644
index 00000000000..e7d0e697b66
--- /dev/null
+++ b/lib/amd/build/local/reactive/debug.min.js
@@ -0,0 +1,2 @@
+define ("core/local/reactive/debug",["exports","core/local/reactive/reactive","core/log"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.initDebug=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}function e(a){return i(a)||h(a)||g(a)||f()}function f(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function g(a,b){if(!a)return;if("string"==typeof a)return j(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return j(a,b)}function h(a){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(a))return Array.from(a)}function i(a){if(Array.isArray(a))return j(a)}function j(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * Reactive module debug tools.\n *\n * @module core/reactive/local/reactive/debug\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Reactive from 'core/local/reactive/reactive';\nimport log from 'core/log';\n\n// The list of reactives instances.\nconst reactiveInstances = {};\n\n// The reactive debugging objects.\nconst reactiveDebuggers = {};\n\n/**\n * Reactive module debug tools.\n *\n * If debug is enabled, this reactive module will spy all the reactive instances and keep a record\n * of the changes and components they have.\n *\n * It is important to note that the Debug class is also a Reactive module. The debug instance keeps\n * the reactive instances data as its own state. This way it is possible to implement development tools\n * that whatches this data.\n *\n * @class core/reactive/local/reactive/debug/Debug\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nclass Debug extends Reactive {\n\n /**\n * Set the initial state.\n *\n * @param {object} stateData the initial state data.\n */\n setInitialState(stateData) {\n super.setInitialState(stateData);\n log.debug(`Debug module \"M.reactive\" loaded.`);\n }\n\n /**\n * List the currents page reactives instances.\n */\n get list() {\n return JSON.parse(JSON.stringify(this.state.reactives));\n }\n\n /**\n * Register a new Reactive instance.\n *\n * This method is called every time a \"new Reactive\" is executed.\n *\n * @param {Reactive} instance the reactive instance\n */\n registerNewInstance(instance) {\n\n // Generate a valid variable name for that instance.\n let name = instance.name ?? `instance${this.state.reactives.length}`;\n name = name.replace(/\\W/g, '');\n\n log.debug(`Registering new reactive instance \"M.reactive.${name}\"`);\n\n reactiveInstances[name] = instance;\n reactiveDebuggers[name] = new DebugInstance(reactiveInstances[name]);\n // Register also in the state.\n this.dispatch('putInstance', name, instance);\n // Add debug watchers to instance.\n const refreshMethod = () => {\n this.dispatch('putInstance', name, instance);\n };\n instance.target.addEventListener('readmode:on', refreshMethod);\n instance.target.addEventListener('readmode:off', refreshMethod);\n instance.target.addEventListener('registerComponent:success', refreshMethod);\n instance.target.addEventListener('transaction:end', refreshMethod);\n // We store the last transaction into the state.\n const storeTransaction = ({detail}) => {\n const changes = detail?.changes;\n this.dispatch('lastTransaction', name, changes);\n };\n instance.target.addEventListener('transaction:start', storeTransaction);\n }\n\n /**\n * Returns a debugging object for a specific Reactive instance.\n *\n * A debugging object is a class that wraps a Reactive instance to quick access some of the\n * reactive methods using the browser JS console.\n *\n * @param {string} name the Reactive instance name\n * @returns {DebugInstance} a debug object wrapping the Reactive instance\n */\n debug(name) {\n return reactiveDebuggers[name];\n }\n}\n\n/**\n * The debug state mutations class.\n *\n * @class core/reactive/local/reactive/debug/Mutations\n */\nclass Mutations {\n\n /**\n * Insert or update a new instance into the debug state.\n *\n * @param {StateManager} stateManager the debug state manager\n * @param {string} name the instance name\n * @param {Reactive} instance the reactive instance\n */\n putInstance(stateManager, name, instance) {\n const state = stateManager.state;\n\n stateManager.setReadOnly(false);\n\n if (state.reactives.has(name)) {\n state.reactives.get(name).countcomponents = instance.components.length;\n state.reactives.get(name).readOnly = instance.stateManager.readonly;\n state.reactives.get(name).modified = new Date().getTime();\n } else {\n state.reactives.add({\n id: name,\n countcomponents: instance.components.length,\n readOnly: instance.stateManager.readonly,\n lastChanges: [],\n modified: new Date().getTime(),\n });\n }\n stateManager.setReadOnly(true);\n }\n\n /**\n * Update the lastChanges attribute with a list of changes\n *\n * @param {StateManager} stateManager the debug reactive state\n * @param {string} name tje instance name\n * @param {array} changes the list of changes\n */\n lastTransaction(stateManager, name, changes) {\n if (!changes || changes.length === 0) {\n return;\n }\n\n const state = stateManager.state;\n const lastChanges = ['transaction:start'];\n\n changes.forEach(change => {\n lastChanges.push(change.eventName);\n });\n\n lastChanges.push('transaction:end');\n\n stateManager.setReadOnly(false);\n\n state.reactives.get(name).lastChanges = lastChanges;\n\n stateManager.setReadOnly(true);\n }\n}\n\n/**\n * Class used to debug a specific instance and manipulate the state from the JS console.\n *\n * @class core/reactive/local/reactive/debug/DebugInstance\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nclass DebugInstance {\n\n /**\n * Constructor.\n *\n * @param {Reactive} instance the reactive instance\n */\n constructor(instance) {\n this.instance = instance;\n // Add some debug data directly into the instance. This way we avoid having attributes\n // that will confuse the console aoutocomplete.\n if (instance._reactiveDebugData === undefined) {\n instance._reactiveDebugData = {\n highlighted: false,\n };\n }\n }\n\n /**\n * Set the read only mode.\n *\n * Quick access to the instance setReadOnly method.\n *\n * @param {bool} value: the new read only value\n */\n set readOnly(value) {\n this.instance.stateManager.setReadOnly(value);\n }\n\n /**\n * Get the read only value\n *\n * @return {bool}\n */\n get readOnly() {\n return this.instance.stateManager.readonly;\n }\n\n /**\n * Return the current state object.\n *\n * @return {object}\n */\n get state() {\n return this.instance.state;\n }\n\n /**\n * Tooggle the reactive HTML element highlight registered in this reactive instance.\n *\n * @param {bool} value the highlight value\n */\n set highlight(value) {\n this.instance._reactiveDebugData.highlighted = value;\n this.instance.components.forEach(({element}) => {\n const border = (value) ? `thick solid #0000FF` : '';\n element.style.border = border;\n });\n }\n\n /**\n * Get the current highligh value.\n *\n * @return {bool}\n */\n get highlight() {\n return this.instance._reactiveDebugData.highlighted;\n }\n\n /**\n * List all the components registered in this instance.\n *\n * @return {array}\n */\n get components() {\n return [...this.instance.components];\n }\n\n /**\n * List all the state changes evenet pending to dispatch.\n *\n * @return {array}\n */\n get changes() {\n const result = [];\n this.instance.stateManager.eventsToPublish.forEach(\n (element) => {\n result.push(element.eventName);\n }\n );\n return result;\n }\n\n /**\n * Dispatch a change in the state.\n *\n * Usually reactive modules throw an error directly to the components when something\n * goes wrong. However, course editor can directly display a notification.\n *\n * @method dispatch\n * @param {string} actionName the action name (usually the mutation name)\n * @param {*} param any number of params the mutation needs.\n */\n async dispatch(...args) {\n this.instance.dispatch(...args);\n }\n\n /**\n * Return all the HTML elements registered in the instance components.\n *\n * @return {array}\n */\n get elements() {\n const result = [];\n this.instance.components.forEach(({element}) => {\n result.push(element);\n });\n return result;\n }\n\n /**\n * Return a plain copy of the state data.\n *\n * @return {object}\n */\n get stateData() {\n return JSON.parse(JSON.stringify(this.state));\n }\n\n /**\n * Process an update state array.\n *\n * @param {array} updates an array of update state messages\n */\n processUpdates(updates) {\n this.instance.stateManager.processUpdates(updates);\n }\n}\n\nconst stateChangedEventName = 'core_reactive_debug:stateChanged';\n\n/**\n * Internal state changed event.\n *\n * @method dispatchStateChangedEvent\n * @param {object} detail the full state\n * @param {object} target the custom event target (document if none provided)\n */\nfunction dispatchStateChangedEvent(detail, target) {\n if (target === undefined) {\n target = document;\n }\n target.dispatchEvent(\n new CustomEvent(\n stateChangedEventName,\n {\n bubbles: true,\n detail: detail,\n }\n )\n );\n}\n\n/**\n * The main init method to initialize the reactive debug.\n * @returns {object}\n */\nexport const initDebug = () => {\n const debug = new Debug({\n name: 'CoreReactiveDebug',\n eventName: stateChangedEventName,\n eventDispatch: dispatchStateChangedEvent,\n mutations: new Mutations(),\n state: {\n reactives: [],\n },\n });\n\n // The reactiveDebuggers will be used as a way of access the debug instances but also to register every new\n // instance. To ensure this will update the reactive debug state we add the registerNewInstance method to it.\n reactiveDebuggers.registerNewInstance = debug.registerNewInstance.bind(debug);\n\n return {\n debug,\n debuggers: reactiveDebuggers,\n };\n};\n"],"file":"debug.min.js"}
\ No newline at end of file
diff --git a/lib/amd/build/local/reactive/debugpanel.min.js b/lib/amd/build/local/reactive/debugpanel.min.js
new file mode 100644
index 00000000000..9b34561cc80
--- /dev/null
+++ b/lib/amd/build/local/reactive/debugpanel.min.js
@@ -0,0 +1,2 @@
+define ("core/local/reactive/debugpanel",["exports","core/reactive","core/log","core/utils"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.initsubpanel=a.init=void 0;c=function(a){return a&&a.__esModule?a:{default:a}}(c);function e(a,b){return k(a)||j(a,b)||g(a,b)||f()}function f(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function g(a,b){if(!a)return;if("string"==typeof a)return h(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return h(a,b)}function h(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * Reactive module debug panel.\n *\n * This module contains all the UI components for the reactive debug tools.\n * Those tools are only available if the debug is enables and could be used\n * from the footer.\n *\n * @module core/local/reactive/debugpanel\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop, debug} from 'core/reactive';\nimport log from 'core/log';\nimport {debounce} from 'core/utils';\n\n/**\n * Init the main reactive panel.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n */\nexport const init = (target, selectors) => {\n const element = document.getElementById(target);\n // Check if the debug reactive module is available.\n if (debug === undefined) {\n element.remove();\n return;\n }\n // Create the main component.\n new GlobalDebugPanel({\n element,\n reactive: debug,\n selectors,\n });\n};\n\n/**\n * Init an instance reactive subpanel.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n */\nexport const initsubpanel = (target, selectors) => {\n const element = document.getElementById(target);\n // Check if the debug reactive module is available.\n if (debug === undefined) {\n element.remove();\n return;\n }\n // Create the main component.\n new DebugInstanceSubpanel({\n element,\n reactive: debug,\n selectors,\n });\n};\n\n/**\n * Component for the main reactive dev panel.\n *\n * This component shows the list of reactive instances and handle the buttons\n * to open a specific instance panel.\n */\nclass GlobalDebugPanel extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'GlobalDebugPanel';\n // Default query selectors.\n this.selectors = {\n LOADERS: `[data-for='loaders']`,\n SUBPANEL: `[data-for='subpanel']`,\n LOG: `[data-for='log']`,\n };\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n stateReady(state) {\n if (state.reactives.size > 0) {\n this.getElement(this.selectors.LOADERS).innerHTML = '';\n }\n // Generate loading buttons.\n state.reactives.forEach(\n instance => {\n this._createLoader(instance);\n }\n );\n // Remove loading wheel.\n this.getElement(this.selectors.SUBPANEL).innerHTML = '';\n }\n\n /**\n * Create a debug panel button for a specific reactive instance.\n *\n * @param {object} instance hte instance data\n */\n _createLoader(instance) {\n const loaders = this.getElement(this.selectors.LOADERS);\n const btn = document.createElement(\"button\");\n btn.innerHTML = instance.id;\n btn.dataset.id = instance.id;\n loaders.appendChild(btn);\n // Add click event.\n this.addEventListener(btn, 'click', () => this._openPanel(btn, instance));\n }\n\n /**\n * Open a debug panel.\n *\n * @param {Element} btn the button element\n * @param {object} instance the instance data\n */\n async _openPanel(btn, instance) {\n try {\n const target = this.getElement(this.selectors.SUBPANEL);\n const data = {...instance};\n await this.renderComponent(target, 'core/local/reactive/debuginstancepanel', data);\n } catch (error) {\n log.error('Cannot load reactive debug subpanel');\n throw error;\n }\n }\n}\n\n/**\n * Component for the main reactive dev panel.\n *\n * This component shows the list of reactive instances and handle the buttons\n * to open a specific instance panel.\n */\nclass DebugInstanceSubpanel extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'DebugInstanceSubpanel';\n // Default query selectors.\n this.selectors = {\n NAME: `[data-for='name']`,\n CLOSE: `[data-for='close']`,\n READMODE: `[data-for='readmode']`,\n HIGHLIGHT: `[data-for='highlight']`,\n LOG: `[data-for='log']`,\n STATE: `[data-for='state']`,\n CLEAN: `[data-for='clean']`,\n PIN: `[data-for='pin']`,\n SAVE: `[data-for='save']`,\n INVALID: `[data-for='invalid']`,\n };\n this.id = this.element.dataset.id;\n this.controller = M.reactive[this.id];\n\n // The component is created always pinned.\n this.draggable = false;\n // We want the element to be dragged like modal.\n this.relativeDrag = true;\n // Save warning (will be loaded when state is ready.\n this.strings = {\n savewarning: '',\n };\n }\n\n /**\n * Initial state ready method.\n *\n */\n stateReady() {\n // Enable drag and drop.\n this.dragdrop = new DragDrop(this);\n\n // Close button.\n this.addEventListener(\n this.getElement(this.selectors.CLOSE),\n 'click',\n this.remove\n );\n // Highlight button.\n if (this.controller.highlight) {\n this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT));\n }\n this.addEventListener(\n this.getElement(this.selectors.HIGHLIGHT),\n 'click',\n () => {\n this.controller.highlight = !this.controller.highlight;\n this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT));\n }\n );\n // Edit mode button.\n this.addEventListener(\n this.getElement(this.selectors.READMODE),\n 'click',\n this._toggleEditMode\n );\n // Clean log and state.\n this.addEventListener(\n this.getElement(this.selectors.CLEAN),\n 'click',\n this._cleanAreas\n );\n // Unpin panel butotn.\n this.addEventListener(\n this.getElement(this.selectors.PIN),\n 'click',\n this._togglePin\n );\n // Save button, state format error message and state textarea.\n this.getElement(this.selectors.SAVE).disabled = true;\n\n this.addEventListener(\n this.getElement(this.selectors.STATE),\n 'keyup',\n debounce(this._checkJSON, 500)\n );\n\n this.addEventListener(\n this.getElement(this.selectors.SAVE),\n 'click',\n this._saveState\n );\n // Save the default save warning message.\n this.strings.savewarning = this.getElement(this.selectors.INVALID)?.innerHTML ?? '';\n // Add current state.\n this._refreshState();\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `reactives[${this.id}].lastChanges:updated`, handler: this._refreshLog},\n {watch: `reactives[${this.id}].modified:updated`, handler: this._refreshState},\n {watch: `reactives[${this.id}].readOnly:updated`, handler: this._refreshReadOnly},\n ];\n }\n\n /**\n * Wtacher method to refresh the log panel.\n *\n * @param {object} detail of the change\n */\n _refreshLog({element}) {\n const list = element?.lastChanges ?? [];\n\n const logContent = list.join(\"\\n\");\n // Append last log.\n const target = this.getElement(this.selectors.LOG);\n target.value += `\\n\\n= Transaction =\\n ${logContent}`;\n target.scrollTop = target.scrollHeight;\n }\n\n /**\n * Listener method to clean the log area.\n */\n _cleanAreas() {\n let target = this.getElement(this.selectors.LOG);\n target.value = '';\n\n this._refreshState();\n }\n\n /**\n * Watcher to refresh the state information.\n */\n _refreshState() {\n const target = this.getElement(this.selectors.STATE);\n target.value = JSON.stringify(this.controller.state, null, 4);\n }\n\n /**\n * Watcher to update the read only information.\n */\n _refreshReadOnly() {\n // Toggle the read mode button.\n const target = this.getElement(this.selectors.READMODE);\n if (target.dataset.readonly === undefined) {\n target.dataset.readonly = target.innerHTML;\n }\n if (this.controller.readOnly) {\n target.innerHTML = target.dataset.readonly;\n } else {\n target.innerHTML = target.dataset.alt;\n }\n }\n\n /**\n * Listener to toggle the edit mode of the component.\n */\n _toggleEditMode() {\n this.controller.readOnly = !this.controller.readOnly;\n }\n\n /**\n * Check that the edited state JSON is valid.\n *\n * Not all valid JSON are suitable for transforming the state. For example,\n * the first level attributes cannot change the type.\n *\n * @return {undefined|array} Array of state updates.\n */\n _checkJSON() {\n const invalid = this.getElement(this.selectors.INVALID);\n const save = this.getElement(this.selectors.SAVE);\n\n const edited = this.getElement(this.selectors.STATE).value;\n\n const currentStateData = this.controller.stateData;\n\n // Check if the json is tha same as state.\n if (edited == JSON.stringify(this.controller.state, null, 4)) {\n invalid.style.color = '';\n invalid.innerHTML = '';\n save.disabled = true;\n return undefined;\n }\n\n // Check if the json format is valid.\n try {\n const newState = JSON.parse(edited);\n // Check the first level did not change types.\n const result = this._generateStateUpdates(currentStateData, newState);\n // Enable save button.\n invalid.style.color = '';\n invalid.innerHTML = this.strings.savewarning;\n save.disabled = false;\n return result;\n } catch (error) {\n invalid.style.color = 'red';\n invalid.innerHTML = error.message ?? 'Invalid JSON sctructure';\n save.disabled = true;\n return undefined;\n }\n }\n\n /**\n * Listener to save the current edited state into the real state.\n */\n _saveState() {\n const updates = this._checkJSON();\n if (!updates) {\n return;\n }\n // Sent the updates to the state manager.\n this.controller.processUpdates(updates);\n }\n\n /**\n * Check that the edited state JSON is valid.\n *\n * Not all valid JSON are suitable for transforming the state. For example,\n * the first level attributes cannot change the type. This method do a two\n * steps comparison between the current state data and the new state data.\n *\n * A reactive state cannot be overridden like any other variable. To keep\n * the watchers updated is necessary to transform the current state into\n * the new one. As a result, this method generates all the necessary state\n * updates to convert the state into the new state.\n *\n * @param {object} currentStateData\n * @param {object} newStateData\n * @return {array} Array of state updates.\n * @throws {Error} is the structure is not compatible\n */\n _generateStateUpdates(currentStateData, newStateData) {\n\n const updates = [];\n\n const ids = {};\n\n // Step 1: Add all overrides newStateData.\n for (const [key, newValue] of Object.entries(newStateData)) {\n // Check is it is new.\n if (Array.isArray(newValue)) {\n ids[key] = {};\n newValue.forEach(element => {\n if (element.id === undefined) {\n throw Error(`Array ${key} element without id attribute`);\n }\n updates.push({\n name: key,\n action: 'override',\n fields: element,\n });\n const index = String(element.id).valueOf();\n ids[key][index] = true;\n });\n } else {\n updates.push({\n name: key,\n action: 'override',\n fields: newValue,\n });\n }\n }\n // Step 2: delete unnecesary data from currentStateData.\n for (const [key, oldValue] of Object.entries(currentStateData)) {\n let deleteField = false;\n // Check if the attribute is still there.\n if (newStateData[key] === undefined) {\n deleteField = true;\n }\n if (Array.isArray(oldValue)) {\n if (!deleteField && ids[key] === undefined) {\n throw Error(`Array ${key} cannot change to object.`);\n }\n oldValue.forEach(element => {\n const index = String(element.id).valueOf();\n let deleteEntry = deleteField;\n // Check if the id is there.\n if (!deleteEntry && ids[key][index] === undefined) {\n deleteEntry = true;\n }\n if (deleteEntry) {\n updates.push({\n name: key,\n action: 'delete',\n fields: element,\n });\n }\n });\n } else {\n if (!deleteField && ids[key] !== undefined) {\n throw Error(`Object ${key} cannot change to array.`);\n }\n if (deleteField) {\n updates.push({\n name: key,\n action: 'delete',\n fields: oldValue,\n });\n }\n }\n }\n // Delete all elements without action.\n return updates;\n }\n\n // Drag and drop methods.\n\n /**\n * Get the draggable data of this component.\n *\n * @returns {Object} exported course module drop data\n */\n getDraggableData() {\n return this.draggable;\n }\n\n /**\n * The element drop end hook.\n *\n * @param {Object} dropdata the dropdata\n * @param {Event} event the dropdata\n */\n dragEnd(dropdata, event) {\n this.element.style.top = `${event.newFixedTop}px`;\n this.element.style.left = `${event.newFixedLeft}px`;\n }\n\n /**\n * Pin and unpin the panel.\n */\n _togglePin() {\n this.draggable = !this.draggable;\n this.dragdrop.setDraggable(this.draggable);\n if (this.draggable) {\n this._unpin();\n } else {\n this._pin();\n }\n }\n\n /**\n * Unpin the panel form the footer.\n */\n _unpin() {\n // Find the initial spot.\n const pageCenterY = window.innerHeight / 2;\n const pageCenterX = window.innerWidth / 2;\n // Put the element in the middle of the screen\n const style = {\n position: 'fixed',\n resize: 'both',\n overflow: 'auto',\n height: '400px',\n width: '400px',\n top: `${pageCenterY - 200}px`,\n left: `${pageCenterX - 200}px`,\n };\n Object.assign(this.element.style, style);\n // Small also the text areas.\n this.getElement(this.selectors.STATE).style.height = '50px';\n this.getElement(this.selectors.LOG).style.height = '50px';\n\n this._toggleButtonText(this.getElement(this.selectors.PIN));\n }\n\n /**\n * Pin the panel into the footer.\n */\n _pin() {\n const props = [\n 'position',\n 'resize',\n 'overflow',\n 'top',\n 'left',\n 'height',\n 'width',\n ];\n props.forEach(\n prop => this.element.style.removeProperty(prop)\n );\n this._toggleButtonText(this.getElement(this.selectors.PIN));\n }\n\n /**\n * Toogle the button text with the data-alt value.\n *\n * @param {Element} element the button element\n */\n _toggleButtonText(element) {\n [element.innerHTML, element.dataset.alt] = [element.dataset.alt, element.innerHTML];\n }\n\n}\n"],"file":"debugpanel.min.js"}
\ No newline at end of file
diff --git a/lib/amd/build/local/reactive/reactive.min.js b/lib/amd/build/local/reactive/reactive.min.js
index 03886840701..1d8b80b51e2 100644
--- a/lib/amd/build/local/reactive/reactive.min.js
+++ b/lib/amd/build/local/reactive/reactive.min.js
@@ -1,2 +1,2 @@
-define ("core/local/reactive/reactive",["exports","core/log","core/local/reactive/statemanager","core/pending"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=e(b);c=e(c);d=e(d);function e(a){return a&&a.__esModule?a:{default:a}}function f(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function g(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function g(a){f(i,d,e,g,h,"next",a)}function h(a){f(i,d,e,g,h,"throw",a)}g(void 0)})}}function h(a,b){return n(a)||m(a,b)||k(a,b)||j()}function j(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function k(a,b){if(!a)return;if("string"==typeof a)return l(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return l(a,b)}function l(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);ca.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * A generic single state reactive module.\n *\n * @module core/reactive/local/reactive/reactive\n * @class core/reactive/local/reactive/reactive\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport log from 'core/log';\nimport StateManager from 'core/local/reactive/statemanager';\nimport Pending from 'core/pending';\n\n// Count the number of pending operations done to ensure we have a unique id for each one.\nlet pendingCount = 0;\n\n/**\n * Set up general reactive class to create a single state application with components.\n *\n * The reactive class is used for registering new UI components and manage the access to the state values\n * and mutations.\n *\n * When a new reactive instance is created, it will contain an empty state and and empty mutations\n * lists. When the state data is ready, the initial state can be loaded using the \"setInitialState\"\n * method. This will protect the state from writing and will trigger all the components \"stateReady\"\n * methods.\n *\n * State can only be altered by mutations. To replace all the mutations with a specific class,\n * use \"setMutations\" method. If you need to just add some new mutation methods, use \"addMutations\".\n *\n * To register new components into a reactive instance, use \"registerComponent\".\n *\n * Inside a component, use \"dispatch\" to invoke a mutation on the state (components can only access\n * the state in read only mode).\n */\nexport default class {\n\n /**\n * The component descriptor data structure.\n *\n * @typedef {object} description\n * @property {string} eventName the custom event name used for state changed events\n * @property {Function} eventDispatch the state update event dispatch function\n * @property {Element} [target] the target of the event dispatch. If not passed a fake element will be created\n * @property {Object} [mutations] an object with state mutations functions\n * @property {Object} [state] an object to initialize the state.\n */\n\n /**\n * Create a basic reactive manager.\n *\n * Note that if your state is not async loaded, you can pass directly on creation by using the\n * description.state attribute. However, this will initialize the state, this means\n * setInitialState will throw an exception because the state is already defined.\n *\n * @param {description} description reactive manager description.\n */\n constructor(description) {\n\n if (description.eventName === undefined || description.eventDispatch === undefined) {\n throw new Error(`Reactivity event required`);\n }\n\n if (description.name !== undefined) {\n this.name = description.name;\n }\n\n // Each reactive instance has its own element anchor to propagate state changes internally.\n // By default the module will create a fake DOM element to target custom events but\n // if all reactive components is constrait to a single element, this can be passed as\n // target in the description.\n this.target = description.target ?? document.createTextNode(null);\n\n this.eventName = description.eventName;\n this.eventDispatch = description.eventDispatch;\n\n // State manager is responsible for dispatch state change events when a mutation happens.\n this.stateManager = new StateManager(this.eventDispatch, this.target);\n\n // An internal registry of watchers and components.\n this.watchers = new Map([]);\n this.components = new Set([]);\n\n // Mutations can be overridden later using setMutations method.\n this.mutations = description.mutations ?? {};\n\n // Register the event to alert watchers when specific state change happens.\n this.target.addEventListener(this.eventName, this.callWatchersHandler.bind(this));\n\n // Add a pending operation waiting for the initial state.\n this.pendingState = new Pending(`core/reactive:registerInstance${pendingCount++}`);\n\n // Set initial state if we already have it.\n if (description.state !== undefined) {\n this.setInitialState(description.state);\n }\n }\n\n /**\n * State changed listener.\n *\n * This function take any state change and send it to the proper watchers.\n *\n * To prevent internal state changes from colliding with other reactive instances, only the\n * general \"state changed\" is triggered at document level. All the internal changes are\n * triggered at private target level without bubbling. This way any reactive instance can alert\n * only its own watchers.\n *\n * @param {CustomEvent} event\n */\n callWatchersHandler(event) {\n // Execute any registered component watchers.\n this.target.dispatchEvent(new CustomEvent(event.detail.action, {\n bubbles: false,\n detail: event.detail,\n }));\n }\n\n /**\n * Set the initial state.\n *\n * @param {object} stateData the initial state data.\n */\n setInitialState(stateData) {\n this.pendingState.resolve();\n this.stateManager.setInitialState(stateData);\n }\n\n /**\n * Add individual functions to the mutations.\n *\n * Note new mutations will be added to the existing ones. To replace the full mutation\n * object with a new one, use setMutations method.\n *\n * @method addMutations\n * @param {Object} newFunctions an object with new mutation functions.\n */\n addMutations(newFunctions) {\n // Mutations can provide an init method to do some setup in the statemanager.\n if (newFunctions.init !== undefined) {\n newFunctions.init(this.stateManager);\n }\n // Save all mutations.\n for (const [mutation, mutationFunction] of Object.entries(newFunctions)) {\n this.mutations[mutation] = mutationFunction.bind(newFunctions);\n }\n }\n\n /**\n * Replace the current mutations with a new object.\n *\n * This method is designed to override the full mutations class, for example by extending\n * the original one. To add some individual mutations, use addMutations instead.\n *\n * @param {object} manager the new mutations intance\n */\n setMutations(manager) {\n this.mutations = manager;\n // Mutations can provide an init method to do some setup in the statemanager.\n if (manager.init !== undefined) {\n manager.init(this.stateManager);\n }\n }\n\n /**\n * Return the current state.\n *\n * @return {object}\n */\n get state() {\n return this.stateManager.state;\n }\n\n /**\n * Return the initial state promise.\n *\n * Typically, components do not require to use this promise because registerComponent\n * will trigger their stateReady method automatically. But it could be useful for complex\n * components that require to combine state, template and string loadings.\n *\n * @method getState\n * @return {Promise}\n */\n getInitialStatePromise() {\n return this.stateManager.getInitialPromise();\n }\n\n /**\n * Register a new component.\n *\n * Component can provide some optional functions to the reactive module:\n * - getWatchers: returns an array of watchers\n * - stateReady: a method to call when the initial state is loaded\n *\n * It can also provide some optional attributes:\n * - name: the component name (default value: \"Unkown component\") to customize debug messages.\n *\n * The method will also use dispatchRegistrationSuccess and dispatchRegistrationFail. Those\n * are BaseComponent methods to inform parent components of the registration status.\n * Components should not override those methods.\n *\n * @method registerComponent\n * @param {object} component the new component\n * @property {string} [component.name] the component name to display in warnings and errors.\n * @property {Function} [component.dispatchRegistrationSuccess] method to notify registration success\n * @property {Function} [component.dispatchRegistrationFail] method to notify registration fail\n * @property {Function} [component.getWatchers] getter of the component watchers\n * @property {Function} [component.stateReady] method to call when the state is ready\n * @return {object} the registered component\n */\n registerComponent(component) {\n\n // Component name is an optional attribute to customize debug messages.\n const componentName = component.name ?? 'Unkown component';\n\n // Components can provide special methods to communicate registration to parent components.\n let dispatchSuccess = () => {\n return;\n };\n let dispatchFail = dispatchSuccess;\n if (component.dispatchRegistrationSuccess !== undefined) {\n dispatchSuccess = component.dispatchRegistrationSuccess.bind(component);\n }\n if (component.dispatchRegistrationFail !== undefined) {\n dispatchFail = component.dispatchRegistrationFail.bind(component);\n }\n\n // Components can be registered only one time.\n if (this.components.has(component)) {\n dispatchSuccess();\n return component;\n }\n\n // Components are fully registered only when the state ready promise is resolved.\n const pendingPromise = new Pending(`core/reactive:registerComponent${pendingCount++}`);\n\n // Keep track of the event listeners.\n let listeners = [];\n\n // Register watchers.\n let handlers = [];\n if (component.getWatchers !== undefined) {\n handlers = component.getWatchers();\n }\n handlers.forEach(({watch, handler}) => {\n\n if (watch === undefined) {\n dispatchFail();\n throw new Error(`Missing watch attribute in ${componentName} watcher`);\n }\n if (handler === undefined) {\n dispatchFail();\n throw new Error(`Missing handler for watcher ${watch} in ${componentName}`);\n }\n\n const listener = (event) => {\n handler.apply(component, [event.detail]);\n };\n\n // Save the listener information in case the component must be unregistered later.\n listeners.push({target: this.target, watch, listener});\n\n // The state manager triggers a general \"state changed\" event at a document level. However,\n // for the internal watchers, each component can listen to specific state changed custom events\n // in the target element. This way we can use the native event loop without colliding with other\n // reactive instances.\n this.target.addEventListener(watch, listener);\n });\n\n // Register state ready function. There's the possibility a component is registered after the initial state\n // is loaded. For those cases we have a state promise to handle this specific state change.\n if (component.stateReady !== undefined) {\n this.getInitialStatePromise()\n .then(state => {\n component.stateReady(state);\n pendingPromise.resolve();\n return true;\n })\n .catch(reason => {\n pendingPromise.resolve();\n log.error(`Initial state in ${componentName} rejected due to: ${reason}`);\n log.error(reason);\n });\n }\n\n // Save unregister data.\n this.watchers.set(component, listeners);\n this.components.add(component);\n\n dispatchSuccess();\n return component;\n }\n\n /**\n * Unregister a component and its watchers.\n *\n * @param {object} component the object instance to unregister\n * @returns {object} the deleted component\n */\n unregisterComponent(component) {\n if (!this.components.has(component)) {\n return component;\n }\n\n this.components.delete(component);\n\n // Remove event listeners.\n const listeners = this.watchers.get(component);\n if (listeners === undefined) {\n return component;\n }\n\n listeners.forEach(({target, watch, listener}) => {\n target.removeEventListener(watch, listener);\n });\n\n this.watchers.delete(component);\n\n return component;\n }\n\n /**\n * Dispatch a change in the state.\n *\n * This method is the only way for components to alter the state. Watchers will receive a\n * read only state to prevent illegal changes. If some user action require a state change, the\n * component should dispatch a mutation to trigger all the necessary logic to alter the state.\n *\n * @method dispatch\n * @param {string} actionName the action name (usually the mutation name)\n * @param {*} param any number of params the mutation needs.\n */\n async dispatch(actionName, ...params) {\n if (typeof actionName !== 'string') {\n throw new Error(`Dispatch action name must be a string`);\n }\n // JS does not have private methods yet. However, we prevent any component from calling\n // a method starting with \"_\" because the most accepted convention for private methods.\n if (actionName.charAt(0) === '_') {\n throw new Error(`Illegal Private ${actionName} mutation method dispatch`);\n }\n if (this.mutations[actionName] === undefined) {\n throw new Error(`Unkown ${actionName} mutation`);\n }\n\n const pendingPromise = new Pending(`core/reactive:${actionName}${pendingCount++}`);\n\n const mutationFunction = this.mutations[actionName];\n try {\n await mutationFunction.apply(this.mutations, [this.stateManager, ...params]);\n pendingPromise.resolve();\n } catch (error) {\n // Ensure the state is locked.\n this.stateManager.setReadOnly(true);\n pendingPromise.resolve();\n throw error;\n }\n }\n}\n"],"file":"reactive.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../../../src/local/reactive/reactive.js"],"names":["pendingCount","description","eventName","eventDispatch","Error","name","target","document","createTextNode","stateManager","StateManager","watchers","Map","components","Set","mutations","addEventListener","callWatchersHandler","bind","pendingState","Pending","state","setInitialState","M","reactive","registerNewInstance","event","dispatchEvent","CustomEvent","detail","action","bubbles","stateData","resolve","newFunctions","init","Object","entries","mutation","mutationFunction","manager","getInitialPromise","component","componentName","dispatchSuccess","dispatchFail","dispatchRegistrationSuccess","dispatchRegistrationFail","has","pendingPromise","listeners","handlers","getWatchers","forEach","watch","handler","listener","apply","push","stateReady","getInitialStatePromise","then","catch","reason","log","error","set","add","delete","get","removeEventListener","actionName","charAt","params","setReadOnly"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A generic single state reactive module.\n *\n * @module core/reactive/local/reactive/reactive\n * @class core/reactive/local/reactive/reactive\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport log from 'core/log';\nimport StateManager from 'core/local/reactive/statemanager';\nimport Pending from 'core/pending';\n\n// Count the number of pending operations done to ensure we have a unique id for each one.\nlet pendingCount = 0;\n\n/**\n * Set up general reactive class to create a single state application with components.\n *\n * The reactive class is used for registering new UI components and manage the access to the state values\n * and mutations.\n *\n * When a new reactive instance is created, it will contain an empty state and and empty mutations\n * lists. When the state data is ready, the initial state can be loaded using the \"setInitialState\"\n * method. This will protect the state from writing and will trigger all the components \"stateReady\"\n * methods.\n *\n * State can only be altered by mutations. To replace all the mutations with a specific class,\n * use \"setMutations\" method. If you need to just add some new mutation methods, use \"addMutations\".\n *\n * To register new components into a reactive instance, use \"registerComponent\".\n *\n * Inside a component, use \"dispatch\" to invoke a mutation on the state (components can only access\n * the state in read only mode).\n */\nexport default class {\n\n /**\n * The component descriptor data structure.\n *\n * @typedef {object} description\n * @property {string} eventName the custom event name used for state changed events\n * @property {Function} eventDispatch the state update event dispatch function\n * @property {Element} [target] the target of the event dispatch. If not passed a fake element will be created\n * @property {Object} [mutations] an object with state mutations functions\n * @property {Object} [state] an object to initialize the state.\n */\n\n /**\n * Create a basic reactive manager.\n *\n * Note that if your state is not async loaded, you can pass directly on creation by using the\n * description.state attribute. However, this will initialize the state, this means\n * setInitialState will throw an exception because the state is already defined.\n *\n * @param {description} description reactive manager description.\n */\n constructor(description) {\n\n if (description.eventName === undefined || description.eventDispatch === undefined) {\n throw new Error(`Reactivity event required`);\n }\n\n if (description.name !== undefined) {\n this.name = description.name;\n }\n\n // Each reactive instance has its own element anchor to propagate state changes internally.\n // By default the module will create a fake DOM element to target custom events but\n // if all reactive components is constrait to a single element, this can be passed as\n // target in the description.\n this.target = description.target ?? document.createTextNode(null);\n\n this.eventName = description.eventName;\n this.eventDispatch = description.eventDispatch;\n\n // State manager is responsible for dispatch state change events when a mutation happens.\n this.stateManager = new StateManager(this.eventDispatch, this.target);\n\n // An internal registry of watchers and components.\n this.watchers = new Map([]);\n this.components = new Set([]);\n\n // Mutations can be overridden later using setMutations method.\n this.mutations = description.mutations ?? {};\n\n // Register the event to alert watchers when specific state change happens.\n this.target.addEventListener(this.eventName, this.callWatchersHandler.bind(this));\n\n // Add a pending operation waiting for the initial state.\n this.pendingState = new Pending(`core/reactive:registerInstance${pendingCount++}`);\n\n // Set initial state if we already have it.\n if (description.state !== undefined) {\n this.setInitialState(description.state);\n }\n\n // Check if we have a debug instance to register the instance.\n if (M.reactive !== undefined) {\n M.reactive.registerNewInstance(this);\n }\n }\n\n /**\n * State changed listener.\n *\n * This function take any state change and send it to the proper watchers.\n *\n * To prevent internal state changes from colliding with other reactive instances, only the\n * general \"state changed\" is triggered at document level. All the internal changes are\n * triggered at private target level without bubbling. This way any reactive instance can alert\n * only its own watchers.\n *\n * @param {CustomEvent} event\n */\n callWatchersHandler(event) {\n // Execute any registered component watchers.\n this.target.dispatchEvent(new CustomEvent(event.detail.action, {\n bubbles: false,\n detail: event.detail,\n }));\n }\n\n /**\n * Set the initial state.\n *\n * @param {object} stateData the initial state data.\n */\n setInitialState(stateData) {\n this.pendingState.resolve();\n this.stateManager.setInitialState(stateData);\n }\n\n /**\n * Add individual functions to the mutations.\n *\n * Note new mutations will be added to the existing ones. To replace the full mutation\n * object with a new one, use setMutations method.\n *\n * @method addMutations\n * @param {Object} newFunctions an object with new mutation functions.\n */\n addMutations(newFunctions) {\n // Mutations can provide an init method to do some setup in the statemanager.\n if (newFunctions.init !== undefined) {\n newFunctions.init(this.stateManager);\n }\n // Save all mutations.\n for (const [mutation, mutationFunction] of Object.entries(newFunctions)) {\n this.mutations[mutation] = mutationFunction.bind(newFunctions);\n }\n }\n\n /**\n * Replace the current mutations with a new object.\n *\n * This method is designed to override the full mutations class, for example by extending\n * the original one. To add some individual mutations, use addMutations instead.\n *\n * @param {object} manager the new mutations intance\n */\n setMutations(manager) {\n this.mutations = manager;\n // Mutations can provide an init method to do some setup in the statemanager.\n if (manager.init !== undefined) {\n manager.init(this.stateManager);\n }\n }\n\n /**\n * Return the current state.\n *\n * @return {object}\n */\n get state() {\n return this.stateManager.state;\n }\n\n /**\n * Return the initial state promise.\n *\n * Typically, components do not require to use this promise because registerComponent\n * will trigger their stateReady method automatically. But it could be useful for complex\n * components that require to combine state, template and string loadings.\n *\n * @method getState\n * @return {Promise}\n */\n getInitialStatePromise() {\n return this.stateManager.getInitialPromise();\n }\n\n /**\n * Register a new component.\n *\n * Component can provide some optional functions to the reactive module:\n * - getWatchers: returns an array of watchers\n * - stateReady: a method to call when the initial state is loaded\n *\n * It can also provide some optional attributes:\n * - name: the component name (default value: \"Unkown component\") to customize debug messages.\n *\n * The method will also use dispatchRegistrationSuccess and dispatchRegistrationFail. Those\n * are BaseComponent methods to inform parent components of the registration status.\n * Components should not override those methods.\n *\n * @method registerComponent\n * @param {object} component the new component\n * @property {string} [component.name] the component name to display in warnings and errors.\n * @property {Function} [component.dispatchRegistrationSuccess] method to notify registration success\n * @property {Function} [component.dispatchRegistrationFail] method to notify registration fail\n * @property {Function} [component.getWatchers] getter of the component watchers\n * @property {Function} [component.stateReady] method to call when the state is ready\n * @return {object} the registered component\n */\n registerComponent(component) {\n\n // Component name is an optional attribute to customize debug messages.\n const componentName = component.name ?? 'Unkown component';\n\n // Components can provide special methods to communicate registration to parent components.\n let dispatchSuccess = () => {\n return;\n };\n let dispatchFail = dispatchSuccess;\n if (component.dispatchRegistrationSuccess !== undefined) {\n dispatchSuccess = component.dispatchRegistrationSuccess.bind(component);\n }\n if (component.dispatchRegistrationFail !== undefined) {\n dispatchFail = component.dispatchRegistrationFail.bind(component);\n }\n\n // Components can be registered only one time.\n if (this.components.has(component)) {\n dispatchSuccess();\n return component;\n }\n\n // Components are fully registered only when the state ready promise is resolved.\n const pendingPromise = new Pending(`core/reactive:registerComponent${pendingCount++}`);\n\n // Keep track of the event listeners.\n let listeners = [];\n\n // Register watchers.\n let handlers = [];\n if (component.getWatchers !== undefined) {\n handlers = component.getWatchers();\n }\n handlers.forEach(({watch, handler}) => {\n\n if (watch === undefined) {\n dispatchFail();\n throw new Error(`Missing watch attribute in ${componentName} watcher`);\n }\n if (handler === undefined) {\n dispatchFail();\n throw new Error(`Missing handler for watcher ${watch} in ${componentName}`);\n }\n\n const listener = (event) => {\n handler.apply(component, [event.detail]);\n };\n\n // Save the listener information in case the component must be unregistered later.\n listeners.push({target: this.target, watch, listener});\n\n // The state manager triggers a general \"state changed\" event at a document level. However,\n // for the internal watchers, each component can listen to specific state changed custom events\n // in the target element. This way we can use the native event loop without colliding with other\n // reactive instances.\n this.target.addEventListener(watch, listener);\n });\n\n // Register state ready function. There's the possibility a component is registered after the initial state\n // is loaded. For those cases we have a state promise to handle this specific state change.\n if (component.stateReady !== undefined) {\n this.getInitialStatePromise()\n .then(state => {\n component.stateReady(state);\n pendingPromise.resolve();\n return true;\n })\n .catch(reason => {\n pendingPromise.resolve();\n log.error(`Initial state in ${componentName} rejected due to: ${reason}`);\n log.error(reason);\n });\n }\n\n // Save unregister data.\n this.watchers.set(component, listeners);\n this.components.add(component);\n\n // Dispatch an event to communicate the registration to the debug module.\n this.target.dispatchEvent(new CustomEvent('registerComponent:success', {\n bubbles: false,\n detail: {component},\n }));\n\n dispatchSuccess();\n return component;\n }\n\n /**\n * Unregister a component and its watchers.\n *\n * @param {object} component the object instance to unregister\n * @returns {object} the deleted component\n */\n unregisterComponent(component) {\n if (!this.components.has(component)) {\n return component;\n }\n\n this.components.delete(component);\n\n // Remove event listeners.\n const listeners = this.watchers.get(component);\n if (listeners === undefined) {\n return component;\n }\n\n listeners.forEach(({target, watch, listener}) => {\n target.removeEventListener(watch, listener);\n });\n\n this.watchers.delete(component);\n\n return component;\n }\n\n /**\n * Dispatch a change in the state.\n *\n * This method is the only way for components to alter the state. Watchers will receive a\n * read only state to prevent illegal changes. If some user action require a state change, the\n * component should dispatch a mutation to trigger all the necessary logic to alter the state.\n *\n * @method dispatch\n * @param {string} actionName the action name (usually the mutation name)\n * @param {*} param any number of params the mutation needs.\n */\n async dispatch(actionName, ...params) {\n if (typeof actionName !== 'string') {\n throw new Error(`Dispatch action name must be a string`);\n }\n // JS does not have private methods yet. However, we prevent any component from calling\n // a method starting with \"_\" because the most accepted convention for private methods.\n if (actionName.charAt(0) === '_') {\n throw new Error(`Illegal Private ${actionName} mutation method dispatch`);\n }\n if (this.mutations[actionName] === undefined) {\n throw new Error(`Unkown ${actionName} mutation`);\n }\n\n const pendingPromise = new Pending(`core/reactive:${actionName}${pendingCount++}`);\n\n const mutationFunction = this.mutations[actionName];\n try {\n await mutationFunction.apply(this.mutations, [this.stateManager, ...params]);\n pendingPromise.resolve();\n } catch (error) {\n // Ensure the state is locked.\n this.stateManager.setReadOnly(true);\n pendingPromise.resolve();\n throw error;\n }\n }\n}\n"],"file":"reactive.min.js"}
\ No newline at end of file
diff --git a/lib/amd/build/local/reactive/statemanager.min.js b/lib/amd/build/local/reactive/statemanager.min.js
index 8b0cd8e54bc..b1b6afca721 100644
--- a/lib/amd/build/local/reactive/statemanager.min.js
+++ b/lib/amd/build/local/reactive/statemanager.min.js
@@ -1,2 +1,2 @@
-define ("core/local/reactive/statemanager",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function b(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){b=function(a){return typeof a}}else{b=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return b(a)}function c(a,b,e){if("undefined"!=typeof Reflect&&Reflect.get){c=Reflect.get}else{c=function(a,b,c){var e=d(a,b);if(!e)return;var f=Object.getOwnPropertyDescriptor(e,b);if(f.get){return f.get.call(c)}return f.value}}return c(a,b,e||a)}function d(a,b){while(!Object.prototype.hasOwnProperty.call(a,b)){a=n(a);if(null===a)break}return a}function e(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)m(a,b)}function f(a){return function(){var b=n(a),c;if(k()){var d=n(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return g(this,c)}}function g(a,c){if(c&&("object"===b(c)||"function"==typeof c)){return c}return h(a)}function h(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function i(a){var b="function"==typeof Map?new Map:void 0;i=function(a){if(null===a||!l(a))return a;if("function"!=typeof a){throw new TypeError("Super expression must either be null or a function")}if("undefined"!=typeof b){if(b.has(a))return b.get(a);b.set(a,c)}function c(){return j(a,arguments,n(this).constructor)}c.prototype=Object.create(a.prototype,{constructor:{value:c,enumerable:!1,writable:!0,configurable:!0}});return m(c,a)};return i(a)}function j(){if(k()){j=Reflect.construct}else{j=function(b,c,d){var e=[null];e.push.apply(e,c);var a=Function.bind.apply(b,e),f=new a;if(d)m(f,d.prototype);return f}}return j.apply(null,arguments)}function k(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function l(a){return-1!==Function.toString.call(a).indexOf("[native code]")}function m(a,b){m=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return m(a,b)}function n(a){n=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return n(a)}function o(a,b){return t(a)||s(a,b)||q(a,b)||p()}function p(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function q(a,b){if(!a)return;if("string"==typeof a)return r(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return r(a,b)}function r(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);ca.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * Reactive simple state manager.\n *\n * The state manager contains the state data, trigger update events and\n * can lock and unlock the state data.\n *\n * This file contains the three main elements of the state manager:\n * - State manager: the public class to alter the state, dispatch events and process update messages.\n * - Proxy handler: a private class to keep track of the state object changes.\n * - StateMap class: a private class extending Map class that triggers event when a state list is modifed.\n *\n * @module core/local/reactive/stateManager\n * @class core/local/reactive/stateManager\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * State manager class.\n *\n * This class handle the reactive state and ensure only valid mutations can modify the state.\n * It also provide methods to apply batch state update messages (see processUpdates function doc\n * for more details on update messages).\n *\n * Implementing a deep state manager is complex and will require many frontend resources. To keep\n * the state fast and simple, the state can ONLY store two kind of data:\n * - Object with attributes\n * - Sets of objects with id attributes.\n *\n * This is an example of a valid state:\n *\n * {\n * course: {\n * name: 'course name',\n * shortname: 'courseshort',\n * sectionlist: [21, 34]\n * },\n * sections: [\n * {id: 21, name: 'Topic 1', visible: true},\n * {id: 34, name: 'Topic 2', visible: false,\n * ],\n * }\n *\n * The following cases are NOT allowed at a state ROOT level (throws an exception if they are assigned):\n * - Simple values (strings, boolean...).\n * - Arrays of simple values.\n * - Array of objects without ID attribute (all arrays will be converted to maps and requires an ID).\n *\n * Thanks to those limitations it can simplify the state update messages and the event names. If You\n * need to store simple data, just group them in an object.\n *\n * To grant any state change triggers the proper events, the class uses two private structures:\n * - proxy handler: any object stored in the state is proxied using this class.\n * - StateMap class: any object set in the state will be converted to StateMap using the\n * objects id attribute.\n */\nexport default class StateManager {\n\n /**\n * Create a basic reactive state store.\n *\n * The state manager is meant to work with native JS events. To ensure each reactive module can use\n * it in its own way, the parent element must provide a valid event dispatcher function and an optional\n * DOM element to anchor the event.\n *\n * @param {function} dispatchEvent the function to dispatch the custom event when the state changes.\n * @param {element} target the state changed custom event target (document if none provided)\n */\n constructor(dispatchEvent, target) {\n\n // The dispatch event function.\n /** @package */\n this.dispatchEvent = dispatchEvent;\n\n // The DOM container to trigger events.\n /** @package */\n this.target = target ?? document;\n\n // State can be altered freely until initial state is set.\n /** @package */\n this.readonly = false;\n\n // List of state changes pending to be published as events.\n /** @package */\n this.eventsToPublish = [];\n\n // The update state types functions.\n /** @package */\n this.updateTypes = {\n \"create\": this.defaultCreate.bind(this),\n \"update\": this.defaultUpdate.bind(this),\n \"delete\": this.defaultDelete.bind(this),\n \"put\": this.defaultPut.bind(this),\n \"override\": this.defaultOverride.bind(this),\n \"prepareFields\": this.defaultPrepareFields.bind(this),\n };\n\n // The state_loaded event is special because it only happens one but all components\n // may react to that state, even if they are registered after the setIinitialState.\n // For these reason we use a promise for that event.\n this.initialPromise = new Promise((resolve) => {\n const initialStateDone = (event) => {\n resolve(event.detail.state);\n };\n this.target.addEventListener('state:loaded', initialStateDone);\n });\n }\n\n /**\n * Loads the initial state.\n *\n * Note this method will trigger a state changed event with \"state:loaded\" actionname.\n *\n * The state mode will be set to read only when the initial state is loaded.\n *\n * @param {object} initialState\n */\n setInitialState(initialState) {\n\n if (this.state !== undefined) {\n throw Error('Initial state can only be initialized ones');\n }\n\n // Create the state object.\n const state = new Proxy({}, new Handler('state', this, true));\n for (const [prop, propValue] of Object.entries(initialState)) {\n state[prop] = propValue;\n }\n this.state = state;\n\n // When the state is loaded we can lock it to prevent illegal changes.\n this.readonly = true;\n\n this.dispatchEvent({\n action: 'state:loaded',\n state: this.state,\n }, this.target);\n }\n\n /**\n * Generate a promise that will be resolved when the initial state is loaded.\n *\n * In most cases the final state will be loaded using an ajax call. This is the reason\n * why states manager are created unlocked and won't be reactive until the initial state is set.\n *\n * @return {Promise} the resulting promise\n */\n getInitialPromise() {\n return this.initialPromise;\n }\n\n /**\n * Locks or unlocks the state to prevent illegal updates.\n *\n * Mutations use this method to modify the state. Once the state is updated, they must\n * block again the state.\n *\n * All changes done while the state is writable will be registered using registerStateAction.\n * When the state is set again to read only the method will trigger _publishEvents to communicate\n * changes to all watchers.\n *\n * @param {bool} readonly if the state is in read only mode enabled\n */\n setReadOnly(readonly) {\n\n this.readonly = readonly;\n\n // When the state is in readonly again is time to publish all pending events.\n if (this.readonly) {\n this._publishEvents();\n }\n }\n\n /**\n * Add methods to process update state messages.\n *\n * The state manager provide a default update, create and delete methods. However,\n * some applications may require to override the default methods or even add new ones\n * like \"refresh\" or \"error\".\n *\n * @param {Object} newFunctions the new update types functions.\n */\n addUpdateTypes(newFunctions) {\n for (const [updateType, updateFunction] of Object.entries(newFunctions)) {\n if (typeof updateFunction === 'function') {\n this.updateTypes[updateType] = updateFunction.bind(newFunctions);\n }\n }\n }\n\n /**\n * Process a state updates array and do all the necessary changes.\n *\n * Note this method unlocks the state while it is executing and relocks it\n * when finishes.\n *\n * @param {array} updates\n * @param {Object} updateTypes optional functions to override the default update types.\n */\n processUpdates(updates, updateTypes) {\n if (!Array.isArray(updates)) {\n throw Error('State updates must be an array');\n }\n this.setReadOnly(false);\n updates.forEach((update) => {\n if (update.name === undefined) {\n throw Error('Missing state update name');\n }\n this.processUpdate(\n update.name,\n update.action,\n update.fields,\n updateTypes\n );\n });\n this.setReadOnly(true);\n }\n\n /**\n * Process a single state update.\n *\n * Note this method will not lock or unlock the state by itself.\n *\n * @param {string} updateName the state element to update\n * @param {string} action to action to perform\n * @param {object} fields the new data\n * @param {Object} updateTypes optional functions to override the default update types.\n */\n processUpdate(updateName, action, fields, updateTypes) {\n\n if (!fields) {\n throw Error('Missing state update fields');\n }\n\n if (updateTypes === undefined) {\n updateTypes = {};\n }\n\n action = action ?? 'update';\n\n const method = updateTypes[action] ?? this.updateTypes[action];\n\n if (method === undefined) {\n throw Error(`Unkown update action ${action}`);\n }\n\n // Some state data may require some cooking before sending to the\n // state. Reactive instances can overrdide the default fieldDefaults\n // method to add extra logic to all updates.\n const prepareFields = updateTypes.prepareFields ?? this.updateTypes.prepareFields;\n\n method(this, updateName, prepareFields(this, updateName, fields));\n }\n\n /**\n * Prepare fields for processing.\n *\n * This method is used to add default values or calculations from the frontend side.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n * @returns {Object} final fields data\n */\n defaultPrepareFields(stateManager, updateName, fields) {\n return fields;\n }\n\n\n /**\n * Process a create state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultCreate(stateManager, updateName, fields) {\n\n let state = stateManager.state;\n\n // Create can be applied only to lists, not to objects.\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n\n /**\n * Process a delete state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultDelete(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n throw Error(`Inexistent ${updateName} ${fields.id}`);\n }\n\n // Process deletion.\n let state = stateManager.state;\n\n if (state[updateName] instanceof StateMap) {\n state[updateName].delete(fields.id);\n return;\n }\n delete state[updateName];\n }\n\n /**\n * Process a update state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultUpdate(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n throw Error(`Inexistent ${updateName} ${fields.id}`);\n }\n\n // Execute updates.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n }\n\n /**\n * Process a put state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultPut(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (current) {\n // Update attributes.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n } else {\n // Create new object.\n let state = stateManager.state;\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n }\n\n /**\n * Process an override state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultOverride(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (current) {\n // Remove any unnecessary fields.\n for (const [fieldName] of Object.entries(current)) {\n if (fields[fieldName] === undefined) {\n delete current[fieldName];\n }\n }\n // Update field.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n } else {\n // Create the element if not exists.\n let state = stateManager.state;\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n }\n\n /**\n * Get an element from the state or form an alternative state object.\n *\n * The altstate param is used by external update functions that gets the current\n * state as param.\n *\n * @param {String} name the state object name\n * @param {*} id and object id for state maps.\n * @return {Object|undefined} the state object found\n */\n get(name, id) {\n const state = this.state;\n\n let current = state[name];\n if (current instanceof StateMap) {\n if (id === undefined) {\n throw Error(`Missing id for ${name} state update`);\n }\n current = state[name].get(id);\n }\n\n return current;\n }\n\n /**\n * Register a state modification and generate the necessary events.\n *\n * This method is used mainly by proxy helpers to dispatch state change event.\n * However, mutations can use it to inform components about non reactive changes\n * in the state (only the two first levels of the state are reactive).\n *\n * Each action can produce several events:\n * - The specific attribute updated, created or deleter (example: \"cm.visible:updated\")\n * - The general state object updated, created or deleted (example: \"cm:updated\")\n * - If the element has an ID attribute, the specific event with id (example: \"cm[42].visible:updated\")\n * - If the element has an ID attribute, the general event with id (example: \"cm[42]:updated\")\n * - A generic state update event \"state:update\"\n *\n * @param {string} field the affected state field name\n * @param {string|null} prop the affecter field property (null if affect the full object)\n * @param {string} action the action done (created/updated/deleted)\n * @param {*} data the affected data\n */\n registerStateAction(field, prop, action, data) {\n\n let parentAction = 'updated';\n\n if (prop !== null) {\n this.eventsToPublish.push({\n eventName: `${field}.${prop}:${action}`,\n eventData: data,\n action,\n });\n } else {\n parentAction = action;\n }\n\n // Trigger extra events if the element has an ID attribute.\n if (data.id !== undefined) {\n if (prop !== null) {\n this.eventsToPublish.push({\n eventName: `${field}[${data.id}].${prop}:${action}`,\n eventData: data,\n action,\n });\n }\n this.eventsToPublish.push({\n eventName: `${field}[${data.id}]:${parentAction}`,\n eventData: data,\n action: parentAction,\n });\n }\n\n // Register the general change.\n this.eventsToPublish.push({\n eventName: `${field}:${parentAction}`,\n eventData: data,\n action: parentAction,\n });\n\n // Register state updated event.\n this.eventsToPublish.push({\n eventName: `state:updated`,\n eventData: data,\n action: 'updated',\n });\n }\n\n /**\n * Internal method to publish events.\n *\n * This is a private method, it will be invoked when the state is set back to read only mode.\n */\n _publishEvents() {\n const fieldChanges = this.eventsToPublish;\n this.eventsToPublish = [];\n\n // Dispatch a transaction start event.\n this.dispatchEvent({\n action: 'transaction:start',\n state: this.state,\n element: null,\n }, this.target);\n\n // State changes can be registered in any order. However it will avoid many\n // components errors if they are sorted to have creations-updates-deletes in case\n // some component needs to create or destroy DOM elements before updating them.\n fieldChanges.sort((a, b) => {\n const weights = {\n created: 0,\n updated: 1,\n deleted: 2,\n };\n const aweight = weights[a.action] ?? 0;\n const bweight = weights[b.action] ?? 0;\n // In case both have the same weight, the eventName length decide.\n if (aweight === bweight) {\n return a.eventName.length - b.eventName.length;\n }\n return aweight - bweight;\n });\n\n // List of the published events to prevent redundancies.\n let publishedEvents = new Set();\n\n fieldChanges.forEach((event) => {\n\n const eventkey = `${event.eventName}.${event.eventData.id ?? 0}`;\n\n if (!publishedEvents.has(eventkey)) {\n this.dispatchEvent({\n action: event.eventName,\n state: this.state,\n element: event.eventData\n }, this.target);\n\n publishedEvents.add(eventkey);\n }\n });\n\n // Dispatch a transaction end event.\n this.dispatchEvent({\n action: 'transaction:end',\n state: this.state,\n element: null,\n }, this.target);\n }\n}\n\n// Proxy helpers.\n\n/**\n * The proxy handler.\n *\n * This class will inform any value change directly to the state manager.\n *\n * The proxied variable will throw an error if it is altered when the state manager is\n * in read only mode.\n */\nclass Handler {\n\n /**\n * Class constructor.\n *\n * @param {string} name the variable name used for identify triggered actions\n * @param {StateManager} stateManager the state manager object\n * @param {boolean} proxyValues if new values must be proxied (used only at state root level)\n */\n constructor(name, stateManager, proxyValues) {\n this.name = name;\n this.stateManager = stateManager;\n this.proxyValues = proxyValues ?? false;\n }\n\n /**\n * Set trap to trigger events when the state changes.\n *\n * @param {object} obj the source object (not proxied)\n * @param {string} prop the attribute to set\n * @param {*} value the value to save\n * @param {*} receiver the proxied element to be attached to events\n * @returns {boolean} if the value is set\n */\n set(obj, prop, value, receiver) {\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${prop} value in ${this.name}.`);\n }\n\n // Check any data change.\n if (JSON.stringify(obj[prop]) === JSON.stringify(value)) {\n return true;\n }\n\n const action = (obj[prop] !== undefined) ? 'updated' : 'created';\n\n // Proxy value if necessary (used at state root level).\n if (this.proxyValues) {\n if (Array.isArray(value)) {\n obj[prop] = new StateMap(prop, this.stateManager).loadValues(value);\n } else {\n obj[prop] = new Proxy(value, new Handler(prop, this.stateManager));\n }\n } else {\n obj[prop] = value;\n }\n\n // If the state is not ready yet means the initial state is not yet loaded.\n if (this.stateManager.state === undefined) {\n return true;\n }\n\n this.stateManager.registerStateAction(this.name, prop, action, receiver);\n\n return true;\n }\n\n /**\n * Delete property trap to trigger state change events.\n *\n * @param {*} obj the affected object (not proxied)\n * @param {*} prop the prop to delete\n * @returns {boolean} if prop is deleted\n */\n deleteProperty(obj, prop) {\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to delete ${prop} in ${this.name}.`);\n }\n if (prop in obj) {\n\n delete obj[prop];\n\n this.stateManager.registerStateAction(this.name, prop, 'deleted', obj);\n }\n return true;\n }\n}\n\n/**\n * Class to add events dispatching to the JS Map class.\n *\n * When the state has a list of objects (with IDs) it will be converted into a StateMap.\n * StateMap is used almost in the same way as a regular JS map. Because all elements have an\n * id attribute, it has some specific methods:\n * - add: a convenient method to add an element without specifying the key (\"id\" attribute will be used as a key).\n * - loadValues: to add many elements at once wihout specifying keys (\"id\" attribute will be used).\n *\n * Apart, the main difference between regular Map and MapState is that this one will inform any change to the\n * state manager.\n */\nclass StateMap extends Map {\n\n /**\n * Create a reactive Map.\n *\n * @param {string} name the property name\n * @param {StateManager} stateManager the state manager\n * @param {iterable} iterable an iterable object to create the Map\n */\n constructor(name, stateManager, iterable) {\n // We don't have any \"this\" until be call super.\n super(iterable);\n this.name = name;\n this.stateManager = stateManager;\n }\n\n /**\n * Set an element into the map.\n *\n * Each value needs it's own id attribute. Objects without id will be rejected.\n * The function will throw an error if the value id and the key are not the same.\n *\n * @param {*} key the key to store\n * @param {*} value the value to store\n * @returns {Map} the resulting Map object\n */\n set(key, value) {\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n }\n\n // Normalize keys as string to prevent json decoding errors.\n key = this.normalizeKey(key);\n\n this.checkValue(value);\n\n if (key === undefined || key === null) {\n throw Error('State lists keys cannot be null or undefined');\n }\n\n // ID is mandatory and should be the same as the key.\n if (this.normalizeKey(value.id) !== key) {\n throw new Error(`State error: ${this.name} list element ID (${value.id}) and key (${key}) mismatch`);\n }\n\n const action = (super.has(key)) ? 'updated' : 'created';\n\n // Save proxied data into the list.\n const result = super.set(key, new Proxy(value, new Handler(this.name, this.stateManager)));\n\n // If the state is not ready yet means the initial state is not yet loaded.\n if (this.stateManager.state === undefined) {\n return result;\n }\n\n this.stateManager.registerStateAction(this.name, null, action, super.get(key));\n\n return result;\n }\n\n /**\n * Check if a value is valid to be stored in a a State List.\n *\n * Only objects with id attribute can be stored in State lists.\n *\n * This method throws an error if the value is not valid.\n *\n * @param {object} value (with ID)\n */\n checkValue(value) {\n if (!typeof value === 'object' && value !== null) {\n throw Error('State lists can contain objects only');\n }\n\n if (value.id === undefined) {\n throw Error('State lists elements must contain at least an id attribute');\n }\n }\n\n /**\n * Return a normalized key value for state map.\n *\n * Regular maps uses strict key comparissons but state maps are indexed by ID.JSON conversions\n * and webservices sometimes do unexpected types conversions so we convert any integer key to string.\n *\n * @param {*} key the provided key\n * @returns {string}\n */\n normalizeKey(key) {\n return String(key).valueOf();\n }\n\n /**\n * Insert a new element int a list.\n *\n * Each value needs it's own id attribute. Objects withouts id will be rejected.\n *\n * @param {object} value the value to add (needs an id attribute)\n * @returns {Map} the resulting Map object\n */\n add(value) {\n this.checkValue(value);\n return this.set(value.id, value);\n }\n\n /**\n * Return a state map element.\n *\n * @param {*} key the element id\n * @return {Object}\n */\n get(key) {\n return super.get(this.normalizeKey(key));\n }\n\n /**\n * Check whether an element with the specified key exists or not.\n *\n * @param {*} key the key to find\n * @return {boolean}\n */\n has(key) {\n return super.has(this.normalizeKey(key));\n }\n\n /**\n * Delete an element from the map.\n *\n * @param {*} key\n * @returns {boolean}\n */\n delete(key) {\n // State maps uses only string keys to avoid strict comparisons.\n key = this.normalizeKey(key);\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n }\n\n const previous = super.get(key);\n\n const result = super.delete(key);\n if (!result) {\n return result;\n }\n\n this.stateManager.registerStateAction(this.name, null, 'deleted', previous);\n\n return result;\n }\n\n /**\n * Return a suitable structure for JSON conversion.\n *\n * This function is needed because new values are compared in JSON. StateMap has Private\n * attributes which cannot be stringified (like this.stateManager which will produce an\n * infinite recursivity).\n *\n * @returns {array}\n */\n toJSON() {\n let result = [];\n this.forEach((value) => {\n result.push(value);\n });\n return result;\n }\n\n /**\n * Insert a full list of values using the id attributes as keys.\n *\n * This method is used mainly to initialize the list. Note each element is indexed by its \"id\" attribute.\n * This is a basic restriction of StateMap. All elements need an id attribute, otherwise it won't be saved.\n *\n * @param {iterable} values the values to load\n * @returns {StateMap} return the this value\n */\n loadValues(values) {\n values.forEach((data) => {\n this.checkValue(data);\n let key = data.id;\n let newvalue = new Proxy(data, new Handler(this.name, this.stateManager));\n this.set(key, newvalue);\n });\n return this;\n }\n}\n"],"file":"statemanager.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../../../src/local/reactive/statemanager.js"],"names":["StateManager","dispatchEvent","target","document","readonly","eventsToPublish","updateTypes","defaultCreate","bind","defaultUpdate","defaultDelete","defaultPut","defaultOverride","defaultPrepareFields","initialPromise","Promise","resolve","addEventListener","initialStateDone","event","detail","state","initialState","Error","Proxy","Handler","Object","entries","prop","propValue","action","mode","_publishEvents","element","newFunctions","updateType","updateFunction","updates","Array","isArray","setReadOnly","forEach","update","name","processUpdate","fields","updateName","method","prepareFields","stateManager","StateMap","add","current","get","id","delete","fieldName","fieldValue","field","data","parentAction","push","eventName","eventData","fieldChanges","changes","sort","a","b","weights","created","updated","deleted","aweight","bweight","length","publishedEvents","Set","eventkey","has","proxyValues","obj","value","receiver","JSON","stringify","loadValues","registerStateAction","iterable","key","normalizeKey","checkValue","result","valueOf","set","previous","values","newvalue","Map"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive simple state manager.\n *\n * The state manager contains the state data, trigger update events and\n * can lock and unlock the state data.\n *\n * This file contains the three main elements of the state manager:\n * - State manager: the public class to alter the state, dispatch events and process update messages.\n * - Proxy handler: a private class to keep track of the state object changes.\n * - StateMap class: a private class extending Map class that triggers event when a state list is modifed.\n *\n * @module core/local/reactive/stateManager\n * @class core/local/reactive/stateManager\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * State manager class.\n *\n * This class handle the reactive state and ensure only valid mutations can modify the state.\n * It also provide methods to apply batch state update messages (see processUpdates function doc\n * for more details on update messages).\n *\n * Implementing a deep state manager is complex and will require many frontend resources. To keep\n * the state fast and simple, the state can ONLY store two kind of data:\n * - Object with attributes\n * - Sets of objects with id attributes.\n *\n * This is an example of a valid state:\n *\n * {\n * course: {\n * name: 'course name',\n * shortname: 'courseshort',\n * sectionlist: [21, 34]\n * },\n * sections: [\n * {id: 21, name: 'Topic 1', visible: true},\n * {id: 34, name: 'Topic 2', visible: false,\n * ],\n * }\n *\n * The following cases are NOT allowed at a state ROOT level (throws an exception if they are assigned):\n * - Simple values (strings, boolean...).\n * - Arrays of simple values.\n * - Array of objects without ID attribute (all arrays will be converted to maps and requires an ID).\n *\n * Thanks to those limitations it can simplify the state update messages and the event names. If You\n * need to store simple data, just group them in an object.\n *\n * To grant any state change triggers the proper events, the class uses two private structures:\n * - proxy handler: any object stored in the state is proxied using this class.\n * - StateMap class: any object set in the state will be converted to StateMap using the\n * objects id attribute.\n */\nexport default class StateManager {\n\n /**\n * Create a basic reactive state store.\n *\n * The state manager is meant to work with native JS events. To ensure each reactive module can use\n * it in its own way, the parent element must provide a valid event dispatcher function and an optional\n * DOM element to anchor the event.\n *\n * @param {function} dispatchEvent the function to dispatch the custom event when the state changes.\n * @param {element} target the state changed custom event target (document if none provided)\n */\n constructor(dispatchEvent, target) {\n\n // The dispatch event function.\n /** @package */\n this.dispatchEvent = dispatchEvent;\n\n // The DOM container to trigger events.\n /** @package */\n this.target = target ?? document;\n\n // State can be altered freely until initial state is set.\n /** @package */\n this.readonly = false;\n\n // List of state changes pending to be published as events.\n /** @package */\n this.eventsToPublish = [];\n\n // The update state types functions.\n /** @package */\n this.updateTypes = {\n \"create\": this.defaultCreate.bind(this),\n \"update\": this.defaultUpdate.bind(this),\n \"delete\": this.defaultDelete.bind(this),\n \"put\": this.defaultPut.bind(this),\n \"override\": this.defaultOverride.bind(this),\n \"prepareFields\": this.defaultPrepareFields.bind(this),\n };\n\n // The state_loaded event is special because it only happens one but all components\n // may react to that state, even if they are registered after the setIinitialState.\n // For these reason we use a promise for that event.\n this.initialPromise = new Promise((resolve) => {\n const initialStateDone = (event) => {\n resolve(event.detail.state);\n };\n this.target.addEventListener('state:loaded', initialStateDone);\n });\n }\n\n /**\n * Loads the initial state.\n *\n * Note this method will trigger a state changed event with \"state:loaded\" actionname.\n *\n * The state mode will be set to read only when the initial state is loaded.\n *\n * @param {object} initialState\n */\n setInitialState(initialState) {\n\n if (this.state !== undefined) {\n throw Error('Initial state can only be initialized ones');\n }\n\n // Create the state object.\n const state = new Proxy({}, new Handler('state', this, true));\n for (const [prop, propValue] of Object.entries(initialState)) {\n state[prop] = propValue;\n }\n this.state = state;\n\n // When the state is loaded we can lock it to prevent illegal changes.\n this.readonly = true;\n\n this.dispatchEvent({\n action: 'state:loaded',\n state: this.state,\n }, this.target);\n }\n\n /**\n * Generate a promise that will be resolved when the initial state is loaded.\n *\n * In most cases the final state will be loaded using an ajax call. This is the reason\n * why states manager are created unlocked and won't be reactive until the initial state is set.\n *\n * @return {Promise} the resulting promise\n */\n getInitialPromise() {\n return this.initialPromise;\n }\n\n /**\n * Locks or unlocks the state to prevent illegal updates.\n *\n * Mutations use this method to modify the state. Once the state is updated, they must\n * block again the state.\n *\n * All changes done while the state is writable will be registered using registerStateAction.\n * When the state is set again to read only the method will trigger _publishEvents to communicate\n * changes to all watchers.\n *\n * @param {bool} readonly if the state is in read only mode enabled\n */\n setReadOnly(readonly) {\n\n this.readonly = readonly;\n\n let mode = 'off';\n\n // When the state is in readonly again is time to publish all pending events.\n if (this.readonly) {\n mode = 'on';\n this._publishEvents();\n }\n\n // Dispatch a read only event.\n this.dispatchEvent({\n action: `readmode:${mode}`,\n state: this.state,\n element: null,\n }, this.target);\n }\n\n /**\n * Add methods to process update state messages.\n *\n * The state manager provide a default update, create and delete methods. However,\n * some applications may require to override the default methods or even add new ones\n * like \"refresh\" or \"error\".\n *\n * @param {Object} newFunctions the new update types functions.\n */\n addUpdateTypes(newFunctions) {\n for (const [updateType, updateFunction] of Object.entries(newFunctions)) {\n if (typeof updateFunction === 'function') {\n this.updateTypes[updateType] = updateFunction.bind(newFunctions);\n }\n }\n }\n\n /**\n * Process a state updates array and do all the necessary changes.\n *\n * Note this method unlocks the state while it is executing and relocks it\n * when finishes.\n *\n * @param {array} updates\n * @param {Object} updateTypes optional functions to override the default update types.\n */\n processUpdates(updates, updateTypes) {\n if (!Array.isArray(updates)) {\n throw Error('State updates must be an array');\n }\n this.setReadOnly(false);\n updates.forEach((update) => {\n if (update.name === undefined) {\n throw Error('Missing state update name');\n }\n this.processUpdate(\n update.name,\n update.action,\n update.fields,\n updateTypes\n );\n });\n this.setReadOnly(true);\n }\n\n /**\n * Process a single state update.\n *\n * Note this method will not lock or unlock the state by itself.\n *\n * @param {string} updateName the state element to update\n * @param {string} action to action to perform\n * @param {object} fields the new data\n * @param {Object} updateTypes optional functions to override the default update types.\n */\n processUpdate(updateName, action, fields, updateTypes) {\n\n if (!fields) {\n throw Error('Missing state update fields');\n }\n\n if (updateTypes === undefined) {\n updateTypes = {};\n }\n\n action = action ?? 'update';\n\n const method = updateTypes[action] ?? this.updateTypes[action];\n\n if (method === undefined) {\n throw Error(`Unkown update action ${action}`);\n }\n\n // Some state data may require some cooking before sending to the\n // state. Reactive instances can overrdide the default fieldDefaults\n // method to add extra logic to all updates.\n const prepareFields = updateTypes.prepareFields ?? this.updateTypes.prepareFields;\n\n method(this, updateName, prepareFields(this, updateName, fields));\n }\n\n /**\n * Prepare fields for processing.\n *\n * This method is used to add default values or calculations from the frontend side.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n * @returns {Object} final fields data\n */\n defaultPrepareFields(stateManager, updateName, fields) {\n return fields;\n }\n\n\n /**\n * Process a create state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultCreate(stateManager, updateName, fields) {\n\n let state = stateManager.state;\n\n // Create can be applied only to lists, not to objects.\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n\n /**\n * Process a delete state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultDelete(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n throw Error(`Inexistent ${updateName} ${fields.id}`);\n }\n\n // Process deletion.\n let state = stateManager.state;\n\n if (state[updateName] instanceof StateMap) {\n state[updateName].delete(fields.id);\n return;\n }\n delete state[updateName];\n }\n\n /**\n * Process a update state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultUpdate(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n throw Error(`Inexistent ${updateName} ${fields.id}`);\n }\n\n // Execute updates.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n }\n\n /**\n * Process a put state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultPut(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (current) {\n // Update attributes.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n } else {\n // Create new object.\n let state = stateManager.state;\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n }\n\n /**\n * Process an override state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultOverride(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (current) {\n // Remove any unnecessary fields.\n for (const [fieldName] of Object.entries(current)) {\n if (fields[fieldName] === undefined) {\n delete current[fieldName];\n }\n }\n // Update field.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n } else {\n // Create the element if not exists.\n let state = stateManager.state;\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n }\n\n /**\n * Get an element from the state or form an alternative state object.\n *\n * The altstate param is used by external update functions that gets the current\n * state as param.\n *\n * @param {String} name the state object name\n * @param {*} id and object id for state maps.\n * @return {Object|undefined} the state object found\n */\n get(name, id) {\n const state = this.state;\n\n let current = state[name];\n if (current instanceof StateMap) {\n if (id === undefined) {\n throw Error(`Missing id for ${name} state update`);\n }\n current = state[name].get(id);\n }\n\n return current;\n }\n\n /**\n * Register a state modification and generate the necessary events.\n *\n * This method is used mainly by proxy helpers to dispatch state change event.\n * However, mutations can use it to inform components about non reactive changes\n * in the state (only the two first levels of the state are reactive).\n *\n * Each action can produce several events:\n * - The specific attribute updated, created or deleter (example: \"cm.visible:updated\")\n * - The general state object updated, created or deleted (example: \"cm:updated\")\n * - If the element has an ID attribute, the specific event with id (example: \"cm[42].visible:updated\")\n * - If the element has an ID attribute, the general event with id (example: \"cm[42]:updated\")\n * - A generic state update event \"state:update\"\n *\n * @param {string} field the affected state field name\n * @param {string|null} prop the affecter field property (null if affect the full object)\n * @param {string} action the action done (created/updated/deleted)\n * @param {*} data the affected data\n */\n registerStateAction(field, prop, action, data) {\n\n let parentAction = 'updated';\n\n if (prop !== null) {\n this.eventsToPublish.push({\n eventName: `${field}.${prop}:${action}`,\n eventData: data,\n action,\n });\n } else {\n parentAction = action;\n }\n\n // Trigger extra events if the element has an ID attribute.\n if (data.id !== undefined) {\n if (prop !== null) {\n this.eventsToPublish.push({\n eventName: `${field}[${data.id}].${prop}:${action}`,\n eventData: data,\n action,\n });\n }\n this.eventsToPublish.push({\n eventName: `${field}[${data.id}]:${parentAction}`,\n eventData: data,\n action: parentAction,\n });\n }\n\n // Register the general change.\n this.eventsToPublish.push({\n eventName: `${field}:${parentAction}`,\n eventData: data,\n action: parentAction,\n });\n\n // Register state updated event.\n this.eventsToPublish.push({\n eventName: `state:updated`,\n eventData: data,\n action: 'updated',\n });\n }\n\n /**\n * Internal method to publish events.\n *\n * This is a private method, it will be invoked when the state is set back to read only mode.\n */\n _publishEvents() {\n const fieldChanges = this.eventsToPublish;\n this.eventsToPublish = [];\n\n // Dispatch a transaction start event.\n this.dispatchEvent({\n action: 'transaction:start',\n state: this.state,\n element: null,\n changes: fieldChanges,\n }, this.target);\n\n // State changes can be registered in any order. However it will avoid many\n // components errors if they are sorted to have creations-updates-deletes in case\n // some component needs to create or destroy DOM elements before updating them.\n fieldChanges.sort((a, b) => {\n const weights = {\n created: 0,\n updated: 1,\n deleted: 2,\n };\n const aweight = weights[a.action] ?? 0;\n const bweight = weights[b.action] ?? 0;\n // In case both have the same weight, the eventName length decide.\n if (aweight === bweight) {\n return a.eventName.length - b.eventName.length;\n }\n return aweight - bweight;\n });\n\n // List of the published events to prevent redundancies.\n let publishedEvents = new Set();\n\n fieldChanges.forEach((event) => {\n\n const eventkey = `${event.eventName}.${event.eventData.id ?? 0}`;\n\n if (!publishedEvents.has(eventkey)) {\n this.dispatchEvent({\n action: event.eventName,\n state: this.state,\n element: event.eventData\n }, this.target);\n\n publishedEvents.add(eventkey);\n }\n });\n\n // Dispatch a transaction end event.\n this.dispatchEvent({\n action: 'transaction:end',\n state: this.state,\n element: null,\n }, this.target);\n }\n}\n\n// Proxy helpers.\n\n/**\n * The proxy handler.\n *\n * This class will inform any value change directly to the state manager.\n *\n * The proxied variable will throw an error if it is altered when the state manager is\n * in read only mode.\n */\nclass Handler {\n\n /**\n * Class constructor.\n *\n * @param {string} name the variable name used for identify triggered actions\n * @param {StateManager} stateManager the state manager object\n * @param {boolean} proxyValues if new values must be proxied (used only at state root level)\n */\n constructor(name, stateManager, proxyValues) {\n this.name = name;\n this.stateManager = stateManager;\n this.proxyValues = proxyValues ?? false;\n }\n\n /**\n * Set trap to trigger events when the state changes.\n *\n * @param {object} obj the source object (not proxied)\n * @param {string} prop the attribute to set\n * @param {*} value the value to save\n * @param {*} receiver the proxied element to be attached to events\n * @returns {boolean} if the value is set\n */\n set(obj, prop, value, receiver) {\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${prop} value in ${this.name}.`);\n }\n\n // Check any data change.\n if (JSON.stringify(obj[prop]) === JSON.stringify(value)) {\n return true;\n }\n\n const action = (obj[prop] !== undefined) ? 'updated' : 'created';\n\n // Proxy value if necessary (used at state root level).\n if (this.proxyValues) {\n if (Array.isArray(value)) {\n obj[prop] = new StateMap(prop, this.stateManager).loadValues(value);\n } else {\n obj[prop] = new Proxy(value, new Handler(prop, this.stateManager));\n }\n } else {\n obj[prop] = value;\n }\n\n // If the state is not ready yet means the initial state is not yet loaded.\n if (this.stateManager.state === undefined) {\n return true;\n }\n\n this.stateManager.registerStateAction(this.name, prop, action, receiver);\n\n return true;\n }\n\n /**\n * Delete property trap to trigger state change events.\n *\n * @param {*} obj the affected object (not proxied)\n * @param {*} prop the prop to delete\n * @returns {boolean} if prop is deleted\n */\n deleteProperty(obj, prop) {\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to delete ${prop} in ${this.name}.`);\n }\n if (prop in obj) {\n\n delete obj[prop];\n\n this.stateManager.registerStateAction(this.name, prop, 'deleted', obj);\n }\n return true;\n }\n}\n\n/**\n * Class to add events dispatching to the JS Map class.\n *\n * When the state has a list of objects (with IDs) it will be converted into a StateMap.\n * StateMap is used almost in the same way as a regular JS map. Because all elements have an\n * id attribute, it has some specific methods:\n * - add: a convenient method to add an element without specifying the key (\"id\" attribute will be used as a key).\n * - loadValues: to add many elements at once wihout specifying keys (\"id\" attribute will be used).\n *\n * Apart, the main difference between regular Map and MapState is that this one will inform any change to the\n * state manager.\n */\nclass StateMap extends Map {\n\n /**\n * Create a reactive Map.\n *\n * @param {string} name the property name\n * @param {StateManager} stateManager the state manager\n * @param {iterable} iterable an iterable object to create the Map\n */\n constructor(name, stateManager, iterable) {\n // We don't have any \"this\" until be call super.\n super(iterable);\n this.name = name;\n this.stateManager = stateManager;\n }\n\n /**\n * Set an element into the map.\n *\n * Each value needs it's own id attribute. Objects without id will be rejected.\n * The function will throw an error if the value id and the key are not the same.\n *\n * @param {*} key the key to store\n * @param {*} value the value to store\n * @returns {Map} the resulting Map object\n */\n set(key, value) {\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n }\n\n // Normalize keys as string to prevent json decoding errors.\n key = this.normalizeKey(key);\n\n this.checkValue(value);\n\n if (key === undefined || key === null) {\n throw Error('State lists keys cannot be null or undefined');\n }\n\n // ID is mandatory and should be the same as the key.\n if (this.normalizeKey(value.id) !== key) {\n throw new Error(`State error: ${this.name} list element ID (${value.id}) and key (${key}) mismatch`);\n }\n\n const action = (super.has(key)) ? 'updated' : 'created';\n\n // Save proxied data into the list.\n const result = super.set(key, new Proxy(value, new Handler(this.name, this.stateManager)));\n\n // If the state is not ready yet means the initial state is not yet loaded.\n if (this.stateManager.state === undefined) {\n return result;\n }\n\n this.stateManager.registerStateAction(this.name, null, action, super.get(key));\n\n return result;\n }\n\n /**\n * Check if a value is valid to be stored in a a State List.\n *\n * Only objects with id attribute can be stored in State lists.\n *\n * This method throws an error if the value is not valid.\n *\n * @param {object} value (with ID)\n */\n checkValue(value) {\n if (!typeof value === 'object' && value !== null) {\n throw Error('State lists can contain objects only');\n }\n\n if (value.id === undefined) {\n throw Error('State lists elements must contain at least an id attribute');\n }\n }\n\n /**\n * Return a normalized key value for state map.\n *\n * Regular maps uses strict key comparissons but state maps are indexed by ID.JSON conversions\n * and webservices sometimes do unexpected types conversions so we convert any integer key to string.\n *\n * @param {*} key the provided key\n * @returns {string}\n */\n normalizeKey(key) {\n return String(key).valueOf();\n }\n\n /**\n * Insert a new element int a list.\n *\n * Each value needs it's own id attribute. Objects withouts id will be rejected.\n *\n * @param {object} value the value to add (needs an id attribute)\n * @returns {Map} the resulting Map object\n */\n add(value) {\n this.checkValue(value);\n return this.set(value.id, value);\n }\n\n /**\n * Return a state map element.\n *\n * @param {*} key the element id\n * @return {Object}\n */\n get(key) {\n return super.get(this.normalizeKey(key));\n }\n\n /**\n * Check whether an element with the specified key exists or not.\n *\n * @param {*} key the key to find\n * @return {boolean}\n */\n has(key) {\n return super.has(this.normalizeKey(key));\n }\n\n /**\n * Delete an element from the map.\n *\n * @param {*} key\n * @returns {boolean}\n */\n delete(key) {\n // State maps uses only string keys to avoid strict comparisons.\n key = this.normalizeKey(key);\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n }\n\n const previous = super.get(key);\n\n const result = super.delete(key);\n if (!result) {\n return result;\n }\n\n this.stateManager.registerStateAction(this.name, null, 'deleted', previous);\n\n return result;\n }\n\n /**\n * Return a suitable structure for JSON conversion.\n *\n * This function is needed because new values are compared in JSON. StateMap has Private\n * attributes which cannot be stringified (like this.stateManager which will produce an\n * infinite recursivity).\n *\n * @returns {array}\n */\n toJSON() {\n let result = [];\n this.forEach((value) => {\n result.push(value);\n });\n return result;\n }\n\n /**\n * Insert a full list of values using the id attributes as keys.\n *\n * This method is used mainly to initialize the list. Note each element is indexed by its \"id\" attribute.\n * This is a basic restriction of StateMap. All elements need an id attribute, otherwise it won't be saved.\n *\n * @param {iterable} values the values to load\n * @returns {StateMap} return the this value\n */\n loadValues(values) {\n values.forEach((data) => {\n this.checkValue(data);\n let key = data.id;\n let newvalue = new Proxy(data, new Handler(this.name, this.stateManager));\n this.set(key, newvalue);\n });\n return this;\n }\n}\n"],"file":"statemanager.min.js"}
\ No newline at end of file
diff --git a/lib/amd/build/reactive.min.js b/lib/amd/build/reactive.min.js
index 2ec39b1b507..4bf4a6f66e2 100644
--- a/lib/amd/build/reactive.min.js
+++ b/lib/amd/build/reactive.min.js
@@ -1,2 +1,2 @@
-define ("core/reactive",["exports","core/local/reactive/basecomponent","core/local/reactive/reactive","core/local/reactive/dragdrop"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"BaseComponent",{enumerable:!0,get:function get(){return b.default}});Object.defineProperty(a,"Reactive",{enumerable:!0,get:function get(){return c.default}});Object.defineProperty(a,"DragDrop",{enumerable:!0,get:function get(){return d.default}});b=e(b);c=e(c);d=e(d);function e(a){return a&&a.__esModule?a:{default:a}}});
+define ("core/reactive",["exports","core/local/reactive/basecomponent","core/local/reactive/reactive","core/local/reactive/dragdrop","core/local/reactive/debug"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"BaseComponent",{enumerable:!0,get:function get(){return b.default}});Object.defineProperty(a,"Reactive",{enumerable:!0,get:function get(){return c.default}});Object.defineProperty(a,"DragDrop",{enumerable:!0,get:function get(){return d.default}});a.debug=void 0;b=f(b);c=f(c);d=f(d);function f(a){return a&&a.__esModule?a:{default:a}}var g;a.debug=g;if(M.cfg.developerdebug&&M.reactive===void 0){var h=(0,e.initDebug)();M.reactive=h.debuggers;a.debug=g=h.debug}});
//# sourceMappingURL=reactive.min.js.map
diff --git a/lib/amd/build/reactive.min.js.map b/lib/amd/build/reactive.min.js.map
index 3956add0c16..aa1b34576d5 100644
--- a/lib/amd/build/reactive.min.js.map
+++ b/lib/amd/build/reactive.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/reactive.js"],"names":[],"mappings":"seAuBA,OACA,OACA,O","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Generic reactive module used in the course editor.\n *\n * @module core/reactive\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BaseComponent from 'core/local/reactive/basecomponent';\nimport Reactive from 'core/local/reactive/reactive';\nimport DragDrop from 'core/local/reactive/dragdrop';\n\nexport {Reactive, BaseComponent, DragDrop};\n"],"file":"reactive.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../src/reactive.js"],"names":["debug","M","cfg","developerdebug","reactive","debugOBject","debuggers"],"mappings":"mhBAuBA,OACA,OACA,O,mDAIA,GAAIA,CAAAA,CAAJ,C,UACA,GAAIC,CAAC,CAACC,GAAF,CAAMC,cAAN,EAAwBF,CAAC,CAACG,QAAF,SAA5B,CAAsD,CAClD,GAAMC,CAAAA,CAAW,CAAG,iBAApB,CACAJ,CAAC,CAACG,QAAF,CAAaC,CAAW,CAACC,SAAzB,CACA,QAAAN,CAAK,CAAGK,CAAW,CAACL,KACvB,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Generic reactive module used in the course editor.\n *\n * @module core/reactive\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BaseComponent from 'core/local/reactive/basecomponent';\nimport Reactive from 'core/local/reactive/reactive';\nimport DragDrop from 'core/local/reactive/dragdrop';\nimport {initDebug} from 'core/local/reactive/debug';\n\n// Register a debug module if we are in debug mode.\nlet debug;\nif (M.cfg.developerdebug && M.reactive === undefined) {\n const debugOBject = initDebug();\n M.reactive = debugOBject.debuggers;\n debug = debugOBject.debug;\n}\n\nexport {Reactive, BaseComponent, DragDrop, debug};\n"],"file":"reactive.min.js"}
\ No newline at end of file
diff --git a/lib/amd/src/local/reactive/debug.js b/lib/amd/src/local/reactive/debug.js
new file mode 100644
index 00000000000..12057481a1f
--- /dev/null
+++ b/lib/amd/src/local/reactive/debug.js
@@ -0,0 +1,372 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Reactive module debug tools.
+ *
+ * @module core/reactive/local/reactive/debug
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import Reactive from 'core/local/reactive/reactive';
+import log from 'core/log';
+
+// The list of reactives instances.
+const reactiveInstances = {};
+
+// The reactive debugging objects.
+const reactiveDebuggers = {};
+
+/**
+ * Reactive module debug tools.
+ *
+ * If debug is enabled, this reactive module will spy all the reactive instances and keep a record
+ * of the changes and components they have.
+ *
+ * It is important to note that the Debug class is also a Reactive module. The debug instance keeps
+ * the reactive instances data as its own state. This way it is possible to implement development tools
+ * that whatches this data.
+ *
+ * @class core/reactive/local/reactive/debug/Debug
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class Debug extends Reactive {
+
+ /**
+ * Set the initial state.
+ *
+ * @param {object} stateData the initial state data.
+ */
+ setInitialState(stateData) {
+ super.setInitialState(stateData);
+ log.debug(`Debug module "M.reactive" loaded.`);
+ }
+
+ /**
+ * List the currents page reactives instances.
+ */
+ get list() {
+ return JSON.parse(JSON.stringify(this.state.reactives));
+ }
+
+ /**
+ * Register a new Reactive instance.
+ *
+ * This method is called every time a "new Reactive" is executed.
+ *
+ * @param {Reactive} instance the reactive instance
+ */
+ registerNewInstance(instance) {
+
+ // Generate a valid variable name for that instance.
+ let name = instance.name ?? `instance${this.state.reactives.length}`;
+ name = name.replace(/\W/g, '');
+
+ log.debug(`Registering new reactive instance "M.reactive.${name}"`);
+
+ reactiveInstances[name] = instance;
+ reactiveDebuggers[name] = new DebugInstance(reactiveInstances[name]);
+ // Register also in the state.
+ this.dispatch('putInstance', name, instance);
+ // Add debug watchers to instance.
+ const refreshMethod = () => {
+ this.dispatch('putInstance', name, instance);
+ };
+ instance.target.addEventListener('readmode:on', refreshMethod);
+ instance.target.addEventListener('readmode:off', refreshMethod);
+ instance.target.addEventListener('registerComponent:success', refreshMethod);
+ instance.target.addEventListener('transaction:end', refreshMethod);
+ // We store the last transaction into the state.
+ const storeTransaction = ({detail}) => {
+ const changes = detail?.changes;
+ this.dispatch('lastTransaction', name, changes);
+ };
+ instance.target.addEventListener('transaction:start', storeTransaction);
+ }
+
+ /**
+ * Returns a debugging object for a specific Reactive instance.
+ *
+ * A debugging object is a class that wraps a Reactive instance to quick access some of the
+ * reactive methods using the browser JS console.
+ *
+ * @param {string} name the Reactive instance name
+ * @returns {DebugInstance} a debug object wrapping the Reactive instance
+ */
+ debug(name) {
+ return reactiveDebuggers[name];
+ }
+}
+
+/**
+ * The debug state mutations class.
+ *
+ * @class core/reactive/local/reactive/debug/Mutations
+ */
+class Mutations {
+
+ /**
+ * Insert or update a new instance into the debug state.
+ *
+ * @param {StateManager} stateManager the debug state manager
+ * @param {string} name the instance name
+ * @param {Reactive} instance the reactive instance
+ */
+ putInstance(stateManager, name, instance) {
+ const state = stateManager.state;
+
+ stateManager.setReadOnly(false);
+
+ if (state.reactives.has(name)) {
+ state.reactives.get(name).countcomponents = instance.components.length;
+ state.reactives.get(name).readOnly = instance.stateManager.readonly;
+ state.reactives.get(name).modified = new Date().getTime();
+ } else {
+ state.reactives.add({
+ id: name,
+ countcomponents: instance.components.length,
+ readOnly: instance.stateManager.readonly,
+ lastChanges: [],
+ modified: new Date().getTime(),
+ });
+ }
+ stateManager.setReadOnly(true);
+ }
+
+ /**
+ * Update the lastChanges attribute with a list of changes
+ *
+ * @param {StateManager} stateManager the debug reactive state
+ * @param {string} name tje instance name
+ * @param {array} changes the list of changes
+ */
+ lastTransaction(stateManager, name, changes) {
+ if (!changes || changes.length === 0) {
+ return;
+ }
+
+ const state = stateManager.state;
+ const lastChanges = ['transaction:start'];
+
+ changes.forEach(change => {
+ lastChanges.push(change.eventName);
+ });
+
+ lastChanges.push('transaction:end');
+
+ stateManager.setReadOnly(false);
+
+ state.reactives.get(name).lastChanges = lastChanges;
+
+ stateManager.setReadOnly(true);
+ }
+}
+
+/**
+ * Class used to debug a specific instance and manipulate the state from the JS console.
+ *
+ * @class core/reactive/local/reactive/debug/DebugInstance
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class DebugInstance {
+
+ /**
+ * Constructor.
+ *
+ * @param {Reactive} instance the reactive instance
+ */
+ constructor(instance) {
+ this.instance = instance;
+ // Add some debug data directly into the instance. This way we avoid having attributes
+ // that will confuse the console aoutocomplete.
+ if (instance._reactiveDebugData === undefined) {
+ instance._reactiveDebugData = {
+ highlighted: false,
+ };
+ }
+ }
+
+ /**
+ * Set the read only mode.
+ *
+ * Quick access to the instance setReadOnly method.
+ *
+ * @param {bool} value: the new read only value
+ */
+ set readOnly(value) {
+ this.instance.stateManager.setReadOnly(value);
+ }
+
+ /**
+ * Get the read only value
+ *
+ * @return {bool}
+ */
+ get readOnly() {
+ return this.instance.stateManager.readonly;
+ }
+
+ /**
+ * Return the current state object.
+ *
+ * @return {object}
+ */
+ get state() {
+ return this.instance.state;
+ }
+
+ /**
+ * Tooggle the reactive HTML element highlight registered in this reactive instance.
+ *
+ * @param {bool} value the highlight value
+ */
+ set highlight(value) {
+ this.instance._reactiveDebugData.highlighted = value;
+ this.instance.components.forEach(({element}) => {
+ const border = (value) ? `thick solid #0000FF` : '';
+ element.style.border = border;
+ });
+ }
+
+ /**
+ * Get the current highligh value.
+ *
+ * @return {bool}
+ */
+ get highlight() {
+ return this.instance._reactiveDebugData.highlighted;
+ }
+
+ /**
+ * List all the components registered in this instance.
+ *
+ * @return {array}
+ */
+ get components() {
+ return [...this.instance.components];
+ }
+
+ /**
+ * List all the state changes evenet pending to dispatch.
+ *
+ * @return {array}
+ */
+ get changes() {
+ const result = [];
+ this.instance.stateManager.eventsToPublish.forEach(
+ (element) => {
+ result.push(element.eventName);
+ }
+ );
+ return result;
+ }
+
+ /**
+ * Dispatch a change in the state.
+ *
+ * Usually reactive modules throw an error directly to the components when something
+ * goes wrong. However, course editor can directly display a notification.
+ *
+ * @method dispatch
+ * @param {string} actionName the action name (usually the mutation name)
+ * @param {*} param any number of params the mutation needs.
+ */
+ async dispatch(...args) {
+ this.instance.dispatch(...args);
+ }
+
+ /**
+ * Return all the HTML elements registered in the instance components.
+ *
+ * @return {array}
+ */
+ get elements() {
+ const result = [];
+ this.instance.components.forEach(({element}) => {
+ result.push(element);
+ });
+ return result;
+ }
+
+ /**
+ * Return a plain copy of the state data.
+ *
+ * @return {object}
+ */
+ get stateData() {
+ return JSON.parse(JSON.stringify(this.state));
+ }
+
+ /**
+ * Process an update state array.
+ *
+ * @param {array} updates an array of update state messages
+ */
+ processUpdates(updates) {
+ this.instance.stateManager.processUpdates(updates);
+ }
+}
+
+const stateChangedEventName = 'core_reactive_debug:stateChanged';
+
+/**
+ * Internal state changed event.
+ *
+ * @method dispatchStateChangedEvent
+ * @param {object} detail the full state
+ * @param {object} target the custom event target (document if none provided)
+ */
+function dispatchStateChangedEvent(detail, target) {
+ if (target === undefined) {
+ target = document;
+ }
+ target.dispatchEvent(
+ new CustomEvent(
+ stateChangedEventName,
+ {
+ bubbles: true,
+ detail: detail,
+ }
+ )
+ );
+}
+
+/**
+ * The main init method to initialize the reactive debug.
+ * @returns {object}
+ */
+export const initDebug = () => {
+ const debug = new Debug({
+ name: 'CoreReactiveDebug',
+ eventName: stateChangedEventName,
+ eventDispatch: dispatchStateChangedEvent,
+ mutations: new Mutations(),
+ state: {
+ reactives: [],
+ },
+ });
+
+ // The reactiveDebuggers will be used as a way of access the debug instances but also to register every new
+ // instance. To ensure this will update the reactive debug state we add the registerNewInstance method to it.
+ reactiveDebuggers.registerNewInstance = debug.registerNewInstance.bind(debug);
+
+ return {
+ debug,
+ debuggers: reactiveDebuggers,
+ };
+};
diff --git a/lib/amd/src/local/reactive/debugpanel.js b/lib/amd/src/local/reactive/debugpanel.js
new file mode 100644
index 00000000000..3aa63937408
--- /dev/null
+++ b/lib/amd/src/local/reactive/debugpanel.js
@@ -0,0 +1,563 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Reactive module debug panel.
+ *
+ * This module contains all the UI components for the reactive debug tools.
+ * Those tools are only available if the debug is enables and could be used
+ * from the footer.
+ *
+ * @module core/local/reactive/debugpanel
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent, DragDrop, debug} from 'core/reactive';
+import log from 'core/log';
+import {debounce} from 'core/utils';
+
+/**
+ * Init the main reactive panel.
+ *
+ * @param {element|string} target the DOM main element or its ID
+ * @param {object} selectors optional css selector overrides
+ */
+export const init = (target, selectors) => {
+ const element = document.getElementById(target);
+ // Check if the debug reactive module is available.
+ if (debug === undefined) {
+ element.remove();
+ return;
+ }
+ // Create the main component.
+ new GlobalDebugPanel({
+ element,
+ reactive: debug,
+ selectors,
+ });
+};
+
+/**
+ * Init an instance reactive subpanel.
+ *
+ * @param {element|string} target the DOM main element or its ID
+ * @param {object} selectors optional css selector overrides
+ */
+export const initsubpanel = (target, selectors) => {
+ const element = document.getElementById(target);
+ // Check if the debug reactive module is available.
+ if (debug === undefined) {
+ element.remove();
+ return;
+ }
+ // Create the main component.
+ new DebugInstanceSubpanel({
+ element,
+ reactive: debug,
+ selectors,
+ });
+};
+
+/**
+ * Component for the main reactive dev panel.
+ *
+ * This component shows the list of reactive instances and handle the buttons
+ * to open a specific instance panel.
+ */
+class GlobalDebugPanel extends BaseComponent {
+
+ /**
+ * Constructor hook.
+ */
+ create() {
+ // Optional component name for debugging.
+ this.name = 'GlobalDebugPanel';
+ // Default query selectors.
+ this.selectors = {
+ LOADERS: `[data-for='loaders']`,
+ SUBPANEL: `[data-for='subpanel']`,
+ LOG: `[data-for='log']`,
+ };
+ }
+
+ /**
+ * Initial state ready method.
+ *
+ * @param {object} state the initial state
+ */
+ stateReady(state) {
+ if (state.reactives.size > 0) {
+ this.getElement(this.selectors.LOADERS).innerHTML = '';
+ }
+ // Generate loading buttons.
+ state.reactives.forEach(
+ instance => {
+ this._createLoader(instance);
+ }
+ );
+ // Remove loading wheel.
+ this.getElement(this.selectors.SUBPANEL).innerHTML = '';
+ }
+
+ /**
+ * Create a debug panel button for a specific reactive instance.
+ *
+ * @param {object} instance hte instance data
+ */
+ _createLoader(instance) {
+ const loaders = this.getElement(this.selectors.LOADERS);
+ const btn = document.createElement("button");
+ btn.innerHTML = instance.id;
+ btn.dataset.id = instance.id;
+ loaders.appendChild(btn);
+ // Add click event.
+ this.addEventListener(btn, 'click', () => this._openPanel(btn, instance));
+ }
+
+ /**
+ * Open a debug panel.
+ *
+ * @param {Element} btn the button element
+ * @param {object} instance the instance data
+ */
+ async _openPanel(btn, instance) {
+ try {
+ const target = this.getElement(this.selectors.SUBPANEL);
+ const data = {...instance};
+ await this.renderComponent(target, 'core/local/reactive/debuginstancepanel', data);
+ } catch (error) {
+ log.error('Cannot load reactive debug subpanel');
+ throw error;
+ }
+ }
+}
+
+/**
+ * Component for the main reactive dev panel.
+ *
+ * This component shows the list of reactive instances and handle the buttons
+ * to open a specific instance panel.
+ */
+class DebugInstanceSubpanel extends BaseComponent {
+
+ /**
+ * Constructor hook.
+ */
+ create() {
+ // Optional component name for debugging.
+ this.name = 'DebugInstanceSubpanel';
+ // Default query selectors.
+ this.selectors = {
+ NAME: `[data-for='name']`,
+ CLOSE: `[data-for='close']`,
+ READMODE: `[data-for='readmode']`,
+ HIGHLIGHT: `[data-for='highlight']`,
+ LOG: `[data-for='log']`,
+ STATE: `[data-for='state']`,
+ CLEAN: `[data-for='clean']`,
+ PIN: `[data-for='pin']`,
+ SAVE: `[data-for='save']`,
+ INVALID: `[data-for='invalid']`,
+ };
+ this.id = this.element.dataset.id;
+ this.controller = M.reactive[this.id];
+
+ // The component is created always pinned.
+ this.draggable = false;
+ // We want the element to be dragged like modal.
+ this.relativeDrag = true;
+ // Save warning (will be loaded when state is ready.
+ this.strings = {
+ savewarning: '',
+ };
+ }
+
+ /**
+ * Initial state ready method.
+ *
+ */
+ stateReady() {
+ // Enable drag and drop.
+ this.dragdrop = new DragDrop(this);
+
+ // Close button.
+ this.addEventListener(
+ this.getElement(this.selectors.CLOSE),
+ 'click',
+ this.remove
+ );
+ // Highlight button.
+ if (this.controller.highlight) {
+ this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT));
+ }
+ this.addEventListener(
+ this.getElement(this.selectors.HIGHLIGHT),
+ 'click',
+ () => {
+ this.controller.highlight = !this.controller.highlight;
+ this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT));
+ }
+ );
+ // Edit mode button.
+ this.addEventListener(
+ this.getElement(this.selectors.READMODE),
+ 'click',
+ this._toggleEditMode
+ );
+ // Clean log and state.
+ this.addEventListener(
+ this.getElement(this.selectors.CLEAN),
+ 'click',
+ this._cleanAreas
+ );
+ // Unpin panel butotn.
+ this.addEventListener(
+ this.getElement(this.selectors.PIN),
+ 'click',
+ this._togglePin
+ );
+ // Save button, state format error message and state textarea.
+ this.getElement(this.selectors.SAVE).disabled = true;
+
+ this.addEventListener(
+ this.getElement(this.selectors.STATE),
+ 'keyup',
+ debounce(this._checkJSON, 500)
+ );
+
+ this.addEventListener(
+ this.getElement(this.selectors.SAVE),
+ 'click',
+ this._saveState
+ );
+ // Save the default save warning message.
+ this.strings.savewarning = this.getElement(this.selectors.INVALID)?.innerHTML ?? '';
+ // Add current state.
+ this._refreshState();
+ }
+
+ /**
+ * Remove all subcomponents dependencies.
+ */
+ destroy() {
+ if (this.dragdrop !== undefined) {
+ this.dragdrop.unregister();
+ }
+ }
+
+ /**
+ * Component watchers.
+ *
+ * @returns {Array} of watchers
+ */
+ getWatchers() {
+ return [
+ {watch: `reactives[${this.id}].lastChanges:updated`, handler: this._refreshLog},
+ {watch: `reactives[${this.id}].modified:updated`, handler: this._refreshState},
+ {watch: `reactives[${this.id}].readOnly:updated`, handler: this._refreshReadOnly},
+ ];
+ }
+
+ /**
+ * Wtacher method to refresh the log panel.
+ *
+ * @param {object} detail of the change
+ */
+ _refreshLog({element}) {
+ const list = element?.lastChanges ?? [];
+
+ const logContent = list.join("\n");
+ // Append last log.
+ const target = this.getElement(this.selectors.LOG);
+ target.value += `\n\n= Transaction =\n ${logContent}`;
+ target.scrollTop = target.scrollHeight;
+ }
+
+ /**
+ * Listener method to clean the log area.
+ */
+ _cleanAreas() {
+ let target = this.getElement(this.selectors.LOG);
+ target.value = '';
+
+ this._refreshState();
+ }
+
+ /**
+ * Watcher to refresh the state information.
+ */
+ _refreshState() {
+ const target = this.getElement(this.selectors.STATE);
+ target.value = JSON.stringify(this.controller.state, null, 4);
+ }
+
+ /**
+ * Watcher to update the read only information.
+ */
+ _refreshReadOnly() {
+ // Toggle the read mode button.
+ const target = this.getElement(this.selectors.READMODE);
+ if (target.dataset.readonly === undefined) {
+ target.dataset.readonly = target.innerHTML;
+ }
+ if (this.controller.readOnly) {
+ target.innerHTML = target.dataset.readonly;
+ } else {
+ target.innerHTML = target.dataset.alt;
+ }
+ }
+
+ /**
+ * Listener to toggle the edit mode of the component.
+ */
+ _toggleEditMode() {
+ this.controller.readOnly = !this.controller.readOnly;
+ }
+
+ /**
+ * Check that the edited state JSON is valid.
+ *
+ * Not all valid JSON are suitable for transforming the state. For example,
+ * the first level attributes cannot change the type.
+ *
+ * @return {undefined|array} Array of state updates.
+ */
+ _checkJSON() {
+ const invalid = this.getElement(this.selectors.INVALID);
+ const save = this.getElement(this.selectors.SAVE);
+
+ const edited = this.getElement(this.selectors.STATE).value;
+
+ const currentStateData = this.controller.stateData;
+
+ // Check if the json is tha same as state.
+ if (edited == JSON.stringify(this.controller.state, null, 4)) {
+ invalid.style.color = '';
+ invalid.innerHTML = '';
+ save.disabled = true;
+ return undefined;
+ }
+
+ // Check if the json format is valid.
+ try {
+ const newState = JSON.parse(edited);
+ // Check the first level did not change types.
+ const result = this._generateStateUpdates(currentStateData, newState);
+ // Enable save button.
+ invalid.style.color = '';
+ invalid.innerHTML = this.strings.savewarning;
+ save.disabled = false;
+ return result;
+ } catch (error) {
+ invalid.style.color = 'red';
+ invalid.innerHTML = error.message ?? 'Invalid JSON sctructure';
+ save.disabled = true;
+ return undefined;
+ }
+ }
+
+ /**
+ * Listener to save the current edited state into the real state.
+ */
+ _saveState() {
+ const updates = this._checkJSON();
+ if (!updates) {
+ return;
+ }
+ // Sent the updates to the state manager.
+ this.controller.processUpdates(updates);
+ }
+
+ /**
+ * Check that the edited state JSON is valid.
+ *
+ * Not all valid JSON are suitable for transforming the state. For example,
+ * the first level attributes cannot change the type. This method do a two
+ * steps comparison between the current state data and the new state data.
+ *
+ * A reactive state cannot be overridden like any other variable. To keep
+ * the watchers updated is necessary to transform the current state into
+ * the new one. As a result, this method generates all the necessary state
+ * updates to convert the state into the new state.
+ *
+ * @param {object} currentStateData
+ * @param {object} newStateData
+ * @return {array} Array of state updates.
+ * @throws {Error} is the structure is not compatible
+ */
+ _generateStateUpdates(currentStateData, newStateData) {
+
+ const updates = [];
+
+ const ids = {};
+
+ // Step 1: Add all overrides newStateData.
+ for (const [key, newValue] of Object.entries(newStateData)) {
+ // Check is it is new.
+ if (Array.isArray(newValue)) {
+ ids[key] = {};
+ newValue.forEach(element => {
+ if (element.id === undefined) {
+ throw Error(`Array ${key} element without id attribute`);
+ }
+ updates.push({
+ name: key,
+ action: 'override',
+ fields: element,
+ });
+ const index = String(element.id).valueOf();
+ ids[key][index] = true;
+ });
+ } else {
+ updates.push({
+ name: key,
+ action: 'override',
+ fields: newValue,
+ });
+ }
+ }
+ // Step 2: delete unnecesary data from currentStateData.
+ for (const [key, oldValue] of Object.entries(currentStateData)) {
+ let deleteField = false;
+ // Check if the attribute is still there.
+ if (newStateData[key] === undefined) {
+ deleteField = true;
+ }
+ if (Array.isArray(oldValue)) {
+ if (!deleteField && ids[key] === undefined) {
+ throw Error(`Array ${key} cannot change to object.`);
+ }
+ oldValue.forEach(element => {
+ const index = String(element.id).valueOf();
+ let deleteEntry = deleteField;
+ // Check if the id is there.
+ if (!deleteEntry && ids[key][index] === undefined) {
+ deleteEntry = true;
+ }
+ if (deleteEntry) {
+ updates.push({
+ name: key,
+ action: 'delete',
+ fields: element,
+ });
+ }
+ });
+ } else {
+ if (!deleteField && ids[key] !== undefined) {
+ throw Error(`Object ${key} cannot change to array.`);
+ }
+ if (deleteField) {
+ updates.push({
+ name: key,
+ action: 'delete',
+ fields: oldValue,
+ });
+ }
+ }
+ }
+ // Delete all elements without action.
+ return updates;
+ }
+
+ // Drag and drop methods.
+
+ /**
+ * Get the draggable data of this component.
+ *
+ * @returns {Object} exported course module drop data
+ */
+ getDraggableData() {
+ return this.draggable;
+ }
+
+ /**
+ * The element drop end hook.
+ *
+ * @param {Object} dropdata the dropdata
+ * @param {Event} event the dropdata
+ */
+ dragEnd(dropdata, event) {
+ this.element.style.top = `${event.newFixedTop}px`;
+ this.element.style.left = `${event.newFixedLeft}px`;
+ }
+
+ /**
+ * Pin and unpin the panel.
+ */
+ _togglePin() {
+ this.draggable = !this.draggable;
+ this.dragdrop.setDraggable(this.draggable);
+ if (this.draggable) {
+ this._unpin();
+ } else {
+ this._pin();
+ }
+ }
+
+ /**
+ * Unpin the panel form the footer.
+ */
+ _unpin() {
+ // Find the initial spot.
+ const pageCenterY = window.innerHeight / 2;
+ const pageCenterX = window.innerWidth / 2;
+ // Put the element in the middle of the screen
+ const style = {
+ position: 'fixed',
+ resize: 'both',
+ overflow: 'auto',
+ height: '400px',
+ width: '400px',
+ top: `${pageCenterY - 200}px`,
+ left: `${pageCenterX - 200}px`,
+ };
+ Object.assign(this.element.style, style);
+ // Small also the text areas.
+ this.getElement(this.selectors.STATE).style.height = '50px';
+ this.getElement(this.selectors.LOG).style.height = '50px';
+
+ this._toggleButtonText(this.getElement(this.selectors.PIN));
+ }
+
+ /**
+ * Pin the panel into the footer.
+ */
+ _pin() {
+ const props = [
+ 'position',
+ 'resize',
+ 'overflow',
+ 'top',
+ 'left',
+ 'height',
+ 'width',
+ ];
+ props.forEach(
+ prop => this.element.style.removeProperty(prop)
+ );
+ this._toggleButtonText(this.getElement(this.selectors.PIN));
+ }
+
+ /**
+ * Toogle the button text with the data-alt value.
+ *
+ * @param {Element} element the button element
+ */
+ _toggleButtonText(element) {
+ [element.innerHTML, element.dataset.alt] = [element.dataset.alt, element.innerHTML];
+ }
+
+}
diff --git a/lib/amd/src/local/reactive/reactive.js b/lib/amd/src/local/reactive/reactive.js
index f772c724b4f..119f2fef98b 100644
--- a/lib/amd/src/local/reactive/reactive.js
+++ b/lib/amd/src/local/reactive/reactive.js
@@ -109,6 +109,11 @@ export default class {
if (description.state !== undefined) {
this.setInitialState(description.state);
}
+
+ // Check if we have a debug instance to register the instance.
+ if (M.reactive !== undefined) {
+ M.reactive.registerNewInstance(this);
+ }
}
/**
@@ -302,6 +307,12 @@ export default class {
this.watchers.set(component, listeners);
this.components.add(component);
+ // Dispatch an event to communicate the registration to the debug module.
+ this.target.dispatchEvent(new CustomEvent('registerComponent:success', {
+ bubbles: false,
+ detail: {component},
+ }));
+
dispatchSuccess();
return component;
}
diff --git a/lib/amd/src/local/reactive/statemanager.js b/lib/amd/src/local/reactive/statemanager.js
index 8f22e0371ef..b6a66001318 100644
--- a/lib/amd/src/local/reactive/statemanager.js
+++ b/lib/amd/src/local/reactive/statemanager.js
@@ -180,10 +180,20 @@ export default class StateManager {
this.readonly = readonly;
+ let mode = 'off';
+
// When the state is in readonly again is time to publish all pending events.
if (this.readonly) {
+ mode = 'on';
this._publishEvents();
}
+
+ // Dispatch a read only event.
+ this.dispatchEvent({
+ action: `readmode:${mode}`,
+ state: this.state,
+ element: null,
+ }, this.target);
}
/**
@@ -509,6 +519,7 @@ export default class StateManager {
action: 'transaction:start',
state: this.state,
element: null,
+ changes: fieldChanges,
}, this.target);
// State changes can be registered in any order. However it will avoid many
diff --git a/lib/amd/src/reactive.js b/lib/amd/src/reactive.js
index 317f3ca7601..1a562ce8cc4 100644
--- a/lib/amd/src/reactive.js
+++ b/lib/amd/src/reactive.js
@@ -24,5 +24,14 @@
import BaseComponent from 'core/local/reactive/basecomponent';
import Reactive from 'core/local/reactive/reactive';
import DragDrop from 'core/local/reactive/dragdrop';
+import {initDebug} from 'core/local/reactive/debug';
-export {Reactive, BaseComponent, DragDrop};
+// Register a debug module if we are in debug mode.
+let debug;
+if (M.cfg.developerdebug && M.reactive === undefined) {
+ const debugOBject = initDebug();
+ M.reactive = debugOBject.debuggers;
+ debug = debugOBject.debug;
+}
+
+export {Reactive, BaseComponent, DragDrop, debug};
diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php
index 5d49e947d1c..8c23b0136f7 100644
--- a/lib/outputrenderers.php
+++ b/lib/outputrenderers.php
@@ -873,6 +873,7 @@ class core_renderer extends renderer_base {
$this->page->debug_summary()) . '';
}
if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) { // Only in developer mode
+
// Add link to profiling report if necessary
if (function_exists('profiling_is_running') && profiling_is_running()) {
$txt = get_string('profiledscript', 'admin');
@@ -885,6 +886,9 @@ class core_renderer extends renderer_base {
'sesskey' => sesskey(), 'returnurl' => $this->page->url->out_as_local_url(false)));
$output .= '
';
+
+ // Reactive module debug panel.
+ $output .= $this->render_from_template('core/local/reactive/debugpanel', []);
}
if (!empty($CFG->debugvalidators)) {
// NOTE: this is not a nice hack, $this->page->url is not always accurate and
diff --git a/lib/templates/local/reactive/debuginstancepanel.mustache b/lib/templates/local/reactive/debuginstancepanel.mustache
new file mode 100644
index 00000000000..761099a7f9f
--- /dev/null
+++ b/lib/templates/local/reactive/debuginstancepanel.mustache
@@ -0,0 +1,86 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/local/reactive/debuginstancepanel
+
+ Template to render the global reactive debug panel.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Example context (json):
+ {
+ "id": "instanceid"
+ }
+}}
+
+{{#js}}
+require(['core/local/reactive/debugpanel'], function(component) {
+ component.initsubpanel('{{uniqid}}-reactive-debugpanel-instance');
+});
+{{/js}}
diff --git a/lib/templates/local/reactive/debugpanel.mustache b/lib/templates/local/reactive/debugpanel.mustache
new file mode 100644
index 00000000000..2b015f1fec8
--- /dev/null
+++ b/lib/templates/local/reactive/debugpanel.mustache
@@ -0,0 +1,45 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/local/reactive/debugpanel
+
+ Template to render the global reactive debug panel.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Example context (json):
+ {
+ }
+}}
+