From b58e78066615d7f65b11a008625dce94ab9f149d Mon Sep 17 00:00:00 2001
From: Ferran Recio <ferran@moodle.com>
Date: Mon, 28 Jun 2021 13:32:00 +0200
Subject: [PATCH] MDL-71995 core: reactive debug tools

---
 lang/en/admin.php                             |   1 +
 lang/en/debug.php                             |  11 +
 lib/amd/build/local/reactive/debug.min.js     |   2 +
 lib/amd/build/local/reactive/debug.min.js.map |   1 +
 .../build/local/reactive/debugpanel.min.js    |   2 +
 .../local/reactive/debugpanel.min.js.map      |   1 +
 lib/amd/build/local/reactive/reactive.min.js  |   2 +-
 .../build/local/reactive/reactive.min.js.map  |   2 +-
 .../build/local/reactive/statemanager.min.js  |   2 +-
 .../local/reactive/statemanager.min.js.map    |   2 +-
 lib/amd/build/reactive.min.js                 |   2 +-
 lib/amd/build/reactive.min.js.map             |   2 +-
 lib/amd/src/local/reactive/debug.js           | 372 ++++++++++++
 lib/amd/src/local/reactive/debugpanel.js      | 563 ++++++++++++++++++
 lib/amd/src/local/reactive/reactive.js        |  11 +
 lib/amd/src/local/reactive/statemanager.js    |  11 +
 lib/amd/src/reactive.js                       |  11 +-
 lib/outputrenderers.php                       |   4 +
 .../reactive/debuginstancepanel.mustache      |  86 +++
 .../local/reactive/debugpanel.mustache        |  45 ++
 20 files changed, 1126 insertions(+), 7 deletions(-)
 create mode 100644 lib/amd/build/local/reactive/debug.min.js
 create mode 100644 lib/amd/build/local/reactive/debug.min.js.map
 create mode 100644 lib/amd/build/local/reactive/debugpanel.min.js
 create mode 100644 lib/amd/build/local/reactive/debugpanel.min.js.map
 create mode 100644 lib/amd/src/local/reactive/debug.js
 create mode 100644 lib/amd/src/local/reactive/debugpanel.js
 create mode 100644 lib/templates/local/reactive/debuginstancepanel.mustache
 create mode 100644 lib/templates/local/reactive/debugpanel.mustache

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<b;c++){d[c]=a[c]}return d}function k(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 l(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){k(h,d,e,f,g,"next",a)}function g(a){k(h,d,e,f,g,"throw",a)}f(void 0)})}}function m(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){m=function(a){return typeof a}}else{m=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return m(a)}function n(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function o(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function p(a,b,c){if(b)o(a.prototype,b);if(c)o(a,c);return a}function q(a,b,c){if("undefined"!=typeof Reflect&&Reflect.get){q=Reflect.get}else{q=function(a,b,c){var d=r(a,b);if(!d)return;var e=Object.getOwnPropertyDescriptor(d,b);if(e.get){return e.get.call(c)}return e.value}}return q(a,b,c||a)}function r(a,b){while(!Object.prototype.hasOwnProperty.call(a,b)){a=y(a);if(null===a)break}return a}function s(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)t(a,b)}function t(a,b){t=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return t(a,b)}function u(a){return function(){var b=y(a),c;if(x()){var d=y(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return v(this,c)}}function v(a,b){if(b&&("object"===m(b)||"function"==typeof b)){return b}return w(a)}function w(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function x(){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 y(a){y=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return y(a)}var A={},B={},C=function(a){s(b,a);var d=u(b);function b(){n(this,b);return d.apply(this,arguments)}p(b,[{key:"setInitialState",value:function setInitialState(a){q(y(b.prototype),"setInitialState",this).call(this,a);c.default.debug("Debug module \"M.reactive\" loaded.")}},{key:"registerNewInstance",value:function registerNewInstance(a){var b,d=this,e=null!==(b=a.name)&&void 0!==b?b:"instance".concat(this.state.reactives.length);e=e.replace(/\W/g,"");c.default.debug("Registering new reactive instance \"M.reactive.".concat(e,"\""));A[e]=a;B[e]=new E(A[e]);this.dispatch("putInstance",e,a);var f=function(){d.dispatch("putInstance",e,a)};a.target.addEventListener("readmode:on",f);a.target.addEventListener("readmode:off",f);a.target.addEventListener("registerComponent:success",f);a.target.addEventListener("transaction:end",f);var g=function(a){var b=a.detail,c=null===b||void 0===b?void 0:b.changes;d.dispatch("lastTransaction",e,c)};a.target.addEventListener("transaction:start",g)}},{key:"debug",value:function debug(a){return B[a]}},{key:"list",get:function get(){return JSON.parse(JSON.stringify(this.state.reactives))}}]);return b}(b.default),D=function(){function a(){n(this,a)}p(a,[{key:"putInstance",value:function putInstance(a,b,c){var d=a.state;a.setReadOnly(!1);if(d.reactives.has(b)){d.reactives.get(b).countcomponents=c.components.length;d.reactives.get(b).readOnly=c.stateManager.readonly;d.reactives.get(b).modified=new Date().getTime()}else{d.reactives.add({id:b,countcomponents:c.components.length,readOnly:c.stateManager.readonly,lastChanges:[],modified:new Date().getTime()})}a.setReadOnly(!0)}},{key:"lastTransaction",value:function lastTransaction(a,b,c){if(!c||0===c.length){return}var d=a.state,e=["transaction:start"];c.forEach(function(a){e.push(a.eventName)});e.push("transaction:end");a.setReadOnly(!1);d.reactives.get(b).lastChanges=e;a.setReadOnly(!0)}}]);return a}(),E=function(){function a(b){n(this,a);this.instance=b;if(b._reactiveDebugData===void 0){b._reactiveDebugData={highlighted:!1}}}p(a,[{key:"dispatch",value:function(){var a=l(regeneratorRuntime.mark(function a(){var b,c=arguments;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:(b=this.instance).dispatch.apply(b,c);case 1:case"end":return a.stop();}}},a,this)}));return function dispatch(){return a.apply(this,arguments)}}()},{key:"processUpdates",value:function processUpdates(a){this.instance.stateManager.processUpdates(a)}},{key:"readOnly",set:function set(a){this.instance.stateManager.setReadOnly(a)},get:function get(){return this.instance.stateManager.readonly}},{key:"state",get:function get(){return this.instance.state}},{key:"highlight",set:function set(a){this.instance._reactiveDebugData.highlighted=a;this.instance.components.forEach(function(b){var c=b.element,d=a?"thick solid #0000FF":"";c.style.border=d})},get:function get(){return this.instance._reactiveDebugData.highlighted}},{key:"components",get:function get(){return e(this.instance.components)}},{key:"changes",get:function get(){var a=[];this.instance.stateManager.eventsToPublish.forEach(function(b){a.push(b.eventName)});return a}},{key:"elements",get:function get(){var a=[];this.instance.components.forEach(function(b){var c=b.element;a.push(c)});return a}},{key:"stateData",get:function get(){return JSON.parse(JSON.stringify(this.state))}}]);return a}(),F="core_reactive_debug:stateChanged";function z(a,b){if(b===void 0){b=document}b.dispatchEvent(new CustomEvent(F,{bubbles:!0,detail:a}))}a.initDebug=function initDebug(){var a=new C({name:"CoreReactiveDebug",eventName:F,eventDispatch:z,mutations:new D,state:{reactives:[]}});B.registerNewInstance=a.registerNewInstance.bind(a);return{debug:a,debuggers:B}}});
+//# sourceMappingURL=debug.min.js.map
diff --git a/lib/amd/build/local/reactive/debug.min.js.map b/lib/amd/build/local/reactive/debug.min.js.map
new file mode 100644
index 00000000000..e6855c5e1f7
--- /dev/null
+++ b/lib/amd/build/local/reactive/debug.min.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../../../src/local/reactive/debug.js"],"names":["reactiveInstances","reactiveDebuggers","Debug","stateData","log","debug","instance","name","state","reactives","length","replace","DebugInstance","dispatch","refreshMethod","target","addEventListener","storeTransaction","detail","changes","JSON","parse","stringify","Reactive","Mutations","stateManager","setReadOnly","has","get","countcomponents","components","readOnly","readonly","modified","Date","getTime","add","id","lastChanges","forEach","change","push","eventName","_reactiveDebugData","highlighted","updates","processUpdates","value","element","border","style","result","eventsToPublish","stateChangedEventName","dispatchStateChangedEvent","document","dispatchEvent","CustomEvent","bubbles","initDebug","eventDispatch","mutations","registerNewInstance","bind","debuggers"],"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 <http://www.gnu.org/licenses/>.\n\n/**\n * Reactive module debug tools.\n *\n * @module     core/reactive/local/reactive/debug\n * @copyright  2021 Ferran Recio <ferran@moodle.com>\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 <ferran@moodle.com>\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 <ferran@moodle.com>\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<b;c++){d[c]=a[c]}return d}function j(a,b){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(a)))return;var c=[],d=!0,e=!1,f=void 0;try{for(var g=a[Symbol.iterator](),h;!(d=(h=g.next()).done);d=!0){c.push(h.value);if(b&&c.length===b)break}}catch(a){e=!0;f=a}finally{try{if(!d&&null!=g["return"])g["return"]()}finally{if(e)throw f}}return c}function k(a){if(Array.isArray(a))return a}function l(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){l=function(a){return typeof a}}else{l=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return l(a)}function m(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function n(a){for(var b=1,c;b<arguments.length;b++){c=null!=arguments[b]?arguments[b]:{};if(b%2){m(Object(c),!0).forEach(function(b){o(a,b,c[b])})}else if(Object.getOwnPropertyDescriptors){Object.defineProperties(a,Object.getOwnPropertyDescriptors(c))}else{m(Object(c)).forEach(function(b){Object.defineProperty(a,b,Object.getOwnPropertyDescriptor(c,b))})}}return a}function o(a,b,c){if(b in a){Object.defineProperty(a,b,{value:c,enumerable:!0,configurable:!0,writable:!0})}else{a[b]=c}return a}function p(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 q(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){p(h,d,e,f,g,"next",a)}function g(a){p(h,d,e,f,g,"throw",a)}f(void 0)})}}function r(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function s(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function t(a,b,c){if(b)s(a.prototype,b);if(c)s(a,c);return a}function u(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)v(a,b)}function v(a,b){v=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return v(a,b)}function w(a){return function(){var b=A(a),c;if(z()){var d=A(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return x(this,c)}}function x(a,b){if(b&&("object"===l(b)||"function"==typeof b)){return b}return y(a)}function y(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function z(){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 A(a){A=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return A(a)}a.init=function init(a,c){var d=document.getElementById(a);if(b.debug===void 0){d.remove();return}new B({element:d,reactive:b.debug,selectors:c})};a.initsubpanel=function initsubpanel(a,c){var d=document.getElementById(a);if(b.debug===void 0){d.remove();return}new C({element:d,reactive:b.debug,selectors:c})};var B=function(a){u(b,a);var d=w(b);function b(){r(this,b);return d.apply(this,arguments)}t(b,[{key:"create",value:function create(){this.name="GlobalDebugPanel";this.selectors={LOADERS:"[data-for='loaders']",SUBPANEL:"[data-for='subpanel']",LOG:"[data-for='log']"}}},{key:"stateReady",value:function stateReady(a){var b=this;if(0<a.reactives.size){this.getElement(this.selectors.LOADERS).innerHTML=""}a.reactives.forEach(function(a){b._createLoader(a)});this.getElement(this.selectors.SUBPANEL).innerHTML=""}},{key:"_createLoader",value:function _createLoader(a){var b=this,c=this.getElement(this.selectors.LOADERS),d=document.createElement("button");d.innerHTML=a.id;d.dataset.id=a.id;c.appendChild(d);this.addEventListener(d,"click",function(){return b._openPanel(d,a)})}},{key:"_openPanel",value:function(){var a=q(regeneratorRuntime.mark(function a(b,d){var e,f;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:a.prev=0;e=this.getElement(this.selectors.SUBPANEL);f=n({},d);a.next=5;return this.renderComponent(e,"core/local/reactive/debuginstancepanel",f);case 5:a.next=11;break;case 7:a.prev=7;a.t0=a["catch"](0);c.default.error("Cannot load reactive debug subpanel");throw a.t0;case 11:case"end":return a.stop();}}},a,this,[[0,7]])}));return function _openPanel(){return a.apply(this,arguments)}}()}]);return b}(b.BaseComponent),C=function(a){u(c,a);var f=w(c);function c(){r(this,c);return f.apply(this,arguments)}t(c,[{key:"create",value:function create(){this.name="DebugInstanceSubpanel";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];this.draggable=!1;this.relativeDrag=!0;this.strings={savewarning:""}}},{key:"stateReady",value:function stateReady(){var a=this,c,e;this.dragdrop=new b.DragDrop(this);this.addEventListener(this.getElement(this.selectors.CLOSE),"click",this.remove);if(this.controller.highlight){this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT))}this.addEventListener(this.getElement(this.selectors.HIGHLIGHT),"click",function(){a.controller.highlight=!a.controller.highlight;a._toggleButtonText(a.getElement(a.selectors.HIGHLIGHT))});this.addEventListener(this.getElement(this.selectors.READMODE),"click",this._toggleEditMode);this.addEventListener(this.getElement(this.selectors.CLEAN),"click",this._cleanAreas);this.addEventListener(this.getElement(this.selectors.PIN),"click",this._togglePin);this.getElement(this.selectors.SAVE).disabled=!0;this.addEventListener(this.getElement(this.selectors.STATE),"keyup",(0,d.debounce)(this._checkJSON,500));this.addEventListener(this.getElement(this.selectors.SAVE),"click",this._saveState);this.strings.savewarning=null!==(c=null===(e=this.getElement(this.selectors.INVALID))||void 0===e?void 0:e.innerHTML)&&void 0!==c?c:"";this._refreshState()}},{key:"destroy",value:function destroy(){if(this.dragdrop!==void 0){this.dragdrop.unregister()}}},{key:"getWatchers",value:function getWatchers(){return[{watch:"reactives[".concat(this.id,"].lastChanges:updated"),handler:this._refreshLog},{watch:"reactives[".concat(this.id,"].modified:updated"),handler:this._refreshState},{watch:"reactives[".concat(this.id,"].readOnly:updated"),handler:this._refreshReadOnly}]}},{key:"_refreshLog",value:function _refreshLog(a){var b,c=a.element,d=null!==(b=null===c||void 0===c?void 0:c.lastChanges)&&void 0!==b?b:[],e=d.join("\n"),f=this.getElement(this.selectors.LOG);f.value+="\n\n= Transaction =\n ".concat(e);f.scrollTop=f.scrollHeight}},{key:"_cleanAreas",value:function _cleanAreas(){var a=this.getElement(this.selectors.LOG);a.value="";this._refreshState()}},{key:"_refreshState",value:function _refreshState(){var a=this.getElement(this.selectors.STATE);a.value=JSON.stringify(this.controller.state,null,4)}},{key:"_refreshReadOnly",value:function _refreshReadOnly(){var a=this.getElement(this.selectors.READMODE);if(a.dataset.readonly===void 0){a.dataset.readonly=a.innerHTML}if(this.controller.readOnly){a.innerHTML=a.dataset.readonly}else{a.innerHTML=a.dataset.alt}}},{key:"_toggleEditMode",value:function _toggleEditMode(){this.controller.readOnly=!this.controller.readOnly}},{key:"_checkJSON",value:function _checkJSON(){var a=this.getElement(this.selectors.INVALID),b=this.getElement(this.selectors.SAVE),c=this.getElement(this.selectors.STATE).value,d=this.controller.stateData;if(c==JSON.stringify(this.controller.state,null,4)){a.style.color="";a.innerHTML="";b.disabled=!0;return}try{var e=JSON.parse(c),f=this._generateStateUpdates(d,e);a.style.color="";a.innerHTML=this.strings.savewarning;b.disabled=!1;return f}catch(c){var g;a.style.color="red";a.innerHTML=null!==(g=c.message)&&void 0!==g?g:"Invalid JSON sctructure";b.disabled=!0}}},{key:"_saveState",value:function _saveState(){var a=this._checkJSON();if(!a){return}this.controller.processUpdates(a)}},{key:"_generateStateUpdates",value:function _generateStateUpdates(a,b){for(var c=[],d={},f=function(){var a=e(h[g],2),b=a[0],f=a[1];if(Array.isArray(f)){d[b]={};f.forEach(function(a){if(a.id===void 0){throw Error("Array ".concat(b," element without id attribute"))}c.push({name:b,action:"override",fields:a});var e=(a.id+"").valueOf();d[b][e]=!0})}else{c.push({name:b,action:"override",fields:f})}},g=0,h=Object.entries(b);g<h.length;g++){f()}for(var i=function(){var a=e(k[j],2),f=a[0],g=a[1],h=!1;if(b[f]===void 0){h=!0}if(Array.isArray(g)){if(!h&&d[f]===void 0){throw Error("Array ".concat(f," cannot change to object."))}g.forEach(function(a){var b=(a.id+"").valueOf(),e=h;if(!e&&d[f][b]===void 0){e=!0}if(e){c.push({name:f,action:"delete",fields:a})}})}else{if(!h&&d[f]!==void 0){throw Error("Object ".concat(f," cannot change to array."))}if(h){c.push({name:f,action:"delete",fields:g})}}},j=0,k=Object.entries(a);j<k.length;j++){i()}return c}},{key:"getDraggableData",value:function getDraggableData(){return this.draggable}},{key:"dragEnd",value:function dragEnd(a,b){this.element.style.top="".concat(b.newFixedTop,"px");this.element.style.left="".concat(b.newFixedLeft,"px")}},{key:"_togglePin",value:function _togglePin(){this.draggable=!this.draggable;this.dragdrop.setDraggable(this.draggable);if(this.draggable){this._unpin()}else{this._pin()}}},{key:"_unpin",value:function _unpin(){var a=window.innerHeight/2,b=window.innerWidth/2,c={position:"fixed",resize:"both",overflow:"auto",height:"400px",width:"400px",top:"".concat(a-200,"px"),left:"".concat(b-200,"px")};Object.assign(this.element.style,c);this.getElement(this.selectors.STATE).style.height="50px";this.getElement(this.selectors.LOG).style.height="50px";this._toggleButtonText(this.getElement(this.selectors.PIN))}},{key:"_pin",value:function _pin(){var a=this;["position","resize","overflow","top","left","height","width"].forEach(function(b){return a.element.style.removeProperty(b)});this._toggleButtonText(this.getElement(this.selectors.PIN))}},{key:"_toggleButtonText",value:function _toggleButtonText(a){var b=[a.dataset.alt,a.innerHTML];a.innerHTML=b[0];a.dataset.alt=b[1]}}]);return c}(b.BaseComponent)});
+//# sourceMappingURL=debugpanel.min.js.map
diff --git a/lib/amd/build/local/reactive/debugpanel.min.js.map b/lib/amd/build/local/reactive/debugpanel.min.js.map
new file mode 100644
index 00000000000..ad4de543e8d
--- /dev/null
+++ b/lib/amd/build/local/reactive/debugpanel.min.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../../../src/local/reactive/debugpanel.js"],"names":["init","target","selectors","element","document","getElementById","debug","remove","GlobalDebugPanel","reactive","initsubpanel","DebugInstanceSubpanel","name","LOADERS","SUBPANEL","LOG","state","reactives","size","getElement","innerHTML","forEach","instance","_createLoader","loaders","btn","createElement","id","dataset","appendChild","addEventListener","_openPanel","data","renderComponent","log","error","BaseComponent","NAME","CLOSE","READMODE","HIGHLIGHT","STATE","CLEAN","PIN","SAVE","INVALID","controller","M","draggable","relativeDrag","strings","savewarning","dragdrop","DragDrop","highlight","_toggleButtonText","_toggleEditMode","_cleanAreas","_togglePin","disabled","_checkJSON","_saveState","_refreshState","unregister","watch","handler","_refreshLog","_refreshReadOnly","list","lastChanges","logContent","join","value","scrollTop","scrollHeight","JSON","stringify","readonly","readOnly","alt","invalid","save","edited","currentStateData","stateData","style","color","newState","parse","result","_generateStateUpdates","message","updates","processUpdates","newStateData","ids","key","newValue","Array","isArray","Error","push","action","fields","index","valueOf","Object","entries","oldValue","deleteField","deleteEntry","dropdata","event","top","newFixedTop","left","newFixedLeft","setDraggable","_unpin","_pin","pageCenterY","window","innerHeight","pageCenterX","innerWidth","position","resize","overflow","height","width","assign","prop","removeProperty"],"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 <http://www.gnu.org/licenses/>.\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 <ferran@moodle.com>\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);c<b;c++){d[c]=a[c]}return d}function m(a,b){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(a)))return;var c=[],d=!0,e=!1,f=void 0;try{for(var g=a[Symbol.iterator](),h;!(d=(h=g.next()).done);d=!0){c.push(h.value);if(b&&c.length===b)break}}catch(a){e=!0;f=a}finally{try{if(!d&&null!=g["return"])g["return"]()}finally{if(e)throw f}}return c}function n(a){if(Array.isArray(a))return a}function o(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function p(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function q(a,b,c){if(b)p(a.prototype,b);if(c)p(a,c);return a}var r=0,s=function(){function a(b){var e,f;o(this,a);if(b.eventName===void 0||b.eventDispatch===void 0){throw new Error("Reactivity event required")}if(b.name!==void 0){this.name=b.name}this.target=null!==(e=b.target)&&void 0!==e?e:document.createTextNode(null);this.eventName=b.eventName;this.eventDispatch=b.eventDispatch;this.stateManager=new c.default(this.eventDispatch,this.target);this.watchers=new Map([]);this.components=new Set([]);this.mutations=null!==(f=b.mutations)&&void 0!==f?f:{};this.target.addEventListener(this.eventName,this.callWatchersHandler.bind(this));this.pendingState=new d.default("core/reactive:registerInstance".concat(r++));if(b.state!==void 0){this.setInitialState(b.state)}}q(a,[{key:"callWatchersHandler",value:function callWatchersHandler(a){this.target.dispatchEvent(new CustomEvent(a.detail.action,{bubbles:!1,detail:a.detail}))}},{key:"setInitialState",value:function setInitialState(a){this.pendingState.resolve();this.stateManager.setInitialState(a)}},{key:"addMutations",value:function addMutations(a){if(a.init!==void 0){a.init(this.stateManager)}for(var b=0,c=Object.entries(a);b<c.length;b++){var d=h(c[b],2),e=d[0],f=d[1];this.mutations[e]=f.bind(a)}}},{key:"setMutations",value:function setMutations(a){this.mutations=a;if(a.init!==void 0){a.init(this.stateManager)}}},{key:"getInitialStatePromise",value:function getInitialStatePromise(){return this.stateManager.getInitialPromise()}},{key:"registerComponent",value:function registerComponent(a){var c,e=this,f=null!==(c=a.name)&&void 0!==c?c:"Unkown component",g=function(){},h=g;if(a.dispatchRegistrationSuccess!==void 0){g=a.dispatchRegistrationSuccess.bind(a)}if(a.dispatchRegistrationFail!==void 0){h=a.dispatchRegistrationFail.bind(a)}if(this.components.has(a)){g();return a}var i=new d.default("core/reactive:registerComponent".concat(r++)),j=[],k=[];if(a.getWatchers!==void 0){k=a.getWatchers()}k.forEach(function(b){var c=b.watch,d=b.handler;if(c===void 0){h();throw new Error("Missing watch attribute in ".concat(f," watcher"))}if(d===void 0){h();throw new Error("Missing handler for watcher ".concat(c," in ").concat(f))}var g=function(b){d.apply(a,[b.detail])};j.push({target:e.target,watch:c,listener:g});e.target.addEventListener(c,g)});if(a.stateReady!==void 0){this.getInitialStatePromise().then(function(b){a.stateReady(b);i.resolve();return!0}).catch(function(a){i.resolve();b.default.error("Initial state in ".concat(f," rejected due to: ").concat(a));b.default.error(a)})}this.watchers.set(a,j);this.components.add(a);g();return a}},{key:"unregisterComponent",value:function unregisterComponent(a){if(!this.components.has(a)){return a}this.components.delete(a);var b=this.watchers.get(a);if(b===void 0){return a}b.forEach(function(a){var b=a.target,c=a.watch,d=a.listener;b.removeEventListener(c,d)});this.watchers.delete(a);return a}},{key:"dispatch",value:function(){var a=g(regeneratorRuntime.mark(function a(b){var c,e,f,g,h,i=arguments;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:if(!("string"!=typeof b)){a.next=2;break}throw new Error("Dispatch action name must be a string");case 2:if(!("_"===b.charAt(0))){a.next=4;break}throw new Error("Illegal Private ".concat(b," mutation method dispatch"));case 4:if(!(this.mutations[b]===void 0)){a.next=6;break}throw new Error("Unkown ".concat(b," mutation"));case 6:c=new d.default("core/reactive:".concat(b).concat(r++));e=this.mutations[b];a.prev=8;for(f=i.length,g=Array(1<f?f-1:0),h=1;h<f;h++){g[h-1]=i[h]}a.next=12;return e.apply(this.mutations,[this.stateManager].concat(g));case 12:c.resolve();a.next=20;break;case 15:a.prev=15;a.t0=a["catch"](8);this.stateManager.setReadOnly(!0);c.resolve();throw a.t0;case 20:case"end":return a.stop();}}},a,this,[[8,15]])}));return function dispatch(){return a.apply(this,arguments)}}()},{key:"state",get:function get(){return this.stateManager.state}}]);return a}();a.default=s;return a.default});
+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);c<b;c++){d[c]=a[c]}return d}function m(a,b){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(a)))return;var c=[],d=!0,e=!1,f=void 0;try{for(var g=a[Symbol.iterator](),h;!(d=(h=g.next()).done);d=!0){c.push(h.value);if(b&&c.length===b)break}}catch(a){e=!0;f=a}finally{try{if(!d&&null!=g["return"])g["return"]()}finally{if(e)throw f}}return c}function n(a){if(Array.isArray(a))return a}function o(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function p(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function q(a,b,c){if(b)p(a.prototype,b);if(c)p(a,c);return a}var r=0,s=function(){function a(b){var e,f;o(this,a);if(b.eventName===void 0||b.eventDispatch===void 0){throw new Error("Reactivity event required")}if(b.name!==void 0){this.name=b.name}this.target=null!==(e=b.target)&&void 0!==e?e:document.createTextNode(null);this.eventName=b.eventName;this.eventDispatch=b.eventDispatch;this.stateManager=new c.default(this.eventDispatch,this.target);this.watchers=new Map([]);this.components=new Set([]);this.mutations=null!==(f=b.mutations)&&void 0!==f?f:{};this.target.addEventListener(this.eventName,this.callWatchersHandler.bind(this));this.pendingState=new d.default("core/reactive:registerInstance".concat(r++));if(b.state!==void 0){this.setInitialState(b.state)}if(M.reactive!==void 0){M.reactive.registerNewInstance(this)}}q(a,[{key:"callWatchersHandler",value:function callWatchersHandler(a){this.target.dispatchEvent(new CustomEvent(a.detail.action,{bubbles:!1,detail:a.detail}))}},{key:"setInitialState",value:function setInitialState(a){this.pendingState.resolve();this.stateManager.setInitialState(a)}},{key:"addMutations",value:function addMutations(a){if(a.init!==void 0){a.init(this.stateManager)}for(var b=0,c=Object.entries(a);b<c.length;b++){var d=h(c[b],2),e=d[0],f=d[1];this.mutations[e]=f.bind(a)}}},{key:"setMutations",value:function setMutations(a){this.mutations=a;if(a.init!==void 0){a.init(this.stateManager)}}},{key:"getInitialStatePromise",value:function getInitialStatePromise(){return this.stateManager.getInitialPromise()}},{key:"registerComponent",value:function registerComponent(a){var c,e=this,f=null!==(c=a.name)&&void 0!==c?c:"Unkown component",g=function(){},h=g;if(a.dispatchRegistrationSuccess!==void 0){g=a.dispatchRegistrationSuccess.bind(a)}if(a.dispatchRegistrationFail!==void 0){h=a.dispatchRegistrationFail.bind(a)}if(this.components.has(a)){g();return a}var i=new d.default("core/reactive:registerComponent".concat(r++)),j=[],k=[];if(a.getWatchers!==void 0){k=a.getWatchers()}k.forEach(function(b){var c=b.watch,d=b.handler;if(c===void 0){h();throw new Error("Missing watch attribute in ".concat(f," watcher"))}if(d===void 0){h();throw new Error("Missing handler for watcher ".concat(c," in ").concat(f))}var g=function(b){d.apply(a,[b.detail])};j.push({target:e.target,watch:c,listener:g});e.target.addEventListener(c,g)});if(a.stateReady!==void 0){this.getInitialStatePromise().then(function(b){a.stateReady(b);i.resolve();return!0}).catch(function(a){i.resolve();b.default.error("Initial state in ".concat(f," rejected due to: ").concat(a));b.default.error(a)})}this.watchers.set(a,j);this.components.add(a);this.target.dispatchEvent(new CustomEvent("registerComponent:success",{bubbles:!1,detail:{component:a}}));g();return a}},{key:"unregisterComponent",value:function unregisterComponent(a){if(!this.components.has(a)){return a}this.components.delete(a);var b=this.watchers.get(a);if(b===void 0){return a}b.forEach(function(a){var b=a.target,c=a.watch,d=a.listener;b.removeEventListener(c,d)});this.watchers.delete(a);return a}},{key:"dispatch",value:function(){var a=g(regeneratorRuntime.mark(function a(b){var c,e,f,g,h,i=arguments;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:if(!("string"!=typeof b)){a.next=2;break}throw new Error("Dispatch action name must be a string");case 2:if(!("_"===b.charAt(0))){a.next=4;break}throw new Error("Illegal Private ".concat(b," mutation method dispatch"));case 4:if(!(this.mutations[b]===void 0)){a.next=6;break}throw new Error("Unkown ".concat(b," mutation"));case 6:c=new d.default("core/reactive:".concat(b).concat(r++));e=this.mutations[b];a.prev=8;for(f=i.length,g=Array(1<f?f-1:0),h=1;h<f;h++){g[h-1]=i[h]}a.next=12;return e.apply(this.mutations,[this.stateManager].concat(g));case 12:c.resolve();a.next=20;break;case 15:a.prev=15;a.t0=a["catch"](8);this.stateManager.setReadOnly(!0);c.resolve();throw a.t0;case 20:case"end":return a.stop();}}},a,this,[[8,15]])}));return function dispatch(){return a.apply(this,arguments)}}()},{key:"state",get:function get(){return this.stateManager.state}}]);return a}();a.default=s;return a.default});
 //# sourceMappingURL=reactive.min.js.map
