diff --git a/lib/amd/build/local/reactive/debugpanel.min.js b/lib/amd/build/local/reactive/debugpanel.min.js index 4a6c8ab5bbd..82de966570b 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']",LOG:"[data-for='log']"}}stateReady(state){state.reactives.size>0&&(this.getElement(this.selectors.LOADERS).innerHTML=""),state.reactives.forEach((instance=>{this._createLoader(instance)})),this.getElement(this.selectors.SUBPANEL).innerHTML=""}_createLoader(instance){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$getElement;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,500)),this.addEventListener(this.getElement(this.selectors.SAVE),"click",this._saveState),this.strings.savewarning=null!==(_this$getElement$inne=null===(_this$getElement=this.getElement(this.selectors.INVALID))||void 0===_this$getElement?void 0:_this$getElement.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(_ref){var _element$lastChanges;let{element:element}=_ref;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,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]}}})); //# 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 7fcbdae461d..b69f3f65440 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 LOG: `[data-for='log']`,\n };\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n stateReady(state) {\n if (state.reactives.size > 0) {\n this.getElement(this.selectors.LOADERS).innerHTML = '';\n }\n // Generate loading buttons.\n state.reactives.forEach(\n instance => {\n this._createLoader(instance);\n }\n );\n // Remove loading wheel.\n this.getElement(this.selectors.SUBPANEL).innerHTML = '';\n }\n\n /**\n * Create a debug panel button for a specific reactive instance.\n *\n * @param {object} instance hte instance data\n */\n _createLoader(instance) {\n const loaders = this.getElement(this.selectors.LOADERS);\n const btn = document.createElement(\"button\");\n btn.innerHTML = instance.id;\n btn.dataset.id = instance.id;\n loaders.appendChild(btn);\n // Add click event.\n this.addEventListener(btn, 'click', () => this._openPanel(btn, instance));\n }\n\n /**\n * Open a debug panel.\n *\n * @param {Element} btn the button element\n * @param {object} instance the instance data\n */\n async _openPanel(btn, instance) {\n try {\n const target = this.getElement(this.selectors.SUBPANEL);\n const data = {...instance};\n await this.renderComponent(target, 'core/local/reactive/debuginstancepanel', data);\n } catch (error) {\n log.error('Cannot load reactive debug subpanel');\n throw error;\n }\n }\n}\n\n/**\n * Component for the main reactive dev panel.\n *\n * This component shows the list of reactive instances and handle the buttons\n * to open a specific instance panel.\n */\nclass DebugInstanceSubpanel extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'DebugInstanceSubpanel';\n // Default query selectors.\n this.selectors = {\n NAME: `[data-for='name']`,\n CLOSE: `[data-for='close']`,\n READMODE: `[data-for='readmode']`,\n HIGHLIGHT: `[data-for='highlight']`,\n LOG: `[data-for='log']`,\n STATE: `[data-for='state']`,\n CLEAN: `[data-for='clean']`,\n PIN: `[data-for='pin']`,\n SAVE: `[data-for='save']`,\n INVALID: `[data-for='invalid']`,\n };\n this.id = this.element.dataset.id;\n this.controller = M.reactive[this.id];\n\n // The component is created always pinned.\n this.draggable = false;\n // We want the element to be dragged like modal.\n this.relativeDrag = true;\n // Save warning (will be loaded when state is ready.\n this.strings = {\n savewarning: '',\n };\n }\n\n /**\n * Initial state ready method.\n *\n */\n stateReady() {\n // Enable drag and drop.\n this.dragdrop = new DragDrop(this);\n\n // Close button.\n this.addEventListener(\n this.getElement(this.selectors.CLOSE),\n 'click',\n this.remove\n );\n // Highlight button.\n if (this.controller.highlight) {\n this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT));\n }\n this.addEventListener(\n this.getElement(this.selectors.HIGHLIGHT),\n 'click',\n () => {\n this.controller.highlight = !this.controller.highlight;\n this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT));\n }\n );\n // Edit mode button.\n this.addEventListener(\n this.getElement(this.selectors.READMODE),\n 'click',\n this._toggleEditMode\n );\n // Clean log and state.\n this.addEventListener(\n this.getElement(this.selectors.CLEAN),\n 'click',\n this._cleanAreas\n );\n // Unpin panel butotn.\n this.addEventListener(\n this.getElement(this.selectors.PIN),\n 'click',\n this._togglePin\n );\n // Save button, state format error message and state textarea.\n this.getElement(this.selectors.SAVE).disabled = true;\n\n this.addEventListener(\n this.getElement(this.selectors.STATE),\n 'keyup',\n debounce(this._checkJSON, 500)\n );\n\n this.addEventListener(\n this.getElement(this.selectors.SAVE),\n 'click',\n this._saveState\n );\n // Save the default save warning message.\n this.strings.savewarning = this.getElement(this.selectors.INVALID)?.innerHTML ?? '';\n // Add current state.\n this._refreshState();\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `reactives[${this.id}].lastChanges:updated`, handler: this._refreshLog},\n {watch: `reactives[${this.id}].modified:updated`, handler: this._refreshState},\n {watch: `reactives[${this.id}].readOnly:updated`, handler: this._refreshReadOnly},\n ];\n }\n\n /**\n * Wtacher method to refresh the log panel.\n *\n * @param {object} 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","LOG","stateReady","state","reactives","size","getElement","this","innerHTML","forEach","instance","_createLoader","loaders","btn","createElement","id","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","_saveState","_this$getElement","_refreshState","destroy","unregister","getWatchers","watch","handler","_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, 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","_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 diff --git a/lib/amd/src/local/reactive/debugpanel.js b/lib/amd/src/local/reactive/debugpanel.js index 64f6ecf4603..738ad6bbad3 100644 --- a/lib/amd/src/local/reactive/debugpanel.js +++ b/lib/amd/src/local/reactive/debugpanel.js @@ -89,8 +89,14 @@ class GlobalDebugPanel extends BaseComponent { this.selectors = { LOADERS: `[data-for='loaders']`, SUBPANEL: `[data-for='subpanel']`, + NOINSTANCES: `[data-for='noinstances']`, LOG: `[data-for='log']`, }; + this.classes = { + HIDE: `d-none`, + }; + // The list of loaded debuggers. + this.subPanels = new Set(); } /** @@ -99,17 +105,38 @@ class GlobalDebugPanel extends BaseComponent { * @param {object} state the initial state */ stateReady(state) { - if (state.reactives.size > 0) { - this.getElement(this.selectors.LOADERS).innerHTML = ''; - } + this._updateReactivesPanels({state}); + // Remove loading wheel. + this.getElement(this.selectors.SUBPANEL).innerHTML = ''; + } + + /** + * Component watchers. + * + * @returns {Array} of watchers + */ + getWatchers() { + return [ + {watch: `reactives:created`, handler: this._updateReactivesPanels}, + ]; + } + + /** + * Update the list of reactive instances. + * @param {Object} args + * @param {Object} args.state the current state + */ + _updateReactivesPanels({state}) { + this.getElement(this.selectors.NOINSTANCES)?.classList?.toggle( + this.classes.HIDE, + state.reactives.size > 0 + ); // Generate loading buttons. state.reactives.forEach( instance => { this._createLoader(instance); } ); - // Remove loading wheel. - this.getElement(this.selectors.SUBPANEL).innerHTML = ''; } /** @@ -118,6 +145,10 @@ class GlobalDebugPanel extends BaseComponent { * @param {object} instance hte instance data */ _createLoader(instance) { + if (this.subPanels.has(instance.id)) { + return; + } + this.subPanels.add(instance.id); const loaders = this.getElement(this.selectors.LOADERS); const btn = document.createElement("button"); btn.innerHTML = instance.id; diff --git a/lib/templates/local/reactive/debugpanel.mustache b/lib/templates/local/reactive/debugpanel.mustache index 2b015f1fec8..7b726e6c127 100644 --- a/lib/templates/local/reactive/debugpanel.mustache +++ b/lib/templates/local/reactive/debugpanel.mustache @@ -32,7 +32,11 @@
{{#str}} reactive_instances , core_debug {{/str}} - {{#str}} reactive_noinstances , core_debug {{/str}} + + + {{#str}} reactive_noinstances , core_debug {{/str}} + +
{{> core/loading }}