From 8dd0ea96e534c3e12a30125741e96f2736df6fa2 Mon Sep 17 00:00:00 2001 From: Frederik Milling Pytlick Date: Thu, 7 Nov 2024 12:35:21 +0100 Subject: [PATCH] MDL-83658 core: Fix reactive debug tool logging Fixed issue where the reactive debug panel does not log a transaction, if the events fired are exactly the same as the previous ones. --- lib/amd/build/local/reactive/debug.min.js | 2 +- lib/amd/build/local/reactive/debug.min.js.map | 2 +- lib/amd/build/local/reactive/debugpanel.min.js | 2 +- lib/amd/build/local/reactive/debugpanel.min.js.map | 2 +- lib/amd/build/local/reactive/statemanager.min.js | 2 +- lib/amd/build/local/reactive/statemanager.min.js.map | 2 +- lib/amd/src/local/reactive/debug.js | 8 ++++++-- lib/amd/src/local/reactive/debugpanel.js | 12 +++++++++--- lib/amd/src/local/reactive/statemanager.js | 3 +++ 9 files changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/amd/build/local/reactive/debug.min.js b/lib/amd/build/local/reactive/debug.min.js index 3539ba5426a..c1cb744e9df 100644 --- a/lib/amd/build/local/reactive/debug.min.js +++ b/lib/amd/build/local/reactive/debug.min.js @@ -20,7 +20,7 @@ define("core/local/reactive/debug",["exports","core/local/reactive/reactive","co * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class Debug extends _reactive.default{setInitialState(stateData){super.setInitialState(stateData),_log.default.debug('Debug module "M.reactive" loaded.')}get list(){return JSON.parse(JSON.stringify(this.state.reactives))}registerNewInstance(instance){var _instance$name;let name=null!==(_instance$name=instance.name)&&void 0!==_instance$name?_instance$name:"instance".concat(this.state.reactives.length);name=name.replace(/\W/g,""),_log.default.debug('Registering new reactive instance "M.reactive.'.concat(name,'"')),reactiveInstances[name]=instance,reactiveDebuggers[name]=new DebugInstance(reactiveInstances[name]),this.dispatch("putInstance",name,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);instance.target.addEventListener("transaction:start",(_ref=>{let{detail:detail}=_ref;const changes=null==detail?void 0:detail.changes;this.dispatch("lastTransaction",name,changes)}))}debug(name){return reactiveDebuggers[name]}}class Mutations{putInstance(stateManager,name,instance){const state=stateManager.state;stateManager.setReadOnly(!1),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()):state.reactives.add({id:name,countcomponents:instance.components.length,readOnly:instance.stateManager.readonly,lastChanges:[],modified:(new Date).getTime()}),stateManager.setReadOnly(!0)}lastTransaction(stateManager,name,changes){if(!changes||0===changes.length)return;const state=stateManager.state,lastChanges=["transaction:start"];changes.forEach((change=>{lastChanges.push(change.eventName)})),lastChanges.push("transaction:end"),stateManager.setReadOnly(!1),state.reactives.get(name).lastChanges=lastChanges,stateManager.setReadOnly(!0)}} +class Debug extends _reactive.default{setInitialState(stateData){super.setInitialState(stateData),_log.default.debug('Debug module "M.reactive" loaded.')}get list(){return JSON.parse(JSON.stringify(this.state.reactives))}registerNewInstance(instance){var _instance$name;let name=null!==(_instance$name=instance.name)&&void 0!==_instance$name?_instance$name:"instance".concat(this.state.reactives.length);name=name.replace(/\W/g,""),_log.default.debug('Registering new reactive instance "M.reactive.'.concat(name,'"')),reactiveInstances[name]=instance,reactiveDebuggers[name]=new DebugInstance(reactiveInstances[name]),this.dispatch("putInstance",name,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);instance.target.addEventListener("transaction:end",(_ref=>{let{detail:detail}=_ref;const changes=null==detail?void 0:detail.changes;this.dispatch("lastTransaction",name,changes)}))}debug(name){return reactiveDebuggers[name]}}class Mutations{putInstance(stateManager,name,instance){const state=stateManager.state;stateManager.setReadOnly(!1),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()):state.reactives.add({id:name,countcomponents:instance.components.length,readOnly:instance.stateManager.readonly,lastChanges:[],modified:(new Date).getTime()}),stateManager.setReadOnly(!0)}lastTransaction(stateManager,name,changes){if(!changes||0===changes.length)return;const state=stateManager.state,lastChanges=["transaction:start"];changes.forEach((change=>{lastChanges.push(change.eventName)})),lastChanges.push("transaction:end"),stateManager.setReadOnly(!1),state.reactives.get(name).lastChanges=[],state.reactives.get(name).lastChanges=lastChanges,stateManager.setReadOnly(!0)}} /** * Class used to debug a specific instance and manipulate the state from the JS console. * diff --git a/lib/amd/build/local/reactive/debug.min.js.map b/lib/amd/build/local/reactive/debug.min.js.map index 033b13f4336..2a6f362f1f6 100644 --- a/lib/amd/build/local/reactive/debug.min.js.map +++ b/lib/amd/build/local/reactive/debug.min.js.map @@ -1 +1 @@ -{"version":3,"file":"debug.min.js","sources":["../../../src/local/reactive/debug.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive module debug tools.\n *\n * @module core/local/reactive/debug\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Reactive from 'core/local/reactive/reactive';\nimport log from 'core/log';\n\n// The list of reactives instances.\nconst reactiveInstances = {};\n\n// The reactive debugging objects.\nconst reactiveDebuggers = {};\n\n/**\n * Reactive module debug tools.\n *\n * If debug is enabled, this reactive module will spy all the reactive instances and keep a record\n * of the changes and components they have.\n *\n * It is important to note that the Debug class is also a Reactive module. The debug instance keeps\n * the reactive instances data as its own state. This way it is possible to implement development tools\n * that whatches this data.\n *\n * @class core/reactive/local/reactive/debug/Debug\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nclass Debug extends Reactive {\n\n /**\n * Set the initial state.\n *\n * @param {object} stateData the initial state data.\n */\n setInitialState(stateData) {\n super.setInitialState(stateData);\n log.debug(`Debug module \"M.reactive\" loaded.`);\n }\n\n /**\n * List the currents page reactives instances.\n */\n get list() {\n return JSON.parse(JSON.stringify(this.state.reactives));\n }\n\n /**\n * Register a new Reactive instance.\n *\n * This method is called every time a \"new Reactive\" is executed.\n *\n * @param {Reactive} instance the reactive instance\n */\n registerNewInstance(instance) {\n\n // Generate a valid variable name for that instance.\n let name = instance.name ?? `instance${this.state.reactives.length}`;\n name = name.replace(/\\W/g, '');\n\n log.debug(`Registering new reactive instance \"M.reactive.${name}\"`);\n\n reactiveInstances[name] = instance;\n reactiveDebuggers[name] = new DebugInstance(reactiveInstances[name]);\n // Register also in the state.\n this.dispatch('putInstance', name, instance);\n // Add debug watchers to instance.\n const refreshMethod = () => {\n this.dispatch('putInstance', name, instance);\n };\n instance.target.addEventListener('readmode:on', refreshMethod);\n instance.target.addEventListener('readmode:off', refreshMethod);\n instance.target.addEventListener('registerComponent:success', refreshMethod);\n instance.target.addEventListener('transaction:end', refreshMethod);\n // We store the last transaction into the state.\n const storeTransaction = ({detail}) => {\n const changes = detail?.changes;\n this.dispatch('lastTransaction', name, changes);\n };\n instance.target.addEventListener('transaction:start', storeTransaction);\n }\n\n /**\n * Returns a debugging object for a specific Reactive instance.\n *\n * A debugging object is a class that wraps a Reactive instance to quick access some of the\n * reactive methods using the browser JS console.\n *\n * @param {string} name the Reactive instance name\n * @returns {DebugInstance} a debug object wrapping the Reactive instance\n */\n debug(name) {\n return reactiveDebuggers[name];\n }\n}\n\n/**\n * The debug state mutations class.\n *\n * @class core/reactive/local/reactive/debug/Mutations\n */\nclass Mutations {\n\n /**\n * Insert or update a new instance into the debug state.\n *\n * @param {StateManager} stateManager the debug state manager\n * @param {string} name the instance name\n * @param {Reactive} instance the reactive instance\n */\n putInstance(stateManager, name, instance) {\n const state = stateManager.state;\n\n stateManager.setReadOnly(false);\n\n if (state.reactives.has(name)) {\n state.reactives.get(name).countcomponents = instance.components.length;\n state.reactives.get(name).readOnly = instance.stateManager.readonly;\n state.reactives.get(name).modified = new Date().getTime();\n } else {\n state.reactives.add({\n id: name,\n countcomponents: instance.components.length,\n readOnly: instance.stateManager.readonly,\n lastChanges: [],\n modified: new Date().getTime(),\n });\n }\n stateManager.setReadOnly(true);\n }\n\n /**\n * Update the lastChanges attribute with a list of changes\n *\n * @param {StateManager} stateManager the debug reactive state\n * @param {string} name tje instance name\n * @param {array} changes the list of changes\n */\n lastTransaction(stateManager, name, changes) {\n if (!changes || changes.length === 0) {\n return;\n }\n\n const state = stateManager.state;\n const lastChanges = ['transaction:start'];\n\n changes.forEach(change => {\n lastChanges.push(change.eventName);\n });\n\n lastChanges.push('transaction:end');\n\n stateManager.setReadOnly(false);\n\n state.reactives.get(name).lastChanges = lastChanges;\n\n stateManager.setReadOnly(true);\n }\n}\n\n/**\n * Class used to debug a specific instance and manipulate the state from the JS console.\n *\n * @class core/reactive/local/reactive/debug/DebugInstance\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nclass DebugInstance {\n\n /**\n * Constructor.\n *\n * @param {Reactive} instance the reactive instance\n */\n constructor(instance) {\n this.instance = instance;\n // Add some debug data directly into the instance. This way we avoid having attributes\n // that will confuse the console aoutocomplete.\n if (instance._reactiveDebugData === undefined) {\n instance._reactiveDebugData = {\n highlighted: false,\n };\n }\n }\n\n /**\n * Set the read only mode.\n *\n * Quick access to the instance setReadOnly method.\n *\n * @param {bool} value the new read only value\n */\n set readOnly(value) {\n this.instance.stateManager.setReadOnly(value);\n }\n\n /**\n * Get the read only value\n *\n * @returns {bool}\n */\n get readOnly() {\n return this.instance.stateManager.readonly;\n }\n\n /**\n * Return the current state object.\n *\n * @returns {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 * @returns {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 * @returns {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 * @returns {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 {*} args\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 * @returns {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 * @returns {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"],"names":["reactiveInstances","reactiveDebuggers","Debug","Reactive","setInitialState","stateData","debug","list","JSON","parse","stringify","this","state","reactives","registerNewInstance","instance","name","length","replace","DebugInstance","dispatch","refreshMethod","target","addEventListener","_ref","detail","changes","Mutations","putInstance","stateManager","setReadOnly","has","get","countcomponents","components","readOnly","readonly","modified","Date","getTime","add","id","lastChanges","lastTransaction","forEach","change","push","eventName","constructor","undefined","_reactiveDebugData","highlighted","value","highlight","_ref2","element","border","style","result","eventsToPublish","elements","_ref3","processUpdates","updates","dispatchStateChangedEvent","document","dispatchEvent","CustomEvent","bubbles","eventDispatch","mutations","bind","debuggers"],"mappings":";;;;;;;2KA2BMA,kBAAoB,GAGpBC,kBAAoB;;;;;;;;;;;;;;;MAgBpBC,cAAcC,kBAOhBC,gBAAgBC,iBACND,gBAAgBC,wBAClBC,2CAMJC,kBACOC,KAAKC,MAAMD,KAAKE,UAAUC,KAAKC,MAAMC,YAUhDC,oBAAoBC,iCAGZC,4BAAOD,SAASC,gEAAmBL,KAAKC,MAAMC,UAAUI,QAC5DD,KAAOA,KAAKE,QAAQ,MAAO,iBAEvBZ,8DAAuDU,WAE3DhB,kBAAkBgB,MAAQD,SAC1Bd,kBAAkBe,MAAQ,IAAIG,cAAcnB,kBAAkBgB,YAEzDI,SAAS,cAAeJ,KAAMD,gBAE7BM,cAAgB,UACbD,SAAS,cAAeJ,KAAMD,WAEvCA,SAASO,OAAOC,iBAAiB,cAAeF,eAChDN,SAASO,OAAOC,iBAAiB,eAAgBF,eACjDN,SAASO,OAAOC,iBAAiB,4BAA6BF,eAC9DN,SAASO,OAAOC,iBAAiB,kBAAmBF,eAMpDN,SAASO,OAAOC,iBAAiB,qBAJRC,WAACC,OAACA,mBACjBC,QAAUD,MAAAA,cAAAA,OAAQC,aACnBN,SAAS,kBAAmBJ,KAAMU,YAc/CpB,MAAMU,aACKf,kBAAkBe,aAS3BW,UASFC,YAAYC,aAAcb,KAAMD,gBACtBH,MAAQiB,aAAajB,MAE3BiB,aAAaC,aAAY,GAErBlB,MAAMC,UAAUkB,IAAIf,OACpBJ,MAAMC,UAAUmB,IAAIhB,MAAMiB,gBAAkBlB,SAASmB,WAAWjB,OAChEL,MAAMC,UAAUmB,IAAIhB,MAAMmB,SAAWpB,SAASc,aAAaO,SAC3DxB,MAAMC,UAAUmB,IAAIhB,MAAMqB,UAAW,IAAIC,MAAOC,WAEhD3B,MAAMC,UAAU2B,IAAI,CAChBC,GAAIzB,KACJiB,gBAAiBlB,SAASmB,WAAWjB,OACrCkB,SAAUpB,SAASc,aAAaO,SAChCM,YAAa,GACbL,UAAU,IAAIC,MAAOC,YAG7BV,aAAaC,aAAY,GAU7Ba,gBAAgBd,aAAcb,KAAMU,aAC3BA,SAA8B,IAAnBA,QAAQT,oBAIlBL,MAAQiB,aAAajB,MACrB8B,YAAc,CAAC,qBAErBhB,QAAQkB,SAAQC,SACZH,YAAYI,KAAKD,OAAOE,cAG5BL,YAAYI,KAAK,mBAEjBjB,aAAaC,aAAY,GAEzBlB,MAAMC,UAAUmB,IAAIhB,MAAM0B,YAAcA,YAExCb,aAAaC,aAAY;;;;;;;WAW3BX,cAOF6B,YAAYjC,eACHA,SAAWA,cAGoBkC,IAAhClC,SAASmC,qBACTnC,SAASmC,mBAAqB,CAC1BC,aAAa,IAYrBhB,aAASiB,YACJrC,SAASc,aAAaC,YAAYsB,OAQvCjB,sBACOxB,KAAKI,SAASc,aAAaO,SAQlCxB,mBACOD,KAAKI,SAASH,MAQrByC,cAAUD,YACLrC,SAASmC,mBAAmBC,YAAcC,WAC1CrC,SAASmB,WAAWU,SAAQU,YAACC,QAACA,qBACzBC,OAAUJ,4BAAiC,GACjDG,QAAQE,MAAMD,OAASA,UAS3BH,uBACO1C,KAAKI,SAASmC,mBAAmBC,YAQxCjB,uBACO,IAAIvB,KAAKI,SAASmB,YAQzBR,oBACMgC,OAAS,eACV3C,SAASc,aAAa8B,gBAAgBf,SACtCW,UACGG,OAAOZ,KAAKS,QAAQR,cAGrBW,6BAaF3C,SAASK,uBAQdwC,qBACMF,OAAS,eACV3C,SAASmB,WAAWU,SAAQiB,YAACN,QAACA,eAC/BG,OAAOZ,KAAKS,YAETG,OAQPrD,uBACOG,KAAKC,MAAMD,KAAKE,UAAUC,KAAKC,QAQ1CkD,eAAeC,cACNhD,SAASc,aAAaiC,eAAeC,mBAazCC,0BAA0BvC,OAAQH,aACxB2B,IAAX3B,SACAA,OAAS2C,UAEb3C,OAAO4C,cACH,IAAIC,YAdkB,mCAgBlB,CACIC,SAAS,EACT3C,OAAQA,6BAUC,WACfnB,MAAQ,IAAIJ,MAAM,CACpBc,KAAM,oBACN+B,UA/BsB,mCAgCtBsB,cAAeL,0BACfM,UAAW,IAAI3C,UACff,MAAO,CACHC,UAAW,aAMnBZ,kBAAkBa,oBAAsBR,MAAMQ,oBAAoByD,KAAKjE,OAEhE,CACHA,MAAAA,MACAkE,UAAWvE"} \ No newline at end of file +{"version":3,"file":"debug.min.js","sources":["../../../src/local/reactive/debug.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive module debug tools.\n *\n * @module core/local/reactive/debug\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Reactive from 'core/local/reactive/reactive';\nimport log from 'core/log';\n\n// The list of reactives instances.\nconst reactiveInstances = {};\n\n// The reactive debugging objects.\nconst reactiveDebuggers = {};\n\n/**\n * Reactive module debug tools.\n *\n * If debug is enabled, this reactive module will spy all the reactive instances and keep a record\n * of the changes and components they have.\n *\n * It is important to note that the Debug class is also a Reactive module. The debug instance keeps\n * the reactive instances data as its own state. This way it is possible to implement development tools\n * that whatches this data.\n *\n * @class core/reactive/local/reactive/debug/Debug\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nclass Debug extends Reactive {\n\n /**\n * Set the initial state.\n *\n * @param {object} stateData the initial state data.\n */\n setInitialState(stateData) {\n super.setInitialState(stateData);\n log.debug(`Debug module \"M.reactive\" loaded.`);\n }\n\n /**\n * List the currents page reactives instances.\n */\n get list() {\n return JSON.parse(JSON.stringify(this.state.reactives));\n }\n\n /**\n * Register a new Reactive instance.\n *\n * This method is called every time a \"new Reactive\" is executed.\n *\n * @param {Reactive} instance the reactive instance\n */\n registerNewInstance(instance) {\n\n // Generate a valid variable name for that instance.\n let name = instance.name ?? `instance${this.state.reactives.length}`;\n name = name.replace(/\\W/g, '');\n\n log.debug(`Registering new reactive instance \"M.reactive.${name}\"`);\n\n reactiveInstances[name] = instance;\n reactiveDebuggers[name] = new DebugInstance(reactiveInstances[name]);\n // Register also in the state.\n this.dispatch('putInstance', name, instance);\n // Add debug watchers to instance.\n const refreshMethod = () => {\n this.dispatch('putInstance', name, instance);\n };\n instance.target.addEventListener('readmode:on', refreshMethod);\n instance.target.addEventListener('readmode:off', refreshMethod);\n instance.target.addEventListener('registerComponent:success', refreshMethod);\n instance.target.addEventListener('transaction:end', refreshMethod);\n // We store the last transaction into the state.\n const storeTransaction = ({detail}) => {\n const changes = detail?.changes;\n this.dispatch('lastTransaction', name, changes);\n };\n instance.target.addEventListener('transaction:end', 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 the 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 // Dirty hack to force the lastChanges:updated event to be dispatched.\n state.reactives.get(name).lastChanges = [];\n\n // Assign the actual value.\n state.reactives.get(name).lastChanges = lastChanges;\n\n stateManager.setReadOnly(true);\n }\n}\n\n/**\n * Class used to debug a specific instance and manipulate the state from the JS console.\n *\n * @class core/reactive/local/reactive/debug/DebugInstance\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nclass DebugInstance {\n\n /**\n * Constructor.\n *\n * @param {Reactive} instance the reactive instance\n */\n constructor(instance) {\n this.instance = instance;\n // Add some debug data directly into the instance. This way we avoid having attributes\n // that will confuse the console aoutocomplete.\n if (instance._reactiveDebugData === undefined) {\n instance._reactiveDebugData = {\n highlighted: false,\n };\n }\n }\n\n /**\n * Set the read only mode.\n *\n * Quick access to the instance setReadOnly method.\n *\n * @param {bool} value the new read only value\n */\n set readOnly(value) {\n this.instance.stateManager.setReadOnly(value);\n }\n\n /**\n * Get the read only value\n *\n * @returns {bool}\n */\n get readOnly() {\n return this.instance.stateManager.readonly;\n }\n\n /**\n * Return the current state object.\n *\n * @returns {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 * @returns {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 * @returns {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 * @returns {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 {*} args\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 * @returns {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 * @returns {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"],"names":["reactiveInstances","reactiveDebuggers","Debug","Reactive","setInitialState","stateData","debug","list","JSON","parse","stringify","this","state","reactives","registerNewInstance","instance","name","length","replace","DebugInstance","dispatch","refreshMethod","target","addEventListener","_ref","detail","changes","Mutations","putInstance","stateManager","setReadOnly","has","get","countcomponents","components","readOnly","readonly","modified","Date","getTime","add","id","lastChanges","lastTransaction","forEach","change","push","eventName","constructor","undefined","_reactiveDebugData","highlighted","value","highlight","_ref2","element","border","style","result","eventsToPublish","elements","_ref3","processUpdates","updates","dispatchStateChangedEvent","document","dispatchEvent","CustomEvent","bubbles","eventDispatch","mutations","bind","debuggers"],"mappings":"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"} \ 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 index d6f23b882a6..fa09589afbf 100644 --- a/lib/amd/build/local/reactive/debugpanel.min.js +++ b/lib/amd/build/local/reactive/debugpanel.min.js @@ -9,6 +9,6 @@ define("core/local/reactive/debugpanel",["exports","core/reactive","core/log","c * @module core/local/reactive/debugpanel * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initsubpanel=_exports.init=void 0,_log=(obj=_log)&&obj.__esModule?obj:{default:obj};_exports.init=(target,selectors)=>{const element=document.getElementById(target);void 0!==_reactive.debug?new GlobalDebugPanel({element:element,reactive:_reactive.debug,selectors:selectors}):element.remove()};_exports.initsubpanel=(target,selectors)=>{const element=document.getElementById(target);void 0!==_reactive.debug?new DebugInstanceSubpanel({element:element,reactive:_reactive.debug,selectors:selectors}):element.remove()};class GlobalDebugPanel extends _reactive.BaseComponent{create(){this.name="GlobalDebugPanel",this.selectors={LOADERS:"[data-for='loaders']",SUBPANEL:"[data-for='subpanel']",NOINSTANCES:"[data-for='noinstances']",LOG:"[data-for='log']"},this.classes={HIDE:"d-none"},this.subPanels=new Set}stateReady(state){this._updateReactivesPanels({state:state}),this.getElement(this.selectors.SUBPANEL).innerHTML=""}getWatchers(){return[{watch:"reactives:created",handler:this._updateReactivesPanels}]}_updateReactivesPanels(_ref){var _this$getElement,_this$getElement$clas;let{state:state}=_ref;null===(_this$getElement=this.getElement(this.selectors.NOINSTANCES))||void 0===_this$getElement||null===(_this$getElement$clas=_this$getElement.classList)||void 0===_this$getElement$clas||_this$getElement$clas.toggle(this.classes.HIDE,state.reactives.size>0),state.reactives.forEach((instance=>{this._createLoader(instance)}))}_createLoader(instance){if(this.subPanels.has(instance.id))return;this.subPanels.add(instance.id);const loaders=this.getElement(this.selectors.LOADERS),btn=document.createElement("button");btn.innerHTML=instance.id,btn.dataset.id=instance.id,loaders.appendChild(btn),this.addEventListener(btn,"click",(()=>this._openPanel(btn,instance)))}async _openPanel(btn,instance){try{const target=this.getElement(this.selectors.SUBPANEL),data={...instance};await this.renderComponent(target,"core/local/reactive/debuginstancepanel",data)}catch(error){throw _log.default.error("Cannot load reactive debug subpanel"),error}}}class DebugInstanceSubpanel extends _reactive.BaseComponent{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:""}}stateReady(){var _this$getElement$inne,_this$getElement2;this.dragdrop=new _reactive.DragDrop(this),this.addEventListener(this.getElement(this.selectors.CLOSE),"click",this.remove),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))})),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,_utils.debounce)(this._checkJSON.bind(this),500)),this.addEventListener(this.getElement(this.selectors.SAVE),"click",this._saveState),this.strings.savewarning=null!==(_this$getElement$inne=null===(_this$getElement2=this.getElement(this.selectors.INVALID))||void 0===_this$getElement2?void 0:_this$getElement2.innerHTML)&&void 0!==_this$getElement$inne?_this$getElement$inne:"",this._refreshState()}destroy(){void 0!==this.dragdrop&&this.dragdrop.unregister()}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}]}_refreshLog(_ref2){var _element$lastChanges;let{element:element}=_ref2;const logContent=(null!==(_element$lastChanges=null==element?void 0:element.lastChanges)&&void 0!==_element$lastChanges?_element$lastChanges:[]).join("\n"),target=this.getElement(this.selectors.LOG);target.value+="\n\n= Transaction =\n ".concat(logContent),target.scrollTop=target.scrollHeight}_cleanAreas(){this.getElement(this.selectors.LOG).value="",this._refreshState()}_refreshState(){this.getElement(this.selectors.STATE).value=JSON.stringify(this.controller.state,null,4)}_refreshReadOnly(){const target=this.getElement(this.selectors.READMODE);void 0===target.dataset.readonly&&(target.dataset.readonly=target.innerHTML),this.controller.readOnly?target.innerHTML=target.dataset.readonly:target.innerHTML=target.dataset.alt}_toggleEditMode(){this.controller.readOnly=!this.controller.readOnly}_checkJSON(){const invalid=this.getElement(this.selectors.INVALID),save=this.getElement(this.selectors.SAVE),edited=this.getElement(this.selectors.STATE).value,currentStateData=this.controller.stateData;if(edited==JSON.stringify(this.controller.state,null,4))return invalid.style.color="",invalid.innerHTML="",void(save.disabled=!0);try{const newState=JSON.parse(edited),result=this._generateStateUpdates(currentStateData,newState);return invalid.style.color="",invalid.innerHTML=this.strings.savewarning,save.disabled=!1,result}catch(error){var _error$message;return invalid.style.color="red",invalid.innerHTML=null!==(_error$message=error.message)&&void 0!==_error$message?_error$message:"Invalid JSON sctructure",void(save.disabled=!0)}}_saveState(){const updates=this._checkJSON();updates&&this.controller.processUpdates(updates)}_generateStateUpdates(currentStateData,newStateData){const updates=[],ids={};for(const[key,newValue]of Object.entries(newStateData))Array.isArray(newValue)?(ids[key]={},newValue.forEach((element=>{if(void 0===element.id)throw Error("Array ".concat(key," element without id attribute"));updates.push({name:key,action:"override",fields:element});const index=String(element.id).valueOf();ids[key][index]=!0}))):updates.push({name:key,action:"override",fields:newValue});for(const[key,oldValue]of Object.entries(currentStateData)){let deleteField=!1;if(void 0===newStateData[key]&&(deleteField=!0),Array.isArray(oldValue)){if(!deleteField&&void 0===ids[key])throw Error("Array ".concat(key," cannot change to object."));oldValue.forEach((element=>{const index=String(element.id).valueOf();let deleteEntry=deleteField;deleteEntry||void 0!==ids[key][index]||(deleteEntry=!0),deleteEntry&&updates.push({name:key,action:"delete",fields:element})}))}else{if(!deleteField&&void 0!==ids[key])throw Error("Object ".concat(key," cannot change to array."));deleteField&&updates.push({name:key,action:"delete",fields:oldValue})}}return updates}getDraggableData(){return this.draggable}dragEnd(dropdata,event){this.element.style.top="".concat(event.newFixedTop,"px"),this.element.style.left="".concat(event.newFixedLeft,"px")}_togglePin(){this.draggable=!this.draggable,this.dragdrop.setDraggable(this.draggable),this.draggable?this._unpin():this._pin()}_unpin(){const pageCenterY=window.innerHeight/2,pageCenterX=window.innerWidth/2,style={position:"fixed",resize:"both",overflow:"auto",height:"400px",width:"400px",top:"".concat(pageCenterY-200,"px"),left:"".concat(pageCenterX-200,"px")};Object.assign(this.element.style,style),this.getElement(this.selectors.STATE).style.height="50px",this.getElement(this.selectors.LOG).style.height="50px",this._toggleButtonText(this.getElement(this.selectors.PIN))}_pin(){["position","resize","overflow","top","left","height","width"].forEach((prop=>this.element.style.removeProperty(prop))),this._toggleButtonText(this.getElement(this.selectors.PIN))}_toggleButtonText(element){[element.innerHTML,element.dataset.alt]=[element.dataset.alt,element.innerHTML]}}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initsubpanel=_exports.init=void 0,_log=(obj=_log)&&obj.__esModule?obj:{default:obj};_exports.init=(target,selectors)=>{const element=document.getElementById(target);void 0!==_reactive.debug?new GlobalDebugPanel({element:element,reactive:_reactive.debug,selectors:selectors}):element.remove()};_exports.initsubpanel=(target,selectors)=>{const element=document.getElementById(target);void 0!==_reactive.debug?new DebugInstanceSubpanel({element:element,reactive:_reactive.debug,selectors:selectors}):element.remove()};class GlobalDebugPanel extends _reactive.BaseComponent{create(){this.name="GlobalDebugPanel",this.selectors={LOADERS:"[data-for='loaders']",SUBPANEL:"[data-for='subpanel']",NOINSTANCES:"[data-for='noinstances']",LOG:"[data-for='log']"},this.classes={HIDE:"d-none"},this.subPanels=new Set}stateReady(state){this._updateReactivesPanels({state:state}),this.getElement(this.selectors.SUBPANEL).innerHTML=""}getWatchers(){return[{watch:"reactives:created",handler:this._updateReactivesPanels}]}_updateReactivesPanels(_ref){var _this$getElement,_this$getElement$clas;let{state:state}=_ref;null===(_this$getElement=this.getElement(this.selectors.NOINSTANCES))||void 0===_this$getElement||null===(_this$getElement$clas=_this$getElement.classList)||void 0===_this$getElement$clas||_this$getElement$clas.toggle(this.classes.HIDE,state.reactives.size>0),state.reactives.forEach((instance=>{this._createLoader(instance)}))}_createLoader(instance){if(this.subPanels.has(instance.id))return;this.subPanels.add(instance.id);const loaders=this.getElement(this.selectors.LOADERS),btn=document.createElement("button");btn.innerHTML=instance.id,btn.dataset.id=instance.id,loaders.appendChild(btn),this.addEventListener(btn,"click",(()=>this._openPanel(btn,instance)))}async _openPanel(btn,instance){try{const target=this.getElement(this.selectors.SUBPANEL),data={...instance};await this.renderComponent(target,"core/local/reactive/debuginstancepanel",data)}catch(error){throw _log.default.error("Cannot load reactive debug subpanel"),error}}}class DebugInstanceSubpanel extends _reactive.BaseComponent{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:""}}stateReady(){var _this$getElement$inne,_this$getElement2;this.dragdrop=new _reactive.DragDrop(this),this.addEventListener(this.getElement(this.selectors.CLOSE),"click",this.remove),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))})),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,_utils.debounce)(this._checkJSON.bind(this),500)),this.addEventListener(this.getElement(this.selectors.SAVE),"click",this._saveState),this.strings.savewarning=null!==(_this$getElement$inne=null===(_this$getElement2=this.getElement(this.selectors.INVALID))||void 0===_this$getElement2?void 0:_this$getElement2.innerHTML)&&void 0!==_this$getElement$inne?_this$getElement$inne:"",this._refreshState()}destroy(){void 0!==this.dragdrop&&this.dragdrop.unregister()}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}]}_refreshLog(_ref2){var _element$lastChanges;let{element:element}=_ref2;const list=null!==(_element$lastChanges=null==element?void 0:element.lastChanges)&&void 0!==_element$lastChanges?_element$lastChanges:[],target=this.getElement(this.selectors.LOG);""!==target.value&&(target.value+="\n\n");const logContent=list.join("\n");target.value+="= Transaction =\n".concat(logContent),target.scrollTop=target.scrollHeight}_cleanAreas(){this.getElement(this.selectors.LOG).value="",this._refreshState()}_refreshState(){this.getElement(this.selectors.STATE).value=JSON.stringify(this.controller.state,null,4)}_refreshReadOnly(){const target=this.getElement(this.selectors.READMODE);void 0===target.dataset.readonly&&(target.dataset.readonly=target.innerHTML),this.controller.readOnly?target.innerHTML=target.dataset.readonly:target.innerHTML=target.dataset.alt}_toggleEditMode(){this.controller.readOnly=!this.controller.readOnly}_checkJSON(){const invalid=this.getElement(this.selectors.INVALID),save=this.getElement(this.selectors.SAVE),edited=this.getElement(this.selectors.STATE).value,currentStateData=this.controller.stateData;if(edited==JSON.stringify(this.controller.state,null,4))return invalid.style.color="",invalid.innerHTML="",void(save.disabled=!0);try{const newState=JSON.parse(edited),result=this._generateStateUpdates(currentStateData,newState);return invalid.style.color="",invalid.innerHTML=this.strings.savewarning,save.disabled=!1,result}catch(error){var _error$message;return invalid.style.color="red",invalid.innerHTML=null!==(_error$message=error.message)&&void 0!==_error$message?_error$message:"Invalid JSON sctructure",void(save.disabled=!0)}}_saveState(){const updates=this._checkJSON();updates&&this.controller.processUpdates(updates)}_generateStateUpdates(currentStateData,newStateData){const updates=[],ids={};for(const[key,newValue]of Object.entries(newStateData))Array.isArray(newValue)?(ids[key]={},newValue.forEach((element=>{if(void 0===element.id)throw Error("Array ".concat(key," element without id attribute"));updates.push({name:key,action:"override",fields:element});const index=String(element.id).valueOf();ids[key][index]=!0}))):updates.push({name:key,action:"override",fields:newValue});for(const[key,oldValue]of Object.entries(currentStateData)){let deleteField=!1;if(void 0===newStateData[key]&&(deleteField=!0),Array.isArray(oldValue)){if(!deleteField&&void 0===ids[key])throw Error("Array ".concat(key," cannot change to object."));oldValue.forEach((element=>{const index=String(element.id).valueOf();let deleteEntry=deleteField;deleteEntry||void 0!==ids[key][index]||(deleteEntry=!0),deleteEntry&&updates.push({name:key,action:"delete",fields:element})}))}else{if(!deleteField&&void 0!==ids[key])throw Error("Object ".concat(key," cannot change to array."));deleteField&&updates.push({name:key,action:"delete",fields:oldValue})}}return updates}getDraggableData(){return this.draggable}dragEnd(dropdata,event){this.element.style.top="".concat(event.newFixedTop,"px"),this.element.style.left="".concat(event.newFixedLeft,"px")}_togglePin(){this.draggable=!this.draggable,this.dragdrop.setDraggable(this.draggable),this.draggable?this._unpin():this._pin()}_unpin(){const pageCenterY=window.innerHeight/2,pageCenterX=window.innerWidth/2,style={position:"fixed",resize:"both",overflow:"auto",height:"400px",width:"400px",top:"".concat(pageCenterY-200,"px"),left:"".concat(pageCenterX-200,"px")};Object.assign(this.element.style,style),this.getElement(this.selectors.STATE).style.height="50px",this.getElement(this.selectors.LOG).style.height="50px",this._toggleButtonText(this.getElement(this.selectors.PIN))}_pin(){["position","resize","overflow","top","left","height","width"].forEach((prop=>this.element.style.removeProperty(prop))),this._toggleButtonText(this.getElement(this.selectors.PIN))}_toggleButtonText(element){[element.innerHTML,element.dataset.alt]=[element.dataset.alt,element.innerHTML]}}})); //# sourceMappingURL=debugpanel.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/reactive/debugpanel.min.js.map b/lib/amd/build/local/reactive/debugpanel.min.js.map index b6841c9f46a..fab4c15f68e 100644 --- a/lib/amd/build/local/reactive/debugpanel.min.js.map +++ b/lib/amd/build/local/reactive/debugpanel.min.js.map @@ -1 +1 @@ -{"version":3,"file":"debugpanel.min.js","sources":["../../../src/local/reactive/debugpanel.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive module debug panel.\n *\n * This module contains all the UI components for the reactive debug tools.\n * Those tools are only available if the debug is enables and could be used\n * from the footer.\n *\n * @module core/local/reactive/debugpanel\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop, debug} from 'core/reactive';\nimport log from 'core/log';\nimport {debounce} from 'core/utils';\n\n/**\n * Init the main reactive panel.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n */\nexport const init = (target, selectors) => {\n const element = document.getElementById(target);\n // Check if the debug reactive module is available.\n if (debug === undefined) {\n element.remove();\n return;\n }\n // Create the main component.\n new GlobalDebugPanel({\n element,\n reactive: debug,\n selectors,\n });\n};\n\n/**\n * Init an instance reactive subpanel.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n */\nexport const initsubpanel = (target, selectors) => {\n const element = document.getElementById(target);\n // Check if the debug reactive module is available.\n if (debug === undefined) {\n element.remove();\n return;\n }\n // Create the main component.\n new DebugInstanceSubpanel({\n element,\n reactive: debug,\n selectors,\n });\n};\n\n/**\n * Component for the main reactive dev panel.\n *\n * This component shows the list of reactive instances and handle the buttons\n * to open a specific instance panel.\n */\nclass GlobalDebugPanel extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'GlobalDebugPanel';\n // Default query selectors.\n this.selectors = {\n LOADERS: `[data-for='loaders']`,\n SUBPANEL: `[data-for='subpanel']`,\n NOINSTANCES: `[data-for='noinstances']`,\n LOG: `[data-for='log']`,\n };\n this.classes = {\n HIDE: `d-none`,\n };\n // The list of loaded debuggers.\n this.subPanels = new Set();\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n stateReady(state) {\n this._updateReactivesPanels({state});\n // Remove loading wheel.\n this.getElement(this.selectors.SUBPANEL).innerHTML = '';\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `reactives:created`, handler: this._updateReactivesPanels},\n ];\n }\n\n /**\n * Update the list of reactive instances.\n * @param {Object} args\n * @param {Object} args.state the current state\n */\n _updateReactivesPanels({state}) {\n this.getElement(this.selectors.NOINSTANCES)?.classList?.toggle(\n this.classes.HIDE,\n state.reactives.size > 0\n );\n // Generate loading buttons.\n state.reactives.forEach(\n instance => {\n this._createLoader(instance);\n }\n );\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 if (this.subPanels.has(instance.id)) {\n return;\n }\n this.subPanels.add(instance.id);\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.bind(this), 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} args\n * @param {HTMLElement} args.element\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"],"names":["target","selectors","element","document","getElementById","undefined","debug","GlobalDebugPanel","reactive","remove","DebugInstanceSubpanel","BaseComponent","create","name","LOADERS","SUBPANEL","NOINSTANCES","LOG","classes","HIDE","subPanels","Set","stateReady","state","_updateReactivesPanels","getElement","this","innerHTML","getWatchers","watch","handler","classList","toggle","reactives","size","forEach","instance","_createLoader","has","id","add","loaders","btn","createElement","dataset","appendChild","addEventListener","_openPanel","data","renderComponent","error","NAME","CLOSE","READMODE","HIGHLIGHT","STATE","CLEAN","PIN","SAVE","INVALID","controller","M","draggable","relativeDrag","strings","savewarning","dragdrop","DragDrop","highlight","_toggleButtonText","_toggleEditMode","_cleanAreas","_togglePin","disabled","_checkJSON","bind","_saveState","_this$getElement2","_refreshState","destroy","unregister","_refreshLog","_refreshReadOnly","logContent","lastChanges","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","Object","entries","Array","isArray","Error","push","action","fields","index","String","valueOf","oldValue","deleteField","deleteEntry","getDraggableData","dragEnd","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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"} \ No newline at end of file +{"version":3,"file":"debugpanel.min.js","sources":["../../../src/local/reactive/debugpanel.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive module debug panel.\n *\n * This module contains all the UI components for the reactive debug tools.\n * Those tools are only available if the debug is enables and could be used\n * from the footer.\n *\n * @module core/local/reactive/debugpanel\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop, debug} from 'core/reactive';\nimport log from 'core/log';\nimport {debounce} from 'core/utils';\n\n/**\n * Init the main reactive panel.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n */\nexport const init = (target, selectors) => {\n const element = document.getElementById(target);\n // Check if the debug reactive module is available.\n if (debug === undefined) {\n element.remove();\n return;\n }\n // Create the main component.\n new GlobalDebugPanel({\n element,\n reactive: debug,\n selectors,\n });\n};\n\n/**\n * Init an instance reactive subpanel.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n */\nexport const initsubpanel = (target, selectors) => {\n const element = document.getElementById(target);\n // Check if the debug reactive module is available.\n if (debug === undefined) {\n element.remove();\n return;\n }\n // Create the main component.\n new DebugInstanceSubpanel({\n element,\n reactive: debug,\n selectors,\n });\n};\n\n/**\n * Component for the main reactive dev panel.\n *\n * This component shows the list of reactive instances and handle the buttons\n * to open a specific instance panel.\n */\nclass GlobalDebugPanel extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'GlobalDebugPanel';\n // Default query selectors.\n this.selectors = {\n LOADERS: `[data-for='loaders']`,\n SUBPANEL: `[data-for='subpanel']`,\n NOINSTANCES: `[data-for='noinstances']`,\n LOG: `[data-for='log']`,\n };\n this.classes = {\n HIDE: `d-none`,\n };\n // The list of loaded debuggers.\n this.subPanels = new Set();\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n stateReady(state) {\n this._updateReactivesPanels({state});\n // Remove loading wheel.\n this.getElement(this.selectors.SUBPANEL).innerHTML = '';\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `reactives:created`, handler: this._updateReactivesPanels},\n ];\n }\n\n /**\n * Update the list of reactive instances.\n * @param {Object} args\n * @param {Object} args.state the current state\n */\n _updateReactivesPanels({state}) {\n this.getElement(this.selectors.NOINSTANCES)?.classList?.toggle(\n this.classes.HIDE,\n state.reactives.size > 0\n );\n // Generate loading buttons.\n state.reactives.forEach(\n instance => {\n this._createLoader(instance);\n }\n );\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 if (this.subPanels.has(instance.id)) {\n return;\n }\n this.subPanels.add(instance.id);\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.bind(this), 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 * Watcher method to refresh the log panel.\n *\n * @param {object} args\n * @param {HTMLElement} args.element\n */\n _refreshLog({element}) {\n const list = element?.lastChanges ?? [];\n\n // Append last log.\n\n const target = this.getElement(this.selectors.LOG);\n if (target.value !== '') {\n target.value += '\\n\\n';\n }\n\n const logContent = list.join(\"\\n\");\n\n target.value += `= 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"],"names":["target","selectors","element","document","getElementById","undefined","debug","GlobalDebugPanel","reactive","remove","DebugInstanceSubpanel","BaseComponent","create","name","LOADERS","SUBPANEL","NOINSTANCES","LOG","classes","HIDE","subPanels","Set","stateReady","state","_updateReactivesPanels","getElement","this","innerHTML","getWatchers","watch","handler","classList","toggle","reactives","size","forEach","instance","_createLoader","has","id","add","loaders","btn","createElement","dataset","appendChild","addEventListener","_openPanel","data","renderComponent","error","NAME","CLOSE","READMODE","HIGHLIGHT","STATE","CLEAN","PIN","SAVE","INVALID","controller","M","draggable","relativeDrag","strings","savewarning","dragdrop","DragDrop","highlight","_toggleButtonText","_toggleEditMode","_cleanAreas","_togglePin","disabled","_checkJSON","bind","_saveState","_this$getElement2","_refreshState","destroy","unregister","_refreshLog","_refreshReadOnly","list","lastChanges","value","logContent","join","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","Object","entries","Array","isArray","Error","push","action","fields","index","String","valueOf","oldValue","deleteField","deleteEntry","getDraggableData","dragEnd","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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"} \ 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 3160922fd0e..3a52147badc 100644 --- a/lib/amd/build/local/reactive/statemanager.min.js +++ b/lib/amd/build/local/reactive/statemanager.min.js @@ -14,6 +14,6 @@ define("core/local/reactive/statemanager",["exports","core/local/reactive/logger * @class StateManager * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_logger=(obj=_logger)&&obj.__esModule?obj:{default:obj};_exports.default=class{constructor(dispatchEvent,target){this.dispatchEvent=dispatchEvent,this.target=null!=target?target: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),remove:this.defaultRemove.bind(this),prepareFields:this.defaultPrepareFields.bind(this)},this.initialPromise=new Promise((resolve=>{this.target.addEventListener("state:loaded",(event=>{resolve(event.detail.state)}))})),this.logger=new _logger.default}setInitialState(initialState){if(void 0!==this.state)throw Error("Initial state can only be initialized ones");const state=new Proxy({},new Handler("state",this,!0));for(const[prop,propValue]of Object.entries(initialState))state[prop]=propValue;this.state=state,this.readonly=!0,this.dispatchEvent({action:"state:loaded",state:this.state},this.target)}getInitialPromise(){return this.initialPromise}setReadOnly(readonly){this.readonly=readonly;let mode="off";this.readonly&&(mode="on",this._publishEvents()),this.dispatchEvent({action:"readmode:".concat(mode),state:this.state,element:null},this.target)}addUpdateTypes(newFunctions){for(const[updateType,updateFunction]of Object.entries(newFunctions))"function"==typeof updateFunction&&(this.updateTypes[updateType]=updateFunction.bind(newFunctions))}processUpdates(updates,updateTypes){if(!Array.isArray(updates))throw Error("State updates must be an array");this.setReadOnly(!1),updates.forEach((update=>{if(void 0===update.name)throw Error("Missing state update name");this.processUpdate(update.name,update.action,update.fields,updateTypes)})),this.setReadOnly(!0)}processUpdate(updateName,action,fields,updateTypes){var _action,_updateTypes$action,_updateTypes$prepareF;if(!fields)throw Error("Missing state update fields");void 0===updateTypes&&(updateTypes={});const method=null!==(_updateTypes$action=updateTypes[action=null!==(_action=action)&&void 0!==_action?_action:"update"])&&void 0!==_updateTypes$action?_updateTypes$action:this.updateTypes[action];if(void 0===method)throw Error("Unkown update action ".concat(action));method(this,updateName,(null!==(_updateTypes$prepareF=updateTypes.prepareFields)&&void 0!==_updateTypes$prepareF?_updateTypes$prepareF:this.updateTypes.prepareFields)(this,updateName,fields))}defaultPrepareFields(stateManager,updateName,fields){return fields}defaultCreate(stateManager,updateName,fields){let state=stateManager.state;state[updateName]instanceof StateMap?state[updateName].add(fields):state[updateName]=fields}defaultDelete(stateManager,updateName,fields){if(!stateManager.get(updateName,fields.id))throw Error("Inexistent ".concat(updateName," ").concat(fields.id));let state=stateManager.state;state[updateName]instanceof StateMap?state[updateName].delete(fields.id):delete state[updateName]}defaultRemove(stateManager,updateName,fields){if(!stateManager.get(updateName,fields.id))return;let state=stateManager.state;state[updateName]instanceof StateMap?state[updateName].delete(fields.id):delete state[updateName]}defaultUpdate(stateManager,updateName,fields){let current=stateManager.get(updateName,fields.id);if(!current)throw Error("Inexistent ".concat(updateName," ").concat(fields.id));for(const[fieldName,fieldValue]of Object.entries(fields))current[fieldName]=fieldValue}defaultPut(stateManager,updateName,fields){let current=stateManager.get(updateName,fields.id);if(current)for(const[fieldName,fieldValue]of Object.entries(fields))current[fieldName]=fieldValue;else{let state=stateManager.state;if(state[updateName]instanceof StateMap)return void state[updateName].add(fields);state[updateName]=fields}}defaultOverride(stateManager,updateName,fields){let current=stateManager.get(updateName,fields.id);if(current){for(const[fieldName]of Object.entries(current))void 0===fields[fieldName]&&delete current[fieldName];for(const[fieldName,fieldValue]of Object.entries(fields))current[fieldName]=fieldValue}else{let state=stateManager.state;if(state[updateName]instanceof StateMap)return void state[updateName].add(fields);state[updateName]=fields}}setLogger(logger){this.logger=logger}addLoggerEntry(entry){this.logger.add(entry)}get(name,id){const state=this.state;let current=state[name];if(current instanceof StateMap){if(void 0===id)throw Error("Missing id for ".concat(name," state update"));current=state[name].get(id)}return current}getIds(name){const state=this.state;if(!(state[name]instanceof StateMap))throw Error("".concat(name," is not an instance of StateMap"));return[...state[name].keys()]}registerStateAction(field,prop,action,data){let parentAction="updated";null!==prop?this.eventsToPublish.push({eventName:"".concat(field,".").concat(prop,":").concat(action),eventData:data,action:action}):parentAction=action,void 0!==data.id&&(null!==prop&&this.eventsToPublish.push({eventName:"".concat(field,"[").concat(data.id,"].").concat(prop,":").concat(action),eventData:data,action:action}),this.eventsToPublish.push({eventName:"".concat(field,"[").concat(data.id,"]:").concat(parentAction),eventData:data,action:parentAction})),this.eventsToPublish.push({eventName:"".concat(field,":").concat(parentAction),eventData:data,action:parentAction}),this.eventsToPublish.push({eventName:"state:updated",eventData:data,action:"updated"})}_publishEvents(){const fieldChanges=this.eventsToPublish;this.eventsToPublish=[],this.dispatchEvent({action:"transaction:start",state:this.state,element:null,changes:fieldChanges},this.target),fieldChanges.sort(((a,b)=>{var _weights$a$action,_weights$b$action;const weights={created:0,updated:1,deleted:2},aweight=null!==(_weights$a$action=weights[a.action])&&void 0!==_weights$a$action?_weights$a$action:0,bweight=null!==(_weights$b$action=weights[b.action])&&void 0!==_weights$b$action?_weights$b$action:0;return aweight===bweight?a.eventName.length-b.eventName.length:aweight-bweight}));let publishedEvents=new Set;fieldChanges.forEach((event=>{var _event$eventData$id;const eventkey="".concat(event.eventName,".").concat(null!==(_event$eventData$id=event.eventData.id)&&void 0!==_event$eventData$id?_event$eventData$id:0);publishedEvents.has(eventkey)||(this.dispatchEvent({action:event.eventName,state:this.state,element:event.eventData},this.target),publishedEvents.add(eventkey))})),this.dispatchEvent({action:"transaction:end",state:this.state,element:null},this.target)}};class Handler{constructor(name,stateManager,proxyValues){this.name=name,this.stateManager=stateManager,this.proxyValues=null!=proxyValues&&proxyValues}set(obj,prop,value,receiver){if(this.stateManager.readonly)throw new Error("State locked. Use mutations to change ".concat(prop," value in ").concat(this.name,"."));if(JSON.stringify(obj[prop])===JSON.stringify(value))return!0;const action=void 0!==obj[prop]?"updated":"created";return this.proxyValues?Array.isArray(value)?obj[prop]=new StateMap(prop,this.stateManager).loadValues(value):obj[prop]=new Proxy(value,new Handler(prop,this.stateManager)):obj[prop]=value,void 0===this.stateManager.state||this.stateManager.registerStateAction(this.name,prop,action,receiver),!0}deleteProperty(obj,prop){if(this.stateManager.readonly)throw new Error("State locked. Use mutations to delete ".concat(prop," in ").concat(this.name,"."));return prop in obj&&(delete obj[prop],this.stateManager.registerStateAction(this.name,prop,"deleted",obj)),!0}}class StateMap extends Map{constructor(name,stateManager,iterable){super(iterable),this.name=name,this.stateManager=stateManager}set(key,value){if(this.stateManager.readonly)throw new Error("State locked. Use mutations to change ".concat(key," value in ").concat(this.name,"."));if(key=this.normalizeKey(key),this.checkValue(value),null==key)throw Error("State lists keys cannot be null or undefined");if(this.normalizeKey(value.id)!==key)throw new Error("State error: ".concat(this.name," list element ID (").concat(value.id,") and key (").concat(key,") mismatch"));const action=super.has(key)?"updated":"created",result=super.set(key,new Proxy(value,new Handler(this.name,this.stateManager)));return void 0===this.stateManager.state||this.stateManager.registerStateAction(this.name,null,action,super.get(key)),result}checkValue(value){if(void 0===value.id)throw Error("State lists elements must contain at least an id attribute")}normalizeKey(key){return String(key).valueOf()}add(value){return this.checkValue(value),this.set(value.id,value)}get(key){return super.get(this.normalizeKey(key))}has(key){return super.has(this.normalizeKey(key))}delete(key){if(key=this.normalizeKey(key),this.stateManager.readonly)throw new Error("State locked. Use mutations to change ".concat(key," value in ").concat(this.name,"."));const previous=super.get(key),result=super.delete(key);return result?(this.stateManager.registerStateAction(this.name,null,"deleted",previous),result):result}toJSON(){let result=[];return this.forEach((value=>{result.push(value)})),result}loadValues(values){return values.forEach((data=>{this.checkValue(data);let key=data.id,newvalue=new Proxy(data,new Handler(this.name,this.stateManager));this.set(key,newvalue)})),this}}return _exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_logger=(obj=_logger)&&obj.__esModule?obj:{default:obj};_exports.default=class{constructor(dispatchEvent,target){this.dispatchEvent=dispatchEvent,this.target=null!=target?target: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),remove:this.defaultRemove.bind(this),prepareFields:this.defaultPrepareFields.bind(this)},this.initialPromise=new Promise((resolve=>{this.target.addEventListener("state:loaded",(event=>{resolve(event.detail.state)}))})),this.logger=new _logger.default}setInitialState(initialState){if(void 0!==this.state)throw Error("Initial state can only be initialized ones");const state=new Proxy({},new Handler("state",this,!0));for(const[prop,propValue]of Object.entries(initialState))state[prop]=propValue;this.state=state,this.readonly=!0,this.dispatchEvent({action:"state:loaded",state:this.state},this.target)}getInitialPromise(){return this.initialPromise}setReadOnly(readonly){this.readonly=readonly;let mode="off";this.readonly&&(mode="on",this._publishEvents()),this.dispatchEvent({action:"readmode:".concat(mode),state:this.state,element:null},this.target)}addUpdateTypes(newFunctions){for(const[updateType,updateFunction]of Object.entries(newFunctions))"function"==typeof updateFunction&&(this.updateTypes[updateType]=updateFunction.bind(newFunctions))}processUpdates(updates,updateTypes){if(!Array.isArray(updates))throw Error("State updates must be an array");this.setReadOnly(!1),updates.forEach((update=>{if(void 0===update.name)throw Error("Missing state update name");this.processUpdate(update.name,update.action,update.fields,updateTypes)})),this.setReadOnly(!0)}processUpdate(updateName,action,fields,updateTypes){var _action,_updateTypes$action,_updateTypes$prepareF;if(!fields)throw Error("Missing state update fields");void 0===updateTypes&&(updateTypes={});const method=null!==(_updateTypes$action=updateTypes[action=null!==(_action=action)&&void 0!==_action?_action:"update"])&&void 0!==_updateTypes$action?_updateTypes$action:this.updateTypes[action];if(void 0===method)throw Error("Unkown update action ".concat(action));method(this,updateName,(null!==(_updateTypes$prepareF=updateTypes.prepareFields)&&void 0!==_updateTypes$prepareF?_updateTypes$prepareF:this.updateTypes.prepareFields)(this,updateName,fields))}defaultPrepareFields(stateManager,updateName,fields){return fields}defaultCreate(stateManager,updateName,fields){let state=stateManager.state;state[updateName]instanceof StateMap?state[updateName].add(fields):state[updateName]=fields}defaultDelete(stateManager,updateName,fields){if(!stateManager.get(updateName,fields.id))throw Error("Inexistent ".concat(updateName," ").concat(fields.id));let state=stateManager.state;state[updateName]instanceof StateMap?state[updateName].delete(fields.id):delete state[updateName]}defaultRemove(stateManager,updateName,fields){if(!stateManager.get(updateName,fields.id))return;let state=stateManager.state;state[updateName]instanceof StateMap?state[updateName].delete(fields.id):delete state[updateName]}defaultUpdate(stateManager,updateName,fields){let current=stateManager.get(updateName,fields.id);if(!current)throw Error("Inexistent ".concat(updateName," ").concat(fields.id));for(const[fieldName,fieldValue]of Object.entries(fields))current[fieldName]=fieldValue}defaultPut(stateManager,updateName,fields){let current=stateManager.get(updateName,fields.id);if(current)for(const[fieldName,fieldValue]of Object.entries(fields))current[fieldName]=fieldValue;else{let state=stateManager.state;if(state[updateName]instanceof StateMap)return void state[updateName].add(fields);state[updateName]=fields}}defaultOverride(stateManager,updateName,fields){let current=stateManager.get(updateName,fields.id);if(current){for(const[fieldName]of Object.entries(current))void 0===fields[fieldName]&&delete current[fieldName];for(const[fieldName,fieldValue]of Object.entries(fields))current[fieldName]=fieldValue}else{let state=stateManager.state;if(state[updateName]instanceof StateMap)return void state[updateName].add(fields);state[updateName]=fields}}setLogger(logger){this.logger=logger}addLoggerEntry(entry){this.logger.add(entry)}get(name,id){const state=this.state;let current=state[name];if(current instanceof StateMap){if(void 0===id)throw Error("Missing id for ".concat(name," state update"));current=state[name].get(id)}return current}getIds(name){const state=this.state;if(!(state[name]instanceof StateMap))throw Error("".concat(name," is not an instance of StateMap"));return[...state[name].keys()]}registerStateAction(field,prop,action,data){let parentAction="updated";null!==prop?this.eventsToPublish.push({eventName:"".concat(field,".").concat(prop,":").concat(action),eventData:data,action:action}):parentAction=action,void 0!==data.id&&(null!==prop&&this.eventsToPublish.push({eventName:"".concat(field,"[").concat(data.id,"].").concat(prop,":").concat(action),eventData:data,action:action}),this.eventsToPublish.push({eventName:"".concat(field,"[").concat(data.id,"]:").concat(parentAction),eventData:data,action:parentAction})),this.eventsToPublish.push({eventName:"".concat(field,":").concat(parentAction),eventData:data,action:parentAction}),this.eventsToPublish.push({eventName:"state:updated",eventData:data,action:"updated"})}_publishEvents(){const fieldChanges=this.eventsToPublish;this.eventsToPublish=[],this.dispatchEvent({action:"transaction:start",state:this.state,element:null,changes:fieldChanges},this.target),fieldChanges.sort(((a,b)=>{var _weights$a$action,_weights$b$action;const weights={created:0,updated:1,deleted:2},aweight=null!==(_weights$a$action=weights[a.action])&&void 0!==_weights$a$action?_weights$a$action:0,bweight=null!==(_weights$b$action=weights[b.action])&&void 0!==_weights$b$action?_weights$b$action:0;return aweight===bweight?a.eventName.length-b.eventName.length:aweight-bweight}));let publishedEvents=new Set,transactionEvents=[];fieldChanges.forEach((event=>{var _event$eventData$id;const eventkey="".concat(event.eventName,".").concat(null!==(_event$eventData$id=event.eventData.id)&&void 0!==_event$eventData$id?_event$eventData$id:0);publishedEvents.has(eventkey)||(this.dispatchEvent({action:event.eventName,state:this.state,element:event.eventData},this.target),publishedEvents.add(eventkey),transactionEvents.push(event))})),this.dispatchEvent({action:"transaction:end",state:this.state,element:null,changes:transactionEvents},this.target)}};class Handler{constructor(name,stateManager,proxyValues){this.name=name,this.stateManager=stateManager,this.proxyValues=null!=proxyValues&&proxyValues}set(obj,prop,value,receiver){if(this.stateManager.readonly)throw new Error("State locked. Use mutations to change ".concat(prop," value in ").concat(this.name,"."));if(JSON.stringify(obj[prop])===JSON.stringify(value))return!0;const action=void 0!==obj[prop]?"updated":"created";return this.proxyValues?Array.isArray(value)?obj[prop]=new StateMap(prop,this.stateManager).loadValues(value):obj[prop]=new Proxy(value,new Handler(prop,this.stateManager)):obj[prop]=value,void 0===this.stateManager.state||this.stateManager.registerStateAction(this.name,prop,action,receiver),!0}deleteProperty(obj,prop){if(this.stateManager.readonly)throw new Error("State locked. Use mutations to delete ".concat(prop," in ").concat(this.name,"."));return prop in obj&&(delete obj[prop],this.stateManager.registerStateAction(this.name,prop,"deleted",obj)),!0}}class StateMap extends Map{constructor(name,stateManager,iterable){super(iterable),this.name=name,this.stateManager=stateManager}set(key,value){if(this.stateManager.readonly)throw new Error("State locked. Use mutations to change ".concat(key," value in ").concat(this.name,"."));if(key=this.normalizeKey(key),this.checkValue(value),null==key)throw Error("State lists keys cannot be null or undefined");if(this.normalizeKey(value.id)!==key)throw new Error("State error: ".concat(this.name," list element ID (").concat(value.id,") and key (").concat(key,") mismatch"));const action=super.has(key)?"updated":"created",result=super.set(key,new Proxy(value,new Handler(this.name,this.stateManager)));return void 0===this.stateManager.state||this.stateManager.registerStateAction(this.name,null,action,super.get(key)),result}checkValue(value){if(void 0===value.id)throw Error("State lists elements must contain at least an id attribute")}normalizeKey(key){return String(key).valueOf()}add(value){return this.checkValue(value),this.set(value.id,value)}get(key){return super.get(this.normalizeKey(key))}has(key){return super.has(this.normalizeKey(key))}delete(key){if(key=this.normalizeKey(key),this.stateManager.readonly)throw new Error("State locked. Use mutations to change ".concat(key," value in ").concat(this.name,"."));const previous=super.get(key),result=super.delete(key);return result?(this.stateManager.registerStateAction(this.name,null,"deleted",previous),result):result}toJSON(){let result=[];return this.forEach((value=>{result.push(value)})),result}loadValues(values){return values.forEach((data=>{this.checkValue(data);let key=data.id,newvalue=new Proxy(data,new Handler(this.name,this.stateManager));this.set(key,newvalue)})),this}}return _exports.default})); //# sourceMappingURL=statemanager.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/reactive/statemanager.min.js.map b/lib/amd/build/local/reactive/statemanager.min.js.map index 3eb9a2fc03a..acb44d958a2 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,"file":"statemanager.min.js","sources":["../../../src/local/reactive/statemanager.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive simple state manager.\n *\n * The state manager contains the state data, trigger update events and\n * can lock and unlock the state data.\n *\n * This file contains the three main elements of the state manager:\n * - State manager: the public class to alter the state, dispatch events and process update messages.\n * - Proxy handler: a private class to keep track of the state object changes.\n * - StateMap class: a private class extending Map class that triggers event when a state list is modifed.\n *\n * @module core/local/reactive/statemanager\n * @class StateManager\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Logger from 'core/local/reactive/logger';\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 \"remove\": this.defaultRemove.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 this.logger = new Logger();\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 remove 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 defaultRemove(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n return;\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 * Set the logger class instance.\n *\n * Reactive instances can provide alternative loggers to provide advanced logging.\n * @param {Logger} logger\n */\n setLogger(logger) {\n this.logger = logger;\n }\n\n /**\n * Add a new log entry into the reactive logger.\n * @param {LoggerEntry} entry\n */\n addLoggerEntry(entry) {\n this.logger.add(entry);\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 * Get all element ids from the given state.\n *\n * @param {String} name the state object name\n * @return {Array} the element ids.\n */\n getIds(name) {\n const state = this.state;\n const current = state[name];\n if (!(current instanceof StateMap)) {\n throw Error(`${name} is not an instance of StateMap`);\n }\n return [...state[name].keys()];\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"],"names":["constructor","dispatchEvent","target","document","readonly","eventsToPublish","updateTypes","this","defaultCreate","bind","defaultUpdate","defaultDelete","defaultPut","defaultOverride","defaultRemove","defaultPrepareFields","initialPromise","Promise","resolve","addEventListener","event","detail","state","logger","Logger","setInitialState","initialState","undefined","Error","Proxy","Handler","prop","propValue","Object","entries","action","getInitialPromise","setReadOnly","mode","_publishEvents","element","addUpdateTypes","newFunctions","updateType","updateFunction","processUpdates","updates","Array","isArray","forEach","update","name","processUpdate","fields","updateName","method","prepareFields","stateManager","StateMap","add","get","id","delete","current","fieldName","fieldValue","setLogger","addLoggerEntry","entry","getIds","keys","registerStateAction","field","data","parentAction","push","eventName","eventData","fieldChanges","changes","sort","a","b","weights","created","updated","deleted","aweight","bweight","length","publishedEvents","Set","eventkey","has","proxyValues","set","obj","value","receiver","JSON","stringify","loadValues","deleteProperty","Map","iterable","key","normalizeKey","checkValue","super","result","String","valueOf","previous","toJSON","values","newvalue"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"statemanager.min.js","sources":["../../../src/local/reactive/statemanager.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive simple state manager.\n *\n * The state manager contains the state data, trigger update events and\n * can lock and unlock the state data.\n *\n * This file contains the three main elements of the state manager:\n * - State manager: the public class to alter the state, dispatch events and process update messages.\n * - Proxy handler: a private class to keep track of the state object changes.\n * - StateMap class: a private class extending Map class that triggers event when a state list is modifed.\n *\n * @module core/local/reactive/statemanager\n * @class StateManager\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Logger from 'core/local/reactive/logger';\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 \"remove\": this.defaultRemove.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 this.logger = new Logger();\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 remove 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 defaultRemove(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n return;\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 * Set the logger class instance.\n *\n * Reactive instances can provide alternative loggers to provide advanced logging.\n * @param {Logger} logger\n */\n setLogger(logger) {\n this.logger = logger;\n }\n\n /**\n * Add a new log entry into the reactive logger.\n * @param {LoggerEntry} entry\n */\n addLoggerEntry(entry) {\n this.logger.add(entry);\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 * Get all element ids from the given state.\n *\n * @param {String} name the state object name\n * @return {Array} the element ids.\n */\n getIds(name) {\n const state = this.state;\n const current = state[name];\n if (!(current instanceof StateMap)) {\n throw Error(`${name} is not an instance of StateMap`);\n }\n return [...state[name].keys()];\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 let transactionEvents = [];\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 transactionEvents.push(event);\n }\n });\n\n // Dispatch a transaction end event.\n this.dispatchEvent({\n action: 'transaction:end',\n state: this.state,\n element: null,\n changes: transactionEvents,\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"],"names":["constructor","dispatchEvent","target","document","readonly","eventsToPublish","updateTypes","this","defaultCreate","bind","defaultUpdate","defaultDelete","defaultPut","defaultOverride","defaultRemove","defaultPrepareFields","initialPromise","Promise","resolve","addEventListener","event","detail","state","logger","Logger","setInitialState","initialState","undefined","Error","Proxy","Handler","prop","propValue","Object","entries","action","getInitialPromise","setReadOnly","mode","_publishEvents","element","addUpdateTypes","newFunctions","updateType","updateFunction","processUpdates","updates","Array","isArray","forEach","update","name","processUpdate","fields","updateName","method","prepareFields","stateManager","StateMap","add","get","id","delete","current","fieldName","fieldValue","setLogger","addLoggerEntry","entry","getIds","keys","registerStateAction","field","data","parentAction","push","eventName","eventData","fieldChanges","changes","sort","a","b","weights","created","updated","deleted","aweight","bweight","length","publishedEvents","Set","transactionEvents","eventkey","has","proxyValues","set","obj","value","receiver","JSON","stringify","loadValues","deleteProperty","Map","iterable","key","normalizeKey","checkValue","super","result","String","valueOf","previous","toJSON","values","newvalue"],"mappings":"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"} \ No newline at end of file diff --git a/lib/amd/src/local/reactive/debug.js b/lib/amd/src/local/reactive/debug.js index 93594357747..7687cd99ba0 100644 --- a/lib/amd/src/local/reactive/debug.js +++ b/lib/amd/src/local/reactive/debug.js @@ -95,7 +95,7 @@ class Debug extends Reactive { const changes = detail?.changes; this.dispatch('lastTransaction', name, changes); }; - instance.target.addEventListener('transaction:start', storeTransaction); + instance.target.addEventListener('transaction:end', storeTransaction); } /** @@ -151,7 +151,7 @@ class Mutations { * Update the lastChanges attribute with a list of changes * * @param {StateManager} stateManager the debug reactive state - * @param {string} name tje instance name + * @param {string} name the instance name * @param {array} changes the list of changes */ lastTransaction(stateManager, name, changes) { @@ -170,6 +170,10 @@ class Mutations { stateManager.setReadOnly(false); + // Dirty hack to force the lastChanges:updated event to be dispatched. + state.reactives.get(name).lastChanges = []; + + // Assign the actual value. state.reactives.get(name).lastChanges = lastChanges; stateManager.setReadOnly(true); diff --git a/lib/amd/src/local/reactive/debugpanel.js b/lib/amd/src/local/reactive/debugpanel.js index f9c3c58909e..9a2aa85bdbe 100644 --- a/lib/amd/src/local/reactive/debugpanel.js +++ b/lib/amd/src/local/reactive/debugpanel.js @@ -303,7 +303,7 @@ class DebugInstanceSubpanel extends BaseComponent { } /** - * Wtacher method to refresh the log panel. + * Watcher method to refresh the log panel. * * @param {object} args * @param {HTMLElement} args.element @@ -311,10 +311,16 @@ class DebugInstanceSubpanel extends BaseComponent { _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}`; + if (target.value !== '') { + target.value += '\n\n'; + } + + const logContent = list.join("\n"); + + target.value += `= Transaction =\n${logContent}`; target.scrollTop = target.scrollHeight; } diff --git a/lib/amd/src/local/reactive/statemanager.js b/lib/amd/src/local/reactive/statemanager.js index 386502be7cd..d8e628877fc 100644 --- a/lib/amd/src/local/reactive/statemanager.js +++ b/lib/amd/src/local/reactive/statemanager.js @@ -605,6 +605,7 @@ export default class StateManager { // List of the published events to prevent redundancies. let publishedEvents = new Set(); + let transactionEvents = []; fieldChanges.forEach((event) => { @@ -618,6 +619,7 @@ export default class StateManager { }, this.target); publishedEvents.add(eventkey); + transactionEvents.push(event); } }); @@ -626,6 +628,7 @@ export default class StateManager { action: 'transaction:end', state: this.state, element: null, + changes: transactionEvents, }, this.target); } }