diff --git a/lib/amd/build/local/reactive/reactive.min.js.map b/lib/amd/build/local/reactive/reactive.min.js.map
index 78a5702f06f..e0fd086cd3a 100644
--- a/lib/amd/build/local/reactive/reactive.min.js.map
+++ b/lib/amd/build/local/reactive/reactive.min.js.map
@@ -1 +1 @@
-{"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","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 <http://www.gnu.org/licenses/>.\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 <ferran@moodle.com>\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 <http://www.gnu.org/licenses/>.\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 <ferran@moodle.com>\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);c<b;c++){d[c]=a[c]}return d}function s(a,b){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(a)))return;var c=[],d=!0,e=!1,f=void 0;try{for(var g=a[Symbol.iterator](),h;!(d=(h=g.next()).done);d=!0){c.push(h.value);if(b&&c.length===b)break}}catch(a){e=!0;f=a}finally{try{if(!d&&null!=g["return"])g["return"]()}finally{if(e)throw f}}return c}function t(a){if(Array.isArray(a))return a}function u(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function v(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function w(a,b,c){if(b)v(a.prototype,b);if(c)v(a,c);return a}var x=function(){function a(b,c){var d=this;u(this,a);this.dispatchEvent=b;this.target=null!==c&&void 0!==c?c:document;this.readonly=!1;this.eventsToPublish=[];this.updateTypes={create:this.defaultCreate.bind(this),update:this.defaultUpdate.bind(this),delete:this.defaultDelete.bind(this),put:this.defaultPut.bind(this),override:this.defaultOverride.bind(this),prepareFields:this.defaultPrepareFields.bind(this)};this.initialPromise=new Promise(function(a){d.target.addEventListener("state:loaded",function initialStateDone(b){a(b.detail.state)})})}w(a,[{key:"setInitialState",value:function setInitialState(a){if(this.state!==void 0){throw Error("Initial state can only be initialized ones")}for(var b=new Proxy({},new y("state",this,!0)),c=0,d=Object.entries(a);c<d.length;c++){var e=o(d[c],2),f=e[0],g=e[1];b[f]=g}this.state=b;this.readonly=!0;this.dispatchEvent({action:"state:loaded",state:this.state},this.target)}},{key:"getInitialPromise",value:function getInitialPromise(){return this.initialPromise}},{key:"setReadOnly",value:function setReadOnly(a){this.readonly=a;if(this.readonly){this._publishEvents()}}},{key:"addUpdateTypes",value:function addUpdateTypes(a){for(var b=0,c=Object.entries(a);b<c.length;b++){var d=o(c[b],2),e=d[0],f=d[1];if("function"==typeof f){this.updateTypes[e]=f.bind(a)}}}},{key:"processUpdates",value:function processUpdates(a,b){var c=this;if(!Array.isArray(a)){throw Error("State updates must be an array")}this.setReadOnly(!1);a.forEach(function(a){if(a.name===void 0){throw Error("Missing state update name")}c.processUpdate(a.name,a.action,a.fields,b)});this.setReadOnly(!0)}},{key:"processUpdate",value:function processUpdate(a,b,c,d){var e,f,g;if(!c){throw Error("Missing state update fields")}if(d===void 0){d={}}b=null!==(e=b)&&void 0!==e?e:"update";var h=null!==(f=d[b])&&void 0!==f?f:this.updateTypes[b];if(h===void 0){throw Error("Unkown update action ".concat(b))}var i=null!==(g=d.prepareFields)&&void 0!==g?g:this.updateTypes.prepareFields;h(this,a,i(this,a,c))}},{key:"defaultPrepareFields",value:function defaultPrepareFields(a,b,c){return c}},{key:"defaultCreate",value:function defaultCreate(a,b,c){var d=a.state;if(d[b]instanceof z){d[b].add(c);return}d[b]=c}},{key:"defaultDelete",value:function defaultDelete(a,b,c){var d=a.get(b,c.id);if(!d){throw Error("Inexistent ".concat(b," ").concat(c.id))}var e=a.state;if(e[b]instanceof z){e[b].delete(c.id);return}delete e[b]}},{key:"defaultUpdate",value:function defaultUpdate(a,b,c){var d=a.get(b,c.id);if(!d){throw Error("Inexistent ".concat(b," ").concat(c.id))}for(var e=0,f=Object.entries(c);e<f.length;e++){var g=o(f[e],2),h=g[0],i=g[1];d[h]=i}}},{key:"defaultPut",value:function defaultPut(a,b,c){var d=a.get(b,c.id);if(d){for(var e=0,f=Object.entries(c);e<f.length;e++){var g=o(f[e],2),h=g[0],i=g[1];d[h]=i}}else{var j=a.state;if(j[b]instanceof z){j[b].add(c);return}j[b]=c}}},{key:"defaultOverride",value:function defaultOverride(a,b,c){var d=a.get(b,c.id);if(d){for(var e=0,f=Object.entries(d);e<f.length;e++){var g=o(f[e],1),h=g[0];if(c[h]===void 0){delete d[h]}}for(var i=0,j=Object.entries(c);i<j.length;i++){var k=o(j[i],2),l=k[0],m=k[1];d[l]=m}}else{var n=a.state;if(n[b]instanceof z){n[b].add(c);return}n[b]=c}}},{key:"get",value:function get(a,b){var c=this.state,d=c[a];if(d instanceof z){if(b===void 0){throw Error("Missing id for ".concat(a," state update"))}d=c[a].get(b)}return d}},{key:"registerStateAction",value:function registerStateAction(a,b,c,d){var e="updated";if(null!==b){this.eventsToPublish.push({eventName:"".concat(a,".").concat(b,":").concat(c),eventData:d,action:c})}else{e=c}if(d.id!==void 0){if(null!==b){this.eventsToPublish.push({eventName:"".concat(a,"[").concat(d.id,"].").concat(b,":").concat(c),eventData:d,action:c})}this.eventsToPublish.push({eventName:"".concat(a,"[").concat(d.id,"]:").concat(e),eventData:d,action:e})}this.eventsToPublish.push({eventName:"".concat(a,":").concat(e),eventData:d,action:e});this.eventsToPublish.push({eventName:"state:updated",eventData:d,action:"updated"})}},{key:"_publishEvents",value:function _publishEvents(){var a=this,b=this.eventsToPublish;this.eventsToPublish=[];this.dispatchEvent({action:"transaction:start",state:this.state,element:null},this.target);b.sort(function(c,a){var b,d,e={created:0,updated:1,deleted:2},f=null!==(b=e[c.action])&&void 0!==b?b:0,g=null!==(d=e[a.action])&&void 0!==d?d:0;if(f===g){return c.eventName.length-a.eventName.length}return f-g});var c=new Set;b.forEach(function(b){var d,e="".concat(b.eventName,".").concat(null!==(d=b.eventData.id)&&void 0!==d?d:0);if(!c.has(e)){a.dispatchEvent({action:b.eventName,state:a.state,element:b.eventData},a.target);c.add(e)}});this.dispatchEvent({action:"transaction:end",state:this.state,element:null},this.target)}}]);return a}();a.default=x;var y=function(){function a(b,c,d){u(this,a);this.name=b;this.stateManager=c;this.proxyValues=null!==d&&void 0!==d?d:!1}w(a,[{key:"set",value:function set(b,c,d,e){if(this.stateManager.readonly){throw new Error("State locked. Use mutations to change ".concat(c," value in ").concat(this.name,"."))}if(JSON.stringify(b[c])===JSON.stringify(d)){return!0}var f=b[c]!==void 0?"updated":"created";if(this.proxyValues){if(Array.isArray(d)){b[c]=new z(c,this.stateManager).loadValues(d)}else{b[c]=new Proxy(d,new a(c,this.stateManager))}}else{b[c]=d}if(this.stateManager.state===void 0){return!0}this.stateManager.registerStateAction(this.name,c,f,e);return!0}},{key:"deleteProperty",value:function deleteProperty(a,b){if(this.stateManager.readonly){throw new Error("State locked. Use mutations to delete ".concat(b," in ").concat(this.name,"."))}if(b in a){delete a[b];this.stateManager.registerStateAction(this.name,b,"deleted",a)}return!0}}]);return a}(),z=function(a){e(d,a);var g=f(d);function d(a,b,c){var e;u(this,d);e=g.call(this,c);e.name=a;e.stateManager=b;return e}w(d,[{key:"set",value:function set(a,b){if(this.stateManager.readonly){throw new Error("State locked. Use mutations to change ".concat(a," value in ").concat(this.name,"."))}a=this.normalizeKey(a);this.checkValue(b);if(a===void 0||null===a){throw Error("State lists keys cannot be null or undefined")}if(this.normalizeKey(b.id)!==a){throw new Error("State error: ".concat(this.name," list element ID (").concat(b.id,") and key (").concat(a,") mismatch"))}var e=c(n(d.prototype),"has",this).call(this,a)?"updated":"created",f=c(n(d.prototype),"set",this).call(this,a,new Proxy(b,new y(this.name,this.stateManager)));if(this.stateManager.state===void 0){return f}this.stateManager.registerStateAction(this.name,null,e,c(n(d.prototype),"get",this).call(this,a));return f}},{key:"checkValue",value:function checkValue(a){if("object"===!b(a)&&null!==a){throw Error("State lists can contain objects only")}if(a.id===void 0){throw Error("State lists elements must contain at least an id attribute")}}},{key:"normalizeKey",value:function normalizeKey(a){return(a+"").valueOf()}},{key:"add",value:function add(a){this.checkValue(a);return this.set(a.id,a)}},{key:"get",value:function get(a){return c(n(d.prototype),"get",this).call(this,this.normalizeKey(a))}},{key:"has",value:function has(a){return c(n(d.prototype),"has",this).call(this,this.normalizeKey(a))}},{key:"delete",value:function _delete(a){a=this.normalizeKey(a);if(this.stateManager.readonly){throw new Error("State locked. Use mutations to change ".concat(a," value in ").concat(this.name,"."))}var b=c(n(d.prototype),"get",this).call(this,a),e=c(n(d.prototype),"delete",this).call(this,a);if(!e){return e}this.stateManager.registerStateAction(this.name,null,"deleted",b);return e}},{key:"toJSON",value:function toJSON(){var a=[];this.forEach(function(b){a.push(b)});return a}},{key:"loadValues",value:function loadValues(a){var b=this;a.forEach(function(a){b.checkValue(a);var c=a.id,d=new Proxy(a,new y(b.name,b.stateManager));b.set(c,d)});return this}}]);return d}(i(Map));return a.default});
+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);c<b;c++){d[c]=a[c]}return d}function s(a,b){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(a)))return;var c=[],d=!0,e=!1,f=void 0;try{for(var g=a[Symbol.iterator](),h;!(d=(h=g.next()).done);d=!0){c.push(h.value);if(b&&c.length===b)break}}catch(a){e=!0;f=a}finally{try{if(!d&&null!=g["return"])g["return"]()}finally{if(e)throw f}}return c}function t(a){if(Array.isArray(a))return a}function u(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function v(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function w(a,b,c){if(b)v(a.prototype,b);if(c)v(a,c);return a}var x=function(){function a(b,c){var d=this;u(this,a);this.dispatchEvent=b;this.target=null!==c&&void 0!==c?c:document;this.readonly=!1;this.eventsToPublish=[];this.updateTypes={create:this.defaultCreate.bind(this),update:this.defaultUpdate.bind(this),delete:this.defaultDelete.bind(this),put:this.defaultPut.bind(this),override:this.defaultOverride.bind(this),prepareFields:this.defaultPrepareFields.bind(this)};this.initialPromise=new Promise(function(a){d.target.addEventListener("state:loaded",function initialStateDone(b){a(b.detail.state)})})}w(a,[{key:"setInitialState",value:function setInitialState(a){if(this.state!==void 0){throw Error("Initial state can only be initialized ones")}for(var b=new Proxy({},new y("state",this,!0)),c=0,d=Object.entries(a);c<d.length;c++){var e=o(d[c],2),f=e[0],g=e[1];b[f]=g}this.state=b;this.readonly=!0;this.dispatchEvent({action:"state:loaded",state:this.state},this.target)}},{key:"getInitialPromise",value:function getInitialPromise(){return this.initialPromise}},{key:"setReadOnly",value:function setReadOnly(a){this.readonly=a;var b="off";if(this.readonly){b="on";this._publishEvents()}this.dispatchEvent({action:"readmode:".concat(b),state:this.state,element:null},this.target)}},{key:"addUpdateTypes",value:function addUpdateTypes(a){for(var b=0,c=Object.entries(a);b<c.length;b++){var d=o(c[b],2),e=d[0],f=d[1];if("function"==typeof f){this.updateTypes[e]=f.bind(a)}}}},{key:"processUpdates",value:function processUpdates(a,b){var c=this;if(!Array.isArray(a)){throw Error("State updates must be an array")}this.setReadOnly(!1);a.forEach(function(a){if(a.name===void 0){throw Error("Missing state update name")}c.processUpdate(a.name,a.action,a.fields,b)});this.setReadOnly(!0)}},{key:"processUpdate",value:function processUpdate(a,b,c,d){var e,f,g;if(!c){throw Error("Missing state update fields")}if(d===void 0){d={}}b=null!==(e=b)&&void 0!==e?e:"update";var h=null!==(f=d[b])&&void 0!==f?f:this.updateTypes[b];if(h===void 0){throw Error("Unkown update action ".concat(b))}var i=null!==(g=d.prepareFields)&&void 0!==g?g:this.updateTypes.prepareFields;h(this,a,i(this,a,c))}},{key:"defaultPrepareFields",value:function defaultPrepareFields(a,b,c){return c}},{key:"defaultCreate",value:function defaultCreate(a,b,c){var d=a.state;if(d[b]instanceof z){d[b].add(c);return}d[b]=c}},{key:"defaultDelete",value:function defaultDelete(a,b,c){var d=a.get(b,c.id);if(!d){throw Error("Inexistent ".concat(b," ").concat(c.id))}var e=a.state;if(e[b]instanceof z){e[b].delete(c.id);return}delete e[b]}},{key:"defaultUpdate",value:function defaultUpdate(a,b,c){var d=a.get(b,c.id);if(!d){throw Error("Inexistent ".concat(b," ").concat(c.id))}for(var e=0,f=Object.entries(c);e<f.length;e++){var g=o(f[e],2),h=g[0],i=g[1];d[h]=i}}},{key:"defaultPut",value:function defaultPut(a,b,c){var d=a.get(b,c.id);if(d){for(var e=0,f=Object.entries(c);e<f.length;e++){var g=o(f[e],2),h=g[0],i=g[1];d[h]=i}}else{var j=a.state;if(j[b]instanceof z){j[b].add(c);return}j[b]=c}}},{key:"defaultOverride",value:function defaultOverride(a,b,c){var d=a.get(b,c.id);if(d){for(var e=0,f=Object.entries(d);e<f.length;e++){var g=o(f[e],1),h=g[0];if(c[h]===void 0){delete d[h]}}for(var i=0,j=Object.entries(c);i<j.length;i++){var k=o(j[i],2),l=k[0],m=k[1];d[l]=m}}else{var n=a.state;if(n[b]instanceof z){n[b].add(c);return}n[b]=c}}},{key:"get",value:function get(a,b){var c=this.state,d=c[a];if(d instanceof z){if(b===void 0){throw Error("Missing id for ".concat(a," state update"))}d=c[a].get(b)}return d}},{key:"registerStateAction",value:function registerStateAction(a,b,c,d){var e="updated";if(null!==b){this.eventsToPublish.push({eventName:"".concat(a,".").concat(b,":").concat(c),eventData:d,action:c})}else{e=c}if(d.id!==void 0){if(null!==b){this.eventsToPublish.push({eventName:"".concat(a,"[").concat(d.id,"].").concat(b,":").concat(c),eventData:d,action:c})}this.eventsToPublish.push({eventName:"".concat(a,"[").concat(d.id,"]:").concat(e),eventData:d,action:e})}this.eventsToPublish.push({eventName:"".concat(a,":").concat(e),eventData:d,action:e});this.eventsToPublish.push({eventName:"state:updated",eventData:d,action:"updated"})}},{key:"_publishEvents",value:function _publishEvents(){var a=this,b=this.eventsToPublish;this.eventsToPublish=[];this.dispatchEvent({action:"transaction:start",state:this.state,element:null,changes:b},this.target);b.sort(function(c,a){var b,d,e={created:0,updated:1,deleted:2},f=null!==(b=e[c.action])&&void 0!==b?b:0,g=null!==(d=e[a.action])&&void 0!==d?d:0;if(f===g){return c.eventName.length-a.eventName.length}return f-g});var c=new Set;b.forEach(function(b){var d,e="".concat(b.eventName,".").concat(null!==(d=b.eventData.id)&&void 0!==d?d:0);if(!c.has(e)){a.dispatchEvent({action:b.eventName,state:a.state,element:b.eventData},a.target);c.add(e)}});this.dispatchEvent({action:"transaction:end",state:this.state,element:null},this.target)}}]);return a}();a.default=x;var y=function(){function a(b,c,d){u(this,a);this.name=b;this.stateManager=c;this.proxyValues=null!==d&&void 0!==d?d:!1}w(a,[{key:"set",value:function set(b,c,d,e){if(this.stateManager.readonly){throw new Error("State locked. Use mutations to change ".concat(c," value in ").concat(this.name,"."))}if(JSON.stringify(b[c])===JSON.stringify(d)){return!0}var f=b[c]!==void 0?"updated":"created";if(this.proxyValues){if(Array.isArray(d)){b[c]=new z(c,this.stateManager).loadValues(d)}else{b[c]=new Proxy(d,new a(c,this.stateManager))}}else{b[c]=d}if(this.stateManager.state===void 0){return!0}this.stateManager.registerStateAction(this.name,c,f,e);return!0}},{key:"deleteProperty",value:function deleteProperty(a,b){if(this.stateManager.readonly){throw new Error("State locked. Use mutations to delete ".concat(b," in ").concat(this.name,"."))}if(b in a){delete a[b];this.stateManager.registerStateAction(this.name,b,"deleted",a)}return!0}}]);return a}(),z=function(a){e(d,a);var g=f(d);function d(a,b,c){var e;u(this,d);e=g.call(this,c);e.name=a;e.stateManager=b;return e}w(d,[{key:"set",value:function set(a,b){if(this.stateManager.readonly){throw new Error("State locked. Use mutations to change ".concat(a," value in ").concat(this.name,"."))}a=this.normalizeKey(a);this.checkValue(b);if(a===void 0||null===a){throw Error("State lists keys cannot be null or undefined")}if(this.normalizeKey(b.id)!==a){throw new Error("State error: ".concat(this.name," list element ID (").concat(b.id,") and key (").concat(a,") mismatch"))}var e=c(n(d.prototype),"has",this).call(this,a)?"updated":"created",f=c(n(d.prototype),"set",this).call(this,a,new Proxy(b,new y(this.name,this.stateManager)));if(this.stateManager.state===void 0){return f}this.stateManager.registerStateAction(this.name,null,e,c(n(d.prototype),"get",this).call(this,a));return f}},{key:"checkValue",value:function checkValue(a){if("object"===!b(a)&&null!==a){throw Error("State lists can contain objects only")}if(a.id===void 0){throw Error("State lists elements must contain at least an id attribute")}}},{key:"normalizeKey",value:function normalizeKey(a){return(a+"").valueOf()}},{key:"add",value:function add(a){this.checkValue(a);return this.set(a.id,a)}},{key:"get",value:function get(a){return c(n(d.prototype),"get",this).call(this,this.normalizeKey(a))}},{key:"has",value:function has(a){return c(n(d.prototype),"has",this).call(this,this.normalizeKey(a))}},{key:"delete",value:function _delete(a){a=this.normalizeKey(a);if(this.stateManager.readonly){throw new Error("State locked. Use mutations to change ".concat(a," value in ").concat(this.name,"."))}var b=c(n(d.prototype),"get",this).call(this,a),e=c(n(d.prototype),"delete",this).call(this,a);if(!e){return e}this.stateManager.registerStateAction(this.name,null,"deleted",b);return e}},{key:"toJSON",value:function toJSON(){var a=[];this.forEach(function(b){a.push(b)});return a}},{key:"loadValues",value:function loadValues(a){var b=this;a.forEach(function(a){b.checkValue(a);var c=a.id,d=new Proxy(a,new y(b.name,b.stateManager));b.set(c,d)});return this}}]);return d}(i(Map));return a.default});
 //# sourceMappingURL=statemanager.min.js.map
diff --git a/lib/amd/build/local/reactive/statemanager.min.js.map b/lib/amd/build/local/reactive/statemanager.min.js.map
index a045bce3539..cff93ef0167 100644
--- a/lib/amd/build/local/reactive/statemanager.min.js.map
+++ b/lib/amd/build/local/reactive/statemanager.min.js.map
@@ -1 +1 @@
-{"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","_publishEvents","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","element","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 <http://www.gnu.org/licenses/>.\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 <ferran@moodle.com>\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":"gxHAuEqBA,CAAAA,C,YAYjB,WAAYC,CAAZ,CAA2BC,CAA3B,CAAmC,sBAI/B,KAAKD,aAAL,CAAqBA,CAArB,CAIA,KAAKC,MAAL,QAAcA,CAAd,WAAcA,CAAd,CAAcA,CAAd,CAAwBC,QAAxB,CAIA,KAAKC,QAAL,IAIA,KAAKC,eAAL,CAAuB,EAAvB,CAIA,KAAKC,WAAL,CAAmB,CACf,OAAU,KAAKC,aAAL,CAAmBC,IAAnB,CAAwB,IAAxB,CADK,CAEf,OAAU,KAAKC,aAAL,CAAmBD,IAAnB,CAAwB,IAAxB,CAFK,CAGf,OAAU,KAAKE,aAAL,CAAmBF,IAAnB,CAAwB,IAAxB,CAHK,CAIf,IAAO,KAAKG,UAAL,CAAgBH,IAAhB,CAAqB,IAArB,CAJQ,CAKf,SAAY,KAAKI,eAAL,CAAqBJ,IAArB,CAA0B,IAA1B,CALG,CAMf,cAAiB,KAAKK,oBAAL,CAA0BL,IAA1B,CAA+B,IAA/B,CANF,CAAnB,CAYA,KAAKM,cAAL,CAAsB,GAAIC,CAAAA,OAAJ,CAAY,SAACC,CAAD,CAAa,CAI3C,CAAI,CAACd,MAAL,CAAYe,gBAAZ,CAA6B,cAA7B,CAHyB,QAAnBC,CAAAA,gBAAmB,CAACC,CAAD,CAAW,CAChCH,CAAO,CAACG,CAAK,CAACC,MAAN,CAAaC,KAAd,CACV,CACD,CACH,CALqB,CAMzB,C,2DAWeC,C,CAAc,CAE1B,GAAI,KAAKD,KAAL,SAAJ,CAA8B,CAC1B,KAAME,CAAAA,KAAK,CAAC,4CAAD,CACd,CAID,OADMF,CAAAA,CAAK,CAAG,GAAIG,CAAAA,KAAJ,CAAU,EAAV,CAAc,GAAIC,CAAAA,CAAJ,CAAY,OAAZ,CAAqB,IAArB,IAAd,CACd,OAAgCC,MAAM,CAACC,OAAP,CAAeL,CAAf,CAAhC,gBAA8D,iBAAlDM,CAAkD,MAA5CC,CAA4C,MAC1DR,CAAK,CAACO,CAAD,CAAL,CAAcC,CACjB,CACD,KAAKR,KAAL,CAAaA,CAAb,CAGA,KAAKjB,QAAL,IAEA,KAAKH,aAAL,CAAmB,CACf6B,MAAM,CAAE,cADO,CAEfT,KAAK,CAAE,KAAKA,KAFG,CAAnB,CAGG,KAAKnB,MAHR,CAIH,C,6DAUmB,CAChB,MAAO,MAAKY,cACf,C,gDAcWV,C,CAAU,CAElB,KAAKA,QAAL,CAAgBA,CAAhB,CAEA,GAAI2B,CAAAA,CAAI,CAAG,KAAX,CAGA,GAAI,KAAK3B,QAAT,CAAmB,CACf2B,CAAI,CAAG,IAAP,CACA,KAAKC,cAAL,EACH,CAGD,KAAK/B,aAAL,CAAmB,CACf6B,MAAM,oBAAcC,CAAd,CADS,CAEfV,KAAK,CAAE,KAAKA,KAFG,CAGfY,OAAO,CAAE,IAHM,CAAnB,CAIG,KAAK/B,MAJR,CAKH,C,sDAWcgC,C,CAAc,CACzB,cAA2CR,MAAM,CAACC,OAAP,CAAeO,CAAf,CAA3C,gBAAyE,iBAA7DC,CAA6D,MAAjDC,CAAiD,MACrE,GAA8B,UAA1B,QAAOA,CAAAA,CAAX,CAA0C,CACtC,KAAK9B,WAAL,CAAiB6B,CAAjB,EAA+BC,CAAc,CAAC5B,IAAf,CAAoB0B,CAApB,CAClC,CACJ,CACJ,C,sDAWcG,C,CAAS/B,C,CAAa,YACjC,GAAI,CAACgC,KAAK,CAACC,OAAN,CAAcF,CAAd,CAAL,CAA6B,CACzB,KAAMd,CAAAA,KAAK,CAAC,gCAAD,CACd,CACD,KAAKiB,WAAL,KACAH,CAAO,CAACI,OAAR,CAAgB,SAACC,CAAD,CAAY,CACxB,GAAIA,CAAM,CAACC,IAAP,SAAJ,CAA+B,CAC3B,KAAMpB,CAAAA,KAAK,CAAC,2BAAD,CACd,CACD,CAAI,CAACqB,aAAL,CACIF,CAAM,CAACC,IADX,CAEID,CAAM,CAACZ,MAFX,CAGIY,CAAM,CAACG,MAHX,CAIIvC,CAJJ,CAMH,CAVD,EAWA,KAAKkC,WAAL,IACH,C,oDAYaM,C,CAAYhB,C,CAAQe,C,CAAQvC,C,CAAa,WAEnD,GAAI,CAACuC,CAAL,CAAa,CACT,KAAMtB,CAAAA,KAAK,CAAC,6BAAD,CACd,CAED,GAAIjB,CAAW,SAAf,CAA+B,CAC3BA,CAAW,CAAG,EACjB,CAEDwB,CAAM,WAAGA,CAAH,gBAAa,QAAnB,CAEA,GAAMiB,CAAAA,CAAM,WAAGzC,CAAW,CAACwB,CAAD,CAAd,gBAA0B,KAAKxB,WAAL,CAAiBwB,CAAjB,CAAtC,CAEA,GAAIiB,CAAM,SAAV,CAA0B,CACtB,KAAMxB,CAAAA,KAAK,gCAAyBO,CAAzB,EACd,CAKD,GAAMkB,CAAAA,CAAa,WAAG1C,CAAW,CAAC0C,aAAf,gBAAgC,KAAK1C,WAAL,CAAiB0C,aAApE,CAEAD,CAAM,CAAC,IAAD,CAAOD,CAAP,CAAmBE,CAAa,CAAC,IAAD,CAAOF,CAAP,CAAmBD,CAAnB,CAAhC,CACT,C,kEAYoBI,C,CAAcH,C,CAAYD,C,CAAQ,CACnD,MAAOA,CAAAA,CACV,C,oDAUaI,C,CAAcH,C,CAAYD,C,CAAQ,CAE5C,GAAIxB,CAAAA,CAAK,CAAG4B,CAAY,CAAC5B,KAAzB,CAGA,GAAIA,CAAK,CAACyB,CAAD,CAAL,UAA6BI,CAAAA,CAAjC,CAA2C,CACvC7B,CAAK,CAACyB,CAAD,CAAL,CAAkBK,GAAlB,CAAsBN,CAAtB,EACA,MACH,CACDxB,CAAK,CAACyB,CAAD,CAAL,CAAoBD,CACvB,C,oDASaI,C,CAAcH,C,CAAYD,C,CAAQ,CAG5C,GAAIO,CAAAA,CAAO,CAAGH,CAAY,CAACI,GAAb,CAAiBP,CAAjB,CAA6BD,CAAM,CAACS,EAApC,CAAd,CACA,GAAI,CAACF,CAAL,CAAc,CACV,KAAM7B,CAAAA,KAAK,sBAAeuB,CAAf,aAA6BD,CAAM,CAACS,EAApC,EACd,CAGD,GAAIjC,CAAAA,CAAK,CAAG4B,CAAY,CAAC5B,KAAzB,CAEA,GAAIA,CAAK,CAACyB,CAAD,CAAL,UAA6BI,CAAAA,CAAjC,CAA2C,CACvC7B,CAAK,CAACyB,CAAD,CAAL,CAAkBS,MAAlB,CAAyBV,CAAM,CAACS,EAAhC,EACA,MACH,CACD,MAAOjC,CAAAA,CAAK,CAACyB,CAAD,CACf,C,oDASaG,C,CAAcH,C,CAAYD,C,CAAQ,CAG5C,GAAIO,CAAAA,CAAO,CAAGH,CAAY,CAACI,GAAb,CAAiBP,CAAjB,CAA6BD,CAAM,CAACS,EAApC,CAAd,CACA,GAAI,CAACF,CAAL,CAAc,CACV,KAAM7B,CAAAA,KAAK,sBAAeuB,CAAf,aAA6BD,CAAM,CAACS,EAApC,EACd,CAGD,cAAsC5B,MAAM,CAACC,OAAP,CAAekB,CAAf,CAAtC,gBAA8D,iBAAlDW,CAAkD,MAAvCC,CAAuC,MAC1DL,CAAO,CAACI,CAAD,CAAP,CAAqBC,CACxB,CACJ,C,8CASUR,C,CAAcH,C,CAAYD,C,CAAQ,CAGzC,GAAIO,CAAAA,CAAO,CAAGH,CAAY,CAACI,GAAb,CAAiBP,CAAjB,CAA6BD,CAAM,CAACS,EAApC,CAAd,CACA,GAAIF,CAAJ,CAAa,CAET,cAAsC1B,MAAM,CAACC,OAAP,CAAekB,CAAf,CAAtC,gBAA8D,iBAAlDW,CAAkD,MAAvCC,CAAuC,MAC1DL,CAAO,CAACI,CAAD,CAAP,CAAqBC,CACxB,CACJ,CALD,IAKO,CAEH,GAAIpC,CAAAA,CAAK,CAAG4B,CAAY,CAAC5B,KAAzB,CACA,GAAIA,CAAK,CAACyB,CAAD,CAAL,UAA6BI,CAAAA,CAAjC,CAA2C,CACvC7B,CAAK,CAACyB,CAAD,CAAL,CAAkBK,GAAlB,CAAsBN,CAAtB,EACA,MACH,CACDxB,CAAK,CAACyB,CAAD,CAAL,CAAoBD,CACvB,CACJ,C,wDASeI,C,CAAcH,C,CAAYD,C,CAAQ,CAG9C,GAAIO,CAAAA,CAAO,CAAGH,CAAY,CAACI,GAAb,CAAiBP,CAAjB,CAA6BD,CAAM,CAACS,EAApC,CAAd,CACA,GAAIF,CAAJ,CAAa,CAET,cAA0B1B,MAAM,CAACC,OAAP,CAAeyB,CAAf,CAA1B,gBAAmD,iBAAvCI,CAAuC,MAC/C,GAAIX,CAAM,CAACW,CAAD,CAAN,SAAJ,CAAqC,CACjC,MAAOJ,CAAAA,CAAO,CAACI,CAAD,CACjB,CACJ,CAED,cAAsC9B,MAAM,CAACC,OAAP,CAAekB,CAAf,CAAtC,gBAA8D,iBAAlDW,CAAkD,MAAvCC,CAAuC,MAC1DL,CAAO,CAACI,CAAD,CAAP,CAAqBC,CACxB,CACJ,CAXD,IAWO,CAEH,GAAIpC,CAAAA,CAAK,CAAG4B,CAAY,CAAC5B,KAAzB,CACA,GAAIA,CAAK,CAACyB,CAAD,CAAL,UAA6BI,CAAAA,CAAjC,CAA2C,CACvC7B,CAAK,CAACyB,CAAD,CAAL,CAAkBK,GAAlB,CAAsBN,CAAtB,EACA,MACH,CACDxB,CAAK,CAACyB,CAAD,CAAL,CAAoBD,CACvB,CACJ,C,gCAYGF,C,CAAMW,C,CAAI,IACJjC,CAAAA,CAAK,CAAG,KAAKA,KADT,CAGN+B,CAAO,CAAG/B,CAAK,CAACsB,CAAD,CAHT,CAIV,GAAIS,CAAO,WAAYF,CAAAA,CAAvB,CAAiC,CAC7B,GAAII,CAAE,SAAN,CAAsB,CAClB,KAAM/B,CAAAA,KAAK,0BAAmBoB,CAAnB,kBACd,CACDS,CAAO,CAAG/B,CAAK,CAACsB,CAAD,CAAL,CAAYU,GAAZ,CAAgBC,CAAhB,CACb,CAED,MAAOF,CAAAA,CACV,C,gEAqBmBM,C,CAAO9B,C,CAAME,C,CAAQ6B,C,CAAM,CAE3C,GAAIC,CAAAA,CAAY,CAAG,SAAnB,CAEA,GAAa,IAAT,GAAAhC,CAAJ,CAAmB,CACf,KAAKvB,eAAL,CAAqBwD,IAArB,CAA0B,CACtBC,SAAS,WAAKJ,CAAL,aAAc9B,CAAd,aAAsBE,CAAtB,CADa,CAEtBiC,SAAS,CAAEJ,CAFW,CAGtB7B,MAAM,CAANA,CAHsB,CAA1B,CAKH,CAND,IAMO,CACH8B,CAAY,CAAG9B,CAClB,CAGD,GAAI6B,CAAI,CAACL,EAAL,SAAJ,CAA2B,CACvB,GAAa,IAAT,GAAA1B,CAAJ,CAAmB,CACf,KAAKvB,eAAL,CAAqBwD,IAArB,CAA0B,CACtBC,SAAS,WAAKJ,CAAL,aAAcC,CAAI,CAACL,EAAnB,cAA0B1B,CAA1B,aAAkCE,CAAlC,CADa,CAEtBiC,SAAS,CAAEJ,CAFW,CAGtB7B,MAAM,CAANA,CAHsB,CAA1B,CAKH,CACD,KAAKzB,eAAL,CAAqBwD,IAArB,CAA0B,CACtBC,SAAS,WAAKJ,CAAL,aAAcC,CAAI,CAACL,EAAnB,cAA0BM,CAA1B,CADa,CAEtBG,SAAS,CAAEJ,CAFW,CAGtB7B,MAAM,CAAE8B,CAHc,CAA1B,CAKH,CAGD,KAAKvD,eAAL,CAAqBwD,IAArB,CAA0B,CACtBC,SAAS,WAAKJ,CAAL,aAAcE,CAAd,CADa,CAEtBG,SAAS,CAAEJ,CAFW,CAGtB7B,MAAM,CAAE8B,CAHc,CAA1B,EAOA,KAAKvD,eAAL,CAAqBwD,IAArB,CAA0B,CACtBC,SAAS,gBADa,CAEtBC,SAAS,CAAEJ,CAFW,CAGtB7B,MAAM,CAAE,SAHc,CAA1B,CAKH,C,uDAOgB,YACPkC,CAAY,CAAG,KAAK3D,eADb,CAEb,KAAKA,eAAL,CAAuB,EAAvB,CAGA,KAAKJ,aAAL,CAAmB,CACf6B,MAAM,CAAE,mBADO,CAEfT,KAAK,CAAE,KAAKA,KAFG,CAGfY,OAAO,CAAE,IAHM,CAIfgC,OAAO,CAAED,CAJM,CAAnB,CAKG,KAAK9D,MALR,EAUA8D,CAAY,CAACE,IAAb,CAAkB,SAACC,CAAD,CAAIC,CAAJ,CAAU,SAClBC,CAAO,CAAG,CACZC,OAAO,CAAE,CADG,CAEZC,OAAO,CAAE,CAFG,CAGZC,OAAO,CAAE,CAHG,CADQ,CAMlBC,CAAO,WAAGJ,CAAO,CAACF,CAAC,CAACrC,MAAH,CAAV,gBAAwB,CANb,CAOlB4C,CAAO,WAAGL,CAAO,CAACD,CAAC,CAACtC,MAAH,CAAV,gBAAwB,CAPb,CASxB,GAAI2C,CAAO,GAAKC,CAAhB,CAAyB,CACrB,MAAOP,CAAAA,CAAC,CAACL,SAAF,CAAYa,MAAZ,CAAqBP,CAAC,CAACN,SAAF,CAAYa,MAC3C,CACD,MAAOF,CAAAA,CAAO,CAAGC,CACpB,CAbD,EAgBA,GAAIE,CAAAA,CAAe,CAAG,GAAIC,CAAAA,GAA1B,CAEAb,CAAY,CAACvB,OAAb,CAAqB,SAACtB,CAAD,CAAW,OAEtB2D,CAAQ,WAAM3D,CAAK,CAAC2C,SAAZ,uBAAyB3C,CAAK,CAAC4C,SAAN,CAAgBT,EAAzC,gBAA+C,CAA/C,CAFc,CAI5B,GAAI,CAACsB,CAAe,CAACG,GAAhB,CAAoBD,CAApB,CAAL,CAAoC,CAChC,CAAI,CAAC7E,aAAL,CAAmB,CACf6B,MAAM,CAAEX,CAAK,CAAC2C,SADC,CAEfzC,KAAK,CAAE,CAAI,CAACA,KAFG,CAGfY,OAAO,CAAEd,CAAK,CAAC4C,SAHA,CAAnB,CAIG,CAAI,CAAC7D,MAJR,EAMA0E,CAAe,CAACzB,GAAhB,CAAoB2B,CAApB,CACH,CACJ,CAbD,EAgBA,KAAK7E,aAAL,CAAmB,CACf6B,MAAM,CAAE,iBADO,CAEfT,KAAK,CAAE,KAAKA,KAFG,CAGfY,OAAO,CAAE,IAHM,CAAnB,CAIG,KAAK/B,MAJR,CAKH,C,+BAaCuB,CAAAA,C,YASF,WAAYkB,CAAZ,CAAkBM,CAAlB,CAAgC+B,CAAhC,CAA6C,WACzC,KAAKrC,IAAL,CAAYA,CAAZ,CACA,KAAKM,YAAL,CAAoBA,CAApB,CACA,KAAK+B,WAAL,QAAmBA,CAAnB,WAAmBA,CAAnB,CAAmBA,CAAnB,GACH,C,mCAWGC,C,CAAKrD,C,CAAMsD,C,CAAOC,C,CAAU,CAG5B,GAAI,KAAKlC,YAAL,CAAkB7C,QAAtB,CAAgC,CAC5B,KAAM,IAAImB,CAAAA,KAAJ,iDAAmDK,CAAnD,sBAAoE,KAAKe,IAAzE,MACT,CAGD,GAAIyC,IAAI,CAACC,SAAL,CAAeJ,CAAG,CAACrD,CAAD,CAAlB,IAA8BwD,IAAI,CAACC,SAAL,CAAeH,CAAf,CAAlC,CAAyD,CACrD,QACH,CAED,GAAMpD,CAAAA,CAAM,CAAImD,CAAG,CAACrD,CAAD,CAAH,SAAD,CAA4B,SAA5B,CAAwC,SAAvD,CAGA,GAAI,KAAKoD,WAAT,CAAsB,CAClB,GAAI1C,KAAK,CAACC,OAAN,CAAc2C,CAAd,CAAJ,CAA0B,CACtBD,CAAG,CAACrD,CAAD,CAAH,CAAY,GAAIsB,CAAAA,CAAJ,CAAatB,CAAb,CAAmB,KAAKqB,YAAxB,EAAsCqC,UAAtC,CAAiDJ,CAAjD,CACf,CAFD,IAEO,CACHD,CAAG,CAACrD,CAAD,CAAH,CAAY,GAAIJ,CAAAA,KAAJ,CAAU0D,CAAV,CAAiB,GAAIzD,CAAAA,CAAJ,CAAYG,CAAZ,CAAkB,KAAKqB,YAAvB,CAAjB,CACf,CACJ,CAND,IAMO,CACHgC,CAAG,CAACrD,CAAD,CAAH,CAAYsD,CACf,CAGD,GAAI,KAAKjC,YAAL,CAAkB5B,KAAlB,SAAJ,CAA2C,CACvC,QACH,CAED,KAAK4B,YAAL,CAAkBsC,mBAAlB,CAAsC,KAAK5C,IAA3C,CAAiDf,CAAjD,CAAuDE,CAAvD,CAA+DqD,CAA/D,EAEA,QACH,C,sDAScF,C,CAAKrD,C,CAAM,CAEtB,GAAI,KAAKqB,YAAL,CAAkB7C,QAAtB,CAAgC,CAC5B,KAAM,IAAImB,CAAAA,KAAJ,iDAAmDK,CAAnD,gBAA8D,KAAKe,IAAnE,MACT,CACD,GAAIf,CAAI,GAAIqD,CAAAA,CAAZ,CAAiB,CAEb,MAAOA,CAAAA,CAAG,CAACrD,CAAD,CAAV,CAEA,KAAKqB,YAAL,CAAkBsC,mBAAlB,CAAsC,KAAK5C,IAA3C,CAAiDf,CAAjD,CAAuD,SAAvD,CAAkEqD,CAAlE,CACH,CACD,QACH,C,gBAeC/B,C,+BASF,WAAYP,CAAZ,CAAkBM,CAAlB,CAAgCuC,CAAhC,CAA0C,iBAEtC,cAAMA,CAAN,EACA,EAAK7C,IAAL,CAAYA,CAAZ,CACA,EAAKM,YAAL,CAAoBA,CAApB,CAJsC,QAKzC,C,mCAYGwC,C,CAAKP,C,CAAO,CAGZ,GAAI,KAAKjC,YAAL,CAAkB7C,QAAtB,CAAgC,CAC5B,KAAM,IAAImB,CAAAA,KAAJ,iDAAmDkE,CAAnD,sBAAmE,KAAK9C,IAAxE,MACT,CAGD8C,CAAG,CAAG,KAAKC,YAAL,CAAkBD,CAAlB,CAAN,CAEA,KAAKE,UAAL,CAAgBT,CAAhB,EAEA,GAAIO,CAAG,SAAH,EAA6B,IAAR,GAAAA,CAAzB,CAAuC,CACnC,KAAMlE,CAAAA,KAAK,CAAC,8CAAD,CACd,CAGD,GAAI,KAAKmE,YAAL,CAAkBR,CAAK,CAAC5B,EAAxB,IAAgCmC,CAApC,CAAyC,CACrC,KAAM,IAAIlE,CAAAA,KAAJ,wBAA0B,KAAKoB,IAA/B,8BAAwDuC,CAAK,CAAC5B,EAA9D,uBAA8EmC,CAA9E,eACT,CAnBW,GAqBN3D,CAAAA,CAAM,CAAG,uCAAW2D,CAAX,EAAmB,SAAnB,CAA+B,SArBlC,CAwBNG,CAAM,wCAAaH,CAAb,CAAkB,GAAIjE,CAAAA,KAAJ,CAAU0D,CAAV,CAAiB,GAAIzD,CAAAA,CAAJ,CAAY,KAAKkB,IAAjB,CAAuB,KAAKM,YAA5B,CAAjB,CAAlB,CAxBA,CA2BZ,GAAI,KAAKA,YAAL,CAAkB5B,KAAlB,SAAJ,CAA2C,CACvC,MAAOuE,CAAAA,CACV,CAED,KAAK3C,YAAL,CAAkBsC,mBAAlB,CAAsC,KAAK5C,IAA3C,CAAiD,IAAjD,CAAuDb,CAAvD,wCAAyE2D,CAAzE,GAEA,MAAOG,CAAAA,CACV,C,8CAWUV,C,CAAO,CACd,GAAsB,QAAlB,MAAQA,CAAR,GAAwC,IAAV,GAAAA,CAAlC,CAAkD,CAC9C,KAAM3D,CAAAA,KAAK,CAAC,sCAAD,CACd,CAED,GAAI2D,CAAK,CAAC5B,EAAN,SAAJ,CAA4B,CACxB,KAAM/B,CAAAA,KAAK,CAAC,4DAAD,CACd,CACJ,C,kDAWYkE,C,CAAK,CACd,MAAO,CAAOA,CAAP,KAAYI,OAAZ,EACV,C,gCAUGX,C,CAAO,CACP,KAAKS,UAAL,CAAgBT,CAAhB,EACA,MAAO,MAAKY,GAAL,CAASZ,CAAK,CAAC5B,EAAf,CAAmB4B,CAAnB,CACV,C,gCAQGO,C,CAAK,CACL,8CAAiB,KAAKC,YAAL,CAAkBD,CAAlB,CAAjB,CACH,C,gCAQGA,C,CAAK,CACL,8CAAiB,KAAKC,YAAL,CAAkBD,CAAlB,CAAjB,CACH,C,uCAQMA,C,CAAK,CAERA,CAAG,CAAG,KAAKC,YAAL,CAAkBD,CAAlB,CAAN,CAGA,GAAI,KAAKxC,YAAL,CAAkB7C,QAAtB,CAAgC,CAC5B,KAAM,IAAImB,CAAAA,KAAJ,iDAAmDkE,CAAnD,sBAAmE,KAAK9C,IAAxE,MACT,CAPO,GASFoD,CAAAA,CAAQ,wCAAaN,CAAb,CATN,CAWFG,CAAM,2CAAgBH,CAAhB,CAXJ,CAYR,GAAI,CAACG,CAAL,CAAa,CACT,MAAOA,CAAAA,CACV,CAED,KAAK3C,YAAL,CAAkBsC,mBAAlB,CAAsC,KAAK5C,IAA3C,CAAiD,IAAjD,CAAuD,SAAvD,CAAkEoD,CAAlE,EAEA,MAAOH,CAAAA,CACV,C,uCAWQ,CACL,GAAIA,CAAAA,CAAM,CAAG,EAAb,CACA,KAAKnD,OAAL,CAAa,SAACyC,CAAD,CAAW,CACpBU,CAAM,CAAC/B,IAAP,CAAYqB,CAAZ,CACH,CAFD,EAGA,MAAOU,CAAAA,CACV,C,8CAWUI,C,CAAQ,YACfA,CAAM,CAACvD,OAAP,CAAe,SAACkB,CAAD,CAAU,CACrB,CAAI,CAACgC,UAAL,CAAgBhC,CAAhB,EADqB,GAEjB8B,CAAAA,CAAG,CAAG9B,CAAI,CAACL,EAFM,CAGjB2C,CAAQ,CAAG,GAAIzE,CAAAA,KAAJ,CAAUmC,CAAV,CAAgB,GAAIlC,CAAAA,CAAJ,CAAY,CAAI,CAACkB,IAAjB,CAAuB,CAAI,CAACM,YAA5B,CAAhB,CAHM,CAIrB,CAAI,CAAC6C,GAAL,CAASL,CAAT,CAAcQ,CAAd,CACH,CALD,EAMA,MAAO,KACV,C,gBA5LkBC,G","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 <http://www.gnu.org/licenses/>.\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 <ferran@moodle.com>\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 <http://www.gnu.org/licenses/>.\n\n/**\n * Generic reactive module used in the course editor.\n *\n * @module     core/reactive\n * @copyright  2021 Ferran Recio <ferran@moodle.com>\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 <http://www.gnu.org/licenses/>.\n\n/**\n * Generic reactive module used in the course editor.\n *\n * @module     core/reactive\n * @copyright  2021 Ferran Recio <ferran@moodle.com>\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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Reactive module debug tools.
+ *
+ * @module     core/reactive/local/reactive/debug
+ * @copyright  2021 Ferran Recio <ferran@moodle.com>
+ * @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 <ferran@moodle.com>
+ * @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 <ferran@moodle.com>
+ * @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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <ferran@moodle.com>
+ * @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()) . '</div>';
         }
         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 .= '<div class="purgecaches">' .
                     html_writer::link($purgeurl, get_string('purgecaches', 'admin')) . '</div>';
+
+            // 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 <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @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"
+    }
+}}
+<div id="{{uniqid}}-reactive-debugpanel-instance" data-id="{{id}}" class="card text-dark">
+    <div class="card-body">
+        <h5 class="card-title" data-for="name">
+            {{id}}
+            <button href="#" class="btn btn-secondary" data-for="pin" data-alt="{{#str}} reactive_pin , core_debug {{/str}}">
+                {{#str}} reactive_unpin , core_debug {{/str}}
+            </button>
+            <button href="#" class="btn btn-danger float-right" data-for="close">
+                {{#str}} close , admin {{/str}}
+            </button>
+        </h5>
+        <p class="card-text">
+            <button
+                class="btn btn-secondary"
+                data-for="highlight"
+                data-alt="{{#str}} reactive_highlighton , core_debug {{/str}}"
+            >
+                {{#str}} reactive_highlightoff , core_debug {{/str}}
+            </button>
+            <button
+                class="btn btn-secondary"
+                data-for="readmode"
+                data-alt="{{#str}} reactive_readmodeoff , core_debug {{/str}}"
+            >
+                {{#str}} reactive_readmodeon , core_debug {{/str}}
+            </button>
+            <button
+                class="btn btn-secondary"
+                data-for="clean"
+            >
+                {{#str}} reactive_resetpanel , core_debug {{/str}}
+            </button>
+        </p>
+        <div>
+            <h6>
+                {{#str}} reactive_statedata , core_debug {{/str}}
+                <button class="btn btn-secondary" data-for="save" disabled>
+                    {{#str}} save , admin {{/str}}
+                </button>
+                <span data-for="invalid">{{#str}} reactive_saveingwarning , core_debug {{/str}}</span>
+            </h6>
+            <textarea class="w-100" style="resize:vertical; height: 20em;" data-for="state" draggable="false"></textarea>
+        </div>
+        <div>
+            <h6>State events log</h6>
+            <textarea class="w-100" style="resize:vertical; height: 20em;" data-for="log" draggable="false"></textarea>
+        </div>
+  </div>
+</div>
+{{#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 <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @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):
+    {
+    }
+}}
+<div id="{{uniqid}}-reactive-debugpanel" class="py-1">
+    <div>
+        {{#str}} reactive_instances , core_debug {{/str}}
+        <span data-for="loaders">{{#str}} reactive_noinstances , core_debug {{/str}}</span>
+    </div>
+    <div data-for="subpanel">
+        {{> core/loading }}
+    </div>
+</div>
+{{#js}}
+require(['core/local/reactive/debugpanel'], function(component) {
+    component.init('{{uniqid}}-reactive-debugpanel');
+});
+{{/js}}