mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 16:32:18 +02:00
MDL-71995 core: reactive debug tools
This commit is contained in:
parent
9145d80b0b
commit
b58e780666
@ -140,6 +140,7 @@ $string['cliupgradefinished'] = 'Command line upgrade from {$a->oldversion} to {
|
||||
$string['cliupgradenoneed'] = 'No upgrade needed for the installed version {$a}. Thanks for coming anyway!';
|
||||
$string['cliupgradepending'] = 'An upgrade is pending';
|
||||
$string['cliyesnoprompt'] = 'type y (means yes) or n (means no)';
|
||||
$string['close'] = 'Close';
|
||||
$string['commentsperpage'] = 'Comments displayed per page';
|
||||
$string['commonactivitysettings'] = 'Common activity settings';
|
||||
$string['commonfiltersettings'] = 'Common filter settings';
|
||||
|
@ -52,6 +52,17 @@ $string['notables'] = 'No tables!';
|
||||
$string['outputbuffer'] = 'Output buffer';
|
||||
$string['phpvaroff'] = 'The PHP server variable \'{$a->name}\' should be Off - {$a->link}';
|
||||
$string['phpvaron'] = 'The PHP server variable \'{$a->name}\' is not turned On - {$a->link}';
|
||||
$string['reactive_instances'] = 'Reactive instances:';
|
||||
$string['reactive_noinstances'] = 'this page has no reactive instances';
|
||||
$string['reactive_pin'] = 'Pin';
|
||||
$string['reactive_unpin'] = 'Unpin';
|
||||
$string['reactive_highlightoff'] = 'Highlight OFF';
|
||||
$string['reactive_highlighton'] = 'Highlight ON';
|
||||
$string['reactive_readmodeon'] = 'Read mode ON';
|
||||
$string['reactive_readmodeoff'] = 'Read mode OFF';
|
||||
$string['reactive_resetpanel'] = 'Reset panel';
|
||||
$string['reactive_statedata'] = 'State data';
|
||||
$string['reactive_saveingwarning'] = 'Edit the state can cause inexpected results';
|
||||
$string['sessionmissing'] = '{$a} object missing from session';
|
||||
$string['sqlrelyonobsoletetable'] = 'This SQL relies on obsolete table(s): {$a}! Your code must be fixed by a developer.';
|
||||
$string['stacktrace'] = 'Stack trace';
|
||||
|
2
lib/amd/build/local/reactive/debug.min.js
vendored
Normal file
2
lib/amd/build/local/reactive/debug.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
lib/amd/build/local/reactive/debug.min.js.map
Normal file
1
lib/amd/build/local/reactive/debug.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
lib/amd/build/local/reactive/debugpanel.min.js
vendored
Normal file
2
lib/amd/build/local/reactive/debugpanel.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
lib/amd/build/local/reactive/debugpanel.min.js.map
Normal file
1
lib/amd/build/local/reactive/debugpanel.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
lib/amd/build/local/reactive/reactive.min.js
vendored
2
lib/amd/build/local/reactive/reactive.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
lib/amd/build/reactive.min.js
vendored
2
lib/amd/build/reactive.min.js
vendored
@ -1,2 +1,2 @@
|
||||
define ("core/reactive",["exports","core/local/reactive/basecomponent","core/local/reactive/reactive","core/local/reactive/dragdrop"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"BaseComponent",{enumerable:!0,get:function get(){return b.default}});Object.defineProperty(a,"Reactive",{enumerable:!0,get:function get(){return c.default}});Object.defineProperty(a,"DragDrop",{enumerable:!0,get:function get(){return d.default}});b=e(b);c=e(c);d=e(d);function e(a){return a&&a.__esModule?a:{default:a}}});
|
||||
define ("core/reactive",["exports","core/local/reactive/basecomponent","core/local/reactive/reactive","core/local/reactive/dragdrop","core/local/reactive/debug"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"BaseComponent",{enumerable:!0,get:function get(){return b.default}});Object.defineProperty(a,"Reactive",{enumerable:!0,get:function get(){return c.default}});Object.defineProperty(a,"DragDrop",{enumerable:!0,get:function get(){return d.default}});a.debug=void 0;b=f(b);c=f(c);d=f(d);function f(a){return a&&a.__esModule?a:{default:a}}var g;a.debug=g;if(M.cfg.developerdebug&&M.reactive===void 0){var h=(0,e.initDebug)();M.reactive=h.debuggers;a.debug=g=h.debug}});
|
||||
//# sourceMappingURL=reactive.min.js.map
|
||||
|
@ -1 +1 @@
|
||||
{"version":3,"sources":["../src/reactive.js"],"names":[],"mappings":"seAuBA,OACA,OACA,O","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Generic reactive module used in the course editor.\n *\n * @module core/reactive\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BaseComponent from 'core/local/reactive/basecomponent';\nimport Reactive from 'core/local/reactive/reactive';\nimport DragDrop from 'core/local/reactive/dragdrop';\n\nexport {Reactive, BaseComponent, DragDrop};\n"],"file":"reactive.min.js"}
|
||||
{"version":3,"sources":["../src/reactive.js"],"names":["debug","M","cfg","developerdebug","reactive","debugOBject","debuggers"],"mappings":"mhBAuBA,OACA,OACA,O,mDAIA,GAAIA,CAAAA,CAAJ,C,UACA,GAAIC,CAAC,CAACC,GAAF,CAAMC,cAAN,EAAwBF,CAAC,CAACG,QAAF,SAA5B,CAAsD,CAClD,GAAMC,CAAAA,CAAW,CAAG,iBAApB,CACAJ,CAAC,CAACG,QAAF,CAAaC,CAAW,CAACC,SAAzB,CACA,QAAAN,CAAK,CAAGK,CAAW,CAACL,KACvB,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Generic reactive module used in the course editor.\n *\n * @module core/reactive\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BaseComponent from 'core/local/reactive/basecomponent';\nimport Reactive from 'core/local/reactive/reactive';\nimport DragDrop from 'core/local/reactive/dragdrop';\nimport {initDebug} from 'core/local/reactive/debug';\n\n// Register a debug module if we are in debug mode.\nlet debug;\nif (M.cfg.developerdebug && M.reactive === undefined) {\n const debugOBject = initDebug();\n M.reactive = debugOBject.debuggers;\n debug = debugOBject.debug;\n}\n\nexport {Reactive, BaseComponent, DragDrop, debug};\n"],"file":"reactive.min.js"}
|
372
lib/amd/src/local/reactive/debug.js
Normal file
372
lib/amd/src/local/reactive/debug.js
Normal file
@ -0,0 +1,372 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Reactive module debug tools.
|
||||
*
|
||||
* @module core/reactive/local/reactive/debug
|
||||
* @copyright 2021 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import Reactive from 'core/local/reactive/reactive';
|
||||
import log from 'core/log';
|
||||
|
||||
// The list of reactives instances.
|
||||
const reactiveInstances = {};
|
||||
|
||||
// The reactive debugging objects.
|
||||
const reactiveDebuggers = {};
|
||||
|
||||
/**
|
||||
* Reactive module debug tools.
|
||||
*
|
||||
* If debug is enabled, this reactive module will spy all the reactive instances and keep a record
|
||||
* of the changes and components they have.
|
||||
*
|
||||
* It is important to note that the Debug class is also a Reactive module. The debug instance keeps
|
||||
* the reactive instances data as its own state. This way it is possible to implement development tools
|
||||
* that whatches this data.
|
||||
*
|
||||
* @class core/reactive/local/reactive/debug/Debug
|
||||
* @copyright 2021 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class Debug extends Reactive {
|
||||
|
||||
/**
|
||||
* Set the initial state.
|
||||
*
|
||||
* @param {object} stateData the initial state data.
|
||||
*/
|
||||
setInitialState(stateData) {
|
||||
super.setInitialState(stateData);
|
||||
log.debug(`Debug module "M.reactive" loaded.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* List the currents page reactives instances.
|
||||
*/
|
||||
get list() {
|
||||
return JSON.parse(JSON.stringify(this.state.reactives));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new Reactive instance.
|
||||
*
|
||||
* This method is called every time a "new Reactive" is executed.
|
||||
*
|
||||
* @param {Reactive} instance the reactive instance
|
||||
*/
|
||||
registerNewInstance(instance) {
|
||||
|
||||
// Generate a valid variable name for that instance.
|
||||
let name = instance.name ?? `instance${this.state.reactives.length}`;
|
||||
name = name.replace(/\W/g, '');
|
||||
|
||||
log.debug(`Registering new reactive instance "M.reactive.${name}"`);
|
||||
|
||||
reactiveInstances[name] = instance;
|
||||
reactiveDebuggers[name] = new DebugInstance(reactiveInstances[name]);
|
||||
// Register also in the state.
|
||||
this.dispatch('putInstance', name, instance);
|
||||
// Add debug watchers to instance.
|
||||
const refreshMethod = () => {
|
||||
this.dispatch('putInstance', name, instance);
|
||||
};
|
||||
instance.target.addEventListener('readmode:on', refreshMethod);
|
||||
instance.target.addEventListener('readmode:off', refreshMethod);
|
||||
instance.target.addEventListener('registerComponent:success', refreshMethod);
|
||||
instance.target.addEventListener('transaction:end', refreshMethod);
|
||||
// We store the last transaction into the state.
|
||||
const storeTransaction = ({detail}) => {
|
||||
const changes = detail?.changes;
|
||||
this.dispatch('lastTransaction', name, changes);
|
||||
};
|
||||
instance.target.addEventListener('transaction:start', storeTransaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a debugging object for a specific Reactive instance.
|
||||
*
|
||||
* A debugging object is a class that wraps a Reactive instance to quick access some of the
|
||||
* reactive methods using the browser JS console.
|
||||
*
|
||||
* @param {string} name the Reactive instance name
|
||||
* @returns {DebugInstance} a debug object wrapping the Reactive instance
|
||||
*/
|
||||
debug(name) {
|
||||
return reactiveDebuggers[name];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The debug state mutations class.
|
||||
*
|
||||
* @class core/reactive/local/reactive/debug/Mutations
|
||||
*/
|
||||
class Mutations {
|
||||
|
||||
/**
|
||||
* Insert or update a new instance into the debug state.
|
||||
*
|
||||
* @param {StateManager} stateManager the debug state manager
|
||||
* @param {string} name the instance name
|
||||
* @param {Reactive} instance the reactive instance
|
||||
*/
|
||||
putInstance(stateManager, name, instance) {
|
||||
const state = stateManager.state;
|
||||
|
||||
stateManager.setReadOnly(false);
|
||||
|
||||
if (state.reactives.has(name)) {
|
||||
state.reactives.get(name).countcomponents = instance.components.length;
|
||||
state.reactives.get(name).readOnly = instance.stateManager.readonly;
|
||||
state.reactives.get(name).modified = new Date().getTime();
|
||||
} else {
|
||||
state.reactives.add({
|
||||
id: name,
|
||||
countcomponents: instance.components.length,
|
||||
readOnly: instance.stateManager.readonly,
|
||||
lastChanges: [],
|
||||
modified: new Date().getTime(),
|
||||
});
|
||||
}
|
||||
stateManager.setReadOnly(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the lastChanges attribute with a list of changes
|
||||
*
|
||||
* @param {StateManager} stateManager the debug reactive state
|
||||
* @param {string} name tje instance name
|
||||
* @param {array} changes the list of changes
|
||||
*/
|
||||
lastTransaction(stateManager, name, changes) {
|
||||
if (!changes || changes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = stateManager.state;
|
||||
const lastChanges = ['transaction:start'];
|
||||
|
||||
changes.forEach(change => {
|
||||
lastChanges.push(change.eventName);
|
||||
});
|
||||
|
||||
lastChanges.push('transaction:end');
|
||||
|
||||
stateManager.setReadOnly(false);
|
||||
|
||||
state.reactives.get(name).lastChanges = lastChanges;
|
||||
|
||||
stateManager.setReadOnly(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class used to debug a specific instance and manipulate the state from the JS console.
|
||||
*
|
||||
* @class core/reactive/local/reactive/debug/DebugInstance
|
||||
* @copyright 2021 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class DebugInstance {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param {Reactive} instance the reactive instance
|
||||
*/
|
||||
constructor(instance) {
|
||||
this.instance = instance;
|
||||
// Add some debug data directly into the instance. This way we avoid having attributes
|
||||
// that will confuse the console aoutocomplete.
|
||||
if (instance._reactiveDebugData === undefined) {
|
||||
instance._reactiveDebugData = {
|
||||
highlighted: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the read only mode.
|
||||
*
|
||||
* Quick access to the instance setReadOnly method.
|
||||
*
|
||||
* @param {bool} value: the new read only value
|
||||
*/
|
||||
set readOnly(value) {
|
||||
this.instance.stateManager.setReadOnly(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the read only value
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
get readOnly() {
|
||||
return this.instance.stateManager.readonly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current state object.
|
||||
*
|
||||
* @return {object}
|
||||
*/
|
||||
get state() {
|
||||
return this.instance.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tooggle the reactive HTML element highlight registered in this reactive instance.
|
||||
*
|
||||
* @param {bool} value the highlight value
|
||||
*/
|
||||
set highlight(value) {
|
||||
this.instance._reactiveDebugData.highlighted = value;
|
||||
this.instance.components.forEach(({element}) => {
|
||||
const border = (value) ? `thick solid #0000FF` : '';
|
||||
element.style.border = border;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current highligh value.
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
get highlight() {
|
||||
return this.instance._reactiveDebugData.highlighted;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all the components registered in this instance.
|
||||
*
|
||||
* @return {array}
|
||||
*/
|
||||
get components() {
|
||||
return [...this.instance.components];
|
||||
}
|
||||
|
||||
/**
|
||||
* List all the state changes evenet pending to dispatch.
|
||||
*
|
||||
* @return {array}
|
||||
*/
|
||||
get changes() {
|
||||
const result = [];
|
||||
this.instance.stateManager.eventsToPublish.forEach(
|
||||
(element) => {
|
||||
result.push(element.eventName);
|
||||
}
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a change in the state.
|
||||
*
|
||||
* Usually reactive modules throw an error directly to the components when something
|
||||
* goes wrong. However, course editor can directly display a notification.
|
||||
*
|
||||
* @method dispatch
|
||||
* @param {string} actionName the action name (usually the mutation name)
|
||||
* @param {*} param any number of params the mutation needs.
|
||||
*/
|
||||
async dispatch(...args) {
|
||||
this.instance.dispatch(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the HTML elements registered in the instance components.
|
||||
*
|
||||
* @return {array}
|
||||
*/
|
||||
get elements() {
|
||||
const result = [];
|
||||
this.instance.components.forEach(({element}) => {
|
||||
result.push(element);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a plain copy of the state data.
|
||||
*
|
||||
* @return {object}
|
||||
*/
|
||||
get stateData() {
|
||||
return JSON.parse(JSON.stringify(this.state));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an update state array.
|
||||
*
|
||||
* @param {array} updates an array of update state messages
|
||||
*/
|
||||
processUpdates(updates) {
|
||||
this.instance.stateManager.processUpdates(updates);
|
||||
}
|
||||
}
|
||||
|
||||
const stateChangedEventName = 'core_reactive_debug:stateChanged';
|
||||
|
||||
/**
|
||||
* Internal state changed event.
|
||||
*
|
||||
* @method dispatchStateChangedEvent
|
||||
* @param {object} detail the full state
|
||||
* @param {object} target the custom event target (document if none provided)
|
||||
*/
|
||||
function dispatchStateChangedEvent(detail, target) {
|
||||
if (target === undefined) {
|
||||
target = document;
|
||||
}
|
||||
target.dispatchEvent(
|
||||
new CustomEvent(
|
||||
stateChangedEventName,
|
||||
{
|
||||
bubbles: true,
|
||||
detail: detail,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The main init method to initialize the reactive debug.
|
||||
* @returns {object}
|
||||
*/
|
||||
export const initDebug = () => {
|
||||
const debug = new Debug({
|
||||
name: 'CoreReactiveDebug',
|
||||
eventName: stateChangedEventName,
|
||||
eventDispatch: dispatchStateChangedEvent,
|
||||
mutations: new Mutations(),
|
||||
state: {
|
||||
reactives: [],
|
||||
},
|
||||
});
|
||||
|
||||
// The reactiveDebuggers will be used as a way of access the debug instances but also to register every new
|
||||
// instance. To ensure this will update the reactive debug state we add the registerNewInstance method to it.
|
||||
reactiveDebuggers.registerNewInstance = debug.registerNewInstance.bind(debug);
|
||||
|
||||
return {
|
||||
debug,
|
||||
debuggers: reactiveDebuggers,
|
||||
};
|
||||
};
|
563
lib/amd/src/local/reactive/debugpanel.js
Normal file
563
lib/amd/src/local/reactive/debugpanel.js
Normal file
@ -0,0 +1,563 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Reactive module debug panel.
|
||||
*
|
||||
* This module contains all the UI components for the reactive debug tools.
|
||||
* Those tools are only available if the debug is enables and could be used
|
||||
* from the footer.
|
||||
*
|
||||
* @module core/local/reactive/debugpanel
|
||||
* @copyright 2021 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import {BaseComponent, DragDrop, debug} from 'core/reactive';
|
||||
import log from 'core/log';
|
||||
import {debounce} from 'core/utils';
|
||||
|
||||
/**
|
||||
* Init the main reactive panel.
|
||||
*
|
||||
* @param {element|string} target the DOM main element or its ID
|
||||
* @param {object} selectors optional css selector overrides
|
||||
*/
|
||||
export const init = (target, selectors) => {
|
||||
const element = document.getElementById(target);
|
||||
// Check if the debug reactive module is available.
|
||||
if (debug === undefined) {
|
||||
element.remove();
|
||||
return;
|
||||
}
|
||||
// Create the main component.
|
||||
new GlobalDebugPanel({
|
||||
element,
|
||||
reactive: debug,
|
||||
selectors,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Init an instance reactive subpanel.
|
||||
*
|
||||
* @param {element|string} target the DOM main element or its ID
|
||||
* @param {object} selectors optional css selector overrides
|
||||
*/
|
||||
export const initsubpanel = (target, selectors) => {
|
||||
const element = document.getElementById(target);
|
||||
// Check if the debug reactive module is available.
|
||||
if (debug === undefined) {
|
||||
element.remove();
|
||||
return;
|
||||
}
|
||||
// Create the main component.
|
||||
new DebugInstanceSubpanel({
|
||||
element,
|
||||
reactive: debug,
|
||||
selectors,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Component for the main reactive dev panel.
|
||||
*
|
||||
* This component shows the list of reactive instances and handle the buttons
|
||||
* to open a specific instance panel.
|
||||
*/
|
||||
class GlobalDebugPanel extends BaseComponent {
|
||||
|
||||
/**
|
||||
* Constructor hook.
|
||||
*/
|
||||
create() {
|
||||
// Optional component name for debugging.
|
||||
this.name = 'GlobalDebugPanel';
|
||||
// Default query selectors.
|
||||
this.selectors = {
|
||||
LOADERS: `[data-for='loaders']`,
|
||||
SUBPANEL: `[data-for='subpanel']`,
|
||||
LOG: `[data-for='log']`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial state ready method.
|
||||
*
|
||||
* @param {object} state the initial state
|
||||
*/
|
||||
stateReady(state) {
|
||||
if (state.reactives.size > 0) {
|
||||
this.getElement(this.selectors.LOADERS).innerHTML = '';
|
||||
}
|
||||
// Generate loading buttons.
|
||||
state.reactives.forEach(
|
||||
instance => {
|
||||
this._createLoader(instance);
|
||||
}
|
||||
);
|
||||
// Remove loading wheel.
|
||||
this.getElement(this.selectors.SUBPANEL).innerHTML = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a debug panel button for a specific reactive instance.
|
||||
*
|
||||
* @param {object} instance hte instance data
|
||||
*/
|
||||
_createLoader(instance) {
|
||||
const loaders = this.getElement(this.selectors.LOADERS);
|
||||
const btn = document.createElement("button");
|
||||
btn.innerHTML = instance.id;
|
||||
btn.dataset.id = instance.id;
|
||||
loaders.appendChild(btn);
|
||||
// Add click event.
|
||||
this.addEventListener(btn, 'click', () => this._openPanel(btn, instance));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a debug panel.
|
||||
*
|
||||
* @param {Element} btn the button element
|
||||
* @param {object} instance the instance data
|
||||
*/
|
||||
async _openPanel(btn, instance) {
|
||||
try {
|
||||
const target = this.getElement(this.selectors.SUBPANEL);
|
||||
const data = {...instance};
|
||||
await this.renderComponent(target, 'core/local/reactive/debuginstancepanel', data);
|
||||
} catch (error) {
|
||||
log.error('Cannot load reactive debug subpanel');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for the main reactive dev panel.
|
||||
*
|
||||
* This component shows the list of reactive instances and handle the buttons
|
||||
* to open a specific instance panel.
|
||||
*/
|
||||
class DebugInstanceSubpanel extends BaseComponent {
|
||||
|
||||
/**
|
||||
* Constructor hook.
|
||||
*/
|
||||
create() {
|
||||
// Optional component name for debugging.
|
||||
this.name = 'DebugInstanceSubpanel';
|
||||
// Default query selectors.
|
||||
this.selectors = {
|
||||
NAME: `[data-for='name']`,
|
||||
CLOSE: `[data-for='close']`,
|
||||
READMODE: `[data-for='readmode']`,
|
||||
HIGHLIGHT: `[data-for='highlight']`,
|
||||
LOG: `[data-for='log']`,
|
||||
STATE: `[data-for='state']`,
|
||||
CLEAN: `[data-for='clean']`,
|
||||
PIN: `[data-for='pin']`,
|
||||
SAVE: `[data-for='save']`,
|
||||
INVALID: `[data-for='invalid']`,
|
||||
};
|
||||
this.id = this.element.dataset.id;
|
||||
this.controller = M.reactive[this.id];
|
||||
|
||||
// The component is created always pinned.
|
||||
this.draggable = false;
|
||||
// We want the element to be dragged like modal.
|
||||
this.relativeDrag = true;
|
||||
// Save warning (will be loaded when state is ready.
|
||||
this.strings = {
|
||||
savewarning: '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial state ready method.
|
||||
*
|
||||
*/
|
||||
stateReady() {
|
||||
// Enable drag and drop.
|
||||
this.dragdrop = new DragDrop(this);
|
||||
|
||||
// Close button.
|
||||
this.addEventListener(
|
||||
this.getElement(this.selectors.CLOSE),
|
||||
'click',
|
||||
this.remove
|
||||
);
|
||||
// Highlight button.
|
||||
if (this.controller.highlight) {
|
||||
this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT));
|
||||
}
|
||||
this.addEventListener(
|
||||
this.getElement(this.selectors.HIGHLIGHT),
|
||||
'click',
|
||||
() => {
|
||||
this.controller.highlight = !this.controller.highlight;
|
||||
this._toggleButtonText(this.getElement(this.selectors.HIGHLIGHT));
|
||||
}
|
||||
);
|
||||
// Edit mode button.
|
||||
this.addEventListener(
|
||||
this.getElement(this.selectors.READMODE),
|
||||
'click',
|
||||
this._toggleEditMode
|
||||
);
|
||||
// Clean log and state.
|
||||
this.addEventListener(
|
||||
this.getElement(this.selectors.CLEAN),
|
||||
'click',
|
||||
this._cleanAreas
|
||||
);
|
||||
// Unpin panel butotn.
|
||||
this.addEventListener(
|
||||
this.getElement(this.selectors.PIN),
|
||||
'click',
|
||||
this._togglePin
|
||||
);
|
||||
// Save button, state format error message and state textarea.
|
||||
this.getElement(this.selectors.SAVE).disabled = true;
|
||||
|
||||
this.addEventListener(
|
||||
this.getElement(this.selectors.STATE),
|
||||
'keyup',
|
||||
debounce(this._checkJSON, 500)
|
||||
);
|
||||
|
||||
this.addEventListener(
|
||||
this.getElement(this.selectors.SAVE),
|
||||
'click',
|
||||
this._saveState
|
||||
);
|
||||
// Save the default save warning message.
|
||||
this.strings.savewarning = this.getElement(this.selectors.INVALID)?.innerHTML ?? '';
|
||||
// Add current state.
|
||||
this._refreshState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all subcomponents dependencies.
|
||||
*/
|
||||
destroy() {
|
||||
if (this.dragdrop !== undefined) {
|
||||
this.dragdrop.unregister();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component watchers.
|
||||
*
|
||||
* @returns {Array} of watchers
|
||||
*/
|
||||
getWatchers() {
|
||||
return [
|
||||
{watch: `reactives[${this.id}].lastChanges:updated`, handler: this._refreshLog},
|
||||
{watch: `reactives[${this.id}].modified:updated`, handler: this._refreshState},
|
||||
{watch: `reactives[${this.id}].readOnly:updated`, handler: this._refreshReadOnly},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wtacher method to refresh the log panel.
|
||||
*
|
||||
* @param {object} detail of the change
|
||||
*/
|
||||
_refreshLog({element}) {
|
||||
const list = element?.lastChanges ?? [];
|
||||
|
||||
const logContent = list.join("\n");
|
||||
// Append last log.
|
||||
const target = this.getElement(this.selectors.LOG);
|
||||
target.value += `\n\n= Transaction =\n ${logContent}`;
|
||||
target.scrollTop = target.scrollHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener method to clean the log area.
|
||||
*/
|
||||
_cleanAreas() {
|
||||
let target = this.getElement(this.selectors.LOG);
|
||||
target.value = '';
|
||||
|
||||
this._refreshState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Watcher to refresh the state information.
|
||||
*/
|
||||
_refreshState() {
|
||||
const target = this.getElement(this.selectors.STATE);
|
||||
target.value = JSON.stringify(this.controller.state, null, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Watcher to update the read only information.
|
||||
*/
|
||||
_refreshReadOnly() {
|
||||
// Toggle the read mode button.
|
||||
const target = this.getElement(this.selectors.READMODE);
|
||||
if (target.dataset.readonly === undefined) {
|
||||
target.dataset.readonly = target.innerHTML;
|
||||
}
|
||||
if (this.controller.readOnly) {
|
||||
target.innerHTML = target.dataset.readonly;
|
||||
} else {
|
||||
target.innerHTML = target.dataset.alt;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener to toggle the edit mode of the component.
|
||||
*/
|
||||
_toggleEditMode() {
|
||||
this.controller.readOnly = !this.controller.readOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the edited state JSON is valid.
|
||||
*
|
||||
* Not all valid JSON are suitable for transforming the state. For example,
|
||||
* the first level attributes cannot change the type.
|
||||
*
|
||||
* @return {undefined|array} Array of state updates.
|
||||
*/
|
||||
_checkJSON() {
|
||||
const invalid = this.getElement(this.selectors.INVALID);
|
||||
const save = this.getElement(this.selectors.SAVE);
|
||||
|
||||
const edited = this.getElement(this.selectors.STATE).value;
|
||||
|
||||
const currentStateData = this.controller.stateData;
|
||||
|
||||
// Check if the json is tha same as state.
|
||||
if (edited == JSON.stringify(this.controller.state, null, 4)) {
|
||||
invalid.style.color = '';
|
||||
invalid.innerHTML = '';
|
||||
save.disabled = true;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check if the json format is valid.
|
||||
try {
|
||||
const newState = JSON.parse(edited);
|
||||
// Check the first level did not change types.
|
||||
const result = this._generateStateUpdates(currentStateData, newState);
|
||||
// Enable save button.
|
||||
invalid.style.color = '';
|
||||
invalid.innerHTML = this.strings.savewarning;
|
||||
save.disabled = false;
|
||||
return result;
|
||||
} catch (error) {
|
||||
invalid.style.color = 'red';
|
||||
invalid.innerHTML = error.message ?? 'Invalid JSON sctructure';
|
||||
save.disabled = true;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener to save the current edited state into the real state.
|
||||
*/
|
||||
_saveState() {
|
||||
const updates = this._checkJSON();
|
||||
if (!updates) {
|
||||
return;
|
||||
}
|
||||
// Sent the updates to the state manager.
|
||||
this.controller.processUpdates(updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the edited state JSON is valid.
|
||||
*
|
||||
* Not all valid JSON are suitable for transforming the state. For example,
|
||||
* the first level attributes cannot change the type. This method do a two
|
||||
* steps comparison between the current state data and the new state data.
|
||||
*
|
||||
* A reactive state cannot be overridden like any other variable. To keep
|
||||
* the watchers updated is necessary to transform the current state into
|
||||
* the new one. As a result, this method generates all the necessary state
|
||||
* updates to convert the state into the new state.
|
||||
*
|
||||
* @param {object} currentStateData
|
||||
* @param {object} newStateData
|
||||
* @return {array} Array of state updates.
|
||||
* @throws {Error} is the structure is not compatible
|
||||
*/
|
||||
_generateStateUpdates(currentStateData, newStateData) {
|
||||
|
||||
const updates = [];
|
||||
|
||||
const ids = {};
|
||||
|
||||
// Step 1: Add all overrides newStateData.
|
||||
for (const [key, newValue] of Object.entries(newStateData)) {
|
||||
// Check is it is new.
|
||||
if (Array.isArray(newValue)) {
|
||||
ids[key] = {};
|
||||
newValue.forEach(element => {
|
||||
if (element.id === undefined) {
|
||||
throw Error(`Array ${key} element without id attribute`);
|
||||
}
|
||||
updates.push({
|
||||
name: key,
|
||||
action: 'override',
|
||||
fields: element,
|
||||
});
|
||||
const index = String(element.id).valueOf();
|
||||
ids[key][index] = true;
|
||||
});
|
||||
} else {
|
||||
updates.push({
|
||||
name: key,
|
||||
action: 'override',
|
||||
fields: newValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Step 2: delete unnecesary data from currentStateData.
|
||||
for (const [key, oldValue] of Object.entries(currentStateData)) {
|
||||
let deleteField = false;
|
||||
// Check if the attribute is still there.
|
||||
if (newStateData[key] === undefined) {
|
||||
deleteField = true;
|
||||
}
|
||||
if (Array.isArray(oldValue)) {
|
||||
if (!deleteField && ids[key] === undefined) {
|
||||
throw Error(`Array ${key} cannot change to object.`);
|
||||
}
|
||||
oldValue.forEach(element => {
|
||||
const index = String(element.id).valueOf();
|
||||
let deleteEntry = deleteField;
|
||||
// Check if the id is there.
|
||||
if (!deleteEntry && ids[key][index] === undefined) {
|
||||
deleteEntry = true;
|
||||
}
|
||||
if (deleteEntry) {
|
||||
updates.push({
|
||||
name: key,
|
||||
action: 'delete',
|
||||
fields: element,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!deleteField && ids[key] !== undefined) {
|
||||
throw Error(`Object ${key} cannot change to array.`);
|
||||
}
|
||||
if (deleteField) {
|
||||
updates.push({
|
||||
name: key,
|
||||
action: 'delete',
|
||||
fields: oldValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delete all elements without action.
|
||||
return updates;
|
||||
}
|
||||
|
||||
// Drag and drop methods.
|
||||
|
||||
/**
|
||||
* Get the draggable data of this component.
|
||||
*
|
||||
* @returns {Object} exported course module drop data
|
||||
*/
|
||||
getDraggableData() {
|
||||
return this.draggable;
|
||||
}
|
||||
|
||||
/**
|
||||
* The element drop end hook.
|
||||
*
|
||||
* @param {Object} dropdata the dropdata
|
||||
* @param {Event} event the dropdata
|
||||
*/
|
||||
dragEnd(dropdata, event) {
|
||||
this.element.style.top = `${event.newFixedTop}px`;
|
||||
this.element.style.left = `${event.newFixedLeft}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin and unpin the panel.
|
||||
*/
|
||||
_togglePin() {
|
||||
this.draggable = !this.draggable;
|
||||
this.dragdrop.setDraggable(this.draggable);
|
||||
if (this.draggable) {
|
||||
this._unpin();
|
||||
} else {
|
||||
this._pin();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin the panel form the footer.
|
||||
*/
|
||||
_unpin() {
|
||||
// Find the initial spot.
|
||||
const pageCenterY = window.innerHeight / 2;
|
||||
const pageCenterX = window.innerWidth / 2;
|
||||
// Put the element in the middle of the screen
|
||||
const style = {
|
||||
position: 'fixed',
|
||||
resize: 'both',
|
||||
overflow: 'auto',
|
||||
height: '400px',
|
||||
width: '400px',
|
||||
top: `${pageCenterY - 200}px`,
|
||||
left: `${pageCenterX - 200}px`,
|
||||
};
|
||||
Object.assign(this.element.style, style);
|
||||
// Small also the text areas.
|
||||
this.getElement(this.selectors.STATE).style.height = '50px';
|
||||
this.getElement(this.selectors.LOG).style.height = '50px';
|
||||
|
||||
this._toggleButtonText(this.getElement(this.selectors.PIN));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin the panel into the footer.
|
||||
*/
|
||||
_pin() {
|
||||
const props = [
|
||||
'position',
|
||||
'resize',
|
||||
'overflow',
|
||||
'top',
|
||||
'left',
|
||||
'height',
|
||||
'width',
|
||||
];
|
||||
props.forEach(
|
||||
prop => this.element.style.removeProperty(prop)
|
||||
);
|
||||
this._toggleButtonText(this.getElement(this.selectors.PIN));
|
||||
}
|
||||
|
||||
/**
|
||||
* Toogle the button text with the data-alt value.
|
||||
*
|
||||
* @param {Element} element the button element
|
||||
*/
|
||||
_toggleButtonText(element) {
|
||||
[element.innerHTML, element.dataset.alt] = [element.dataset.alt, element.innerHTML];
|
||||
}
|
||||
|
||||
}
|
@ -109,6 +109,11 @@ export default class {
|
||||
if (description.state !== undefined) {
|
||||
this.setInitialState(description.state);
|
||||
}
|
||||
|
||||
// Check if we have a debug instance to register the instance.
|
||||
if (M.reactive !== undefined) {
|
||||
M.reactive.registerNewInstance(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -302,6 +307,12 @@ export default class {
|
||||
this.watchers.set(component, listeners);
|
||||
this.components.add(component);
|
||||
|
||||
// Dispatch an event to communicate the registration to the debug module.
|
||||
this.target.dispatchEvent(new CustomEvent('registerComponent:success', {
|
||||
bubbles: false,
|
||||
detail: {component},
|
||||
}));
|
||||
|
||||
dispatchSuccess();
|
||||
return component;
|
||||
}
|
||||
|
@ -180,10 +180,20 @@ export default class StateManager {
|
||||
|
||||
this.readonly = readonly;
|
||||
|
||||
let mode = 'off';
|
||||
|
||||
// When the state is in readonly again is time to publish all pending events.
|
||||
if (this.readonly) {
|
||||
mode = 'on';
|
||||
this._publishEvents();
|
||||
}
|
||||
|
||||
// Dispatch a read only event.
|
||||
this.dispatchEvent({
|
||||
action: `readmode:${mode}`,
|
||||
state: this.state,
|
||||
element: null,
|
||||
}, this.target);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -509,6 +519,7 @@ export default class StateManager {
|
||||
action: 'transaction:start',
|
||||
state: this.state,
|
||||
element: null,
|
||||
changes: fieldChanges,
|
||||
}, this.target);
|
||||
|
||||
// State changes can be registered in any order. However it will avoid many
|
||||
|
@ -24,5 +24,14 @@
|
||||
import BaseComponent from 'core/local/reactive/basecomponent';
|
||||
import Reactive from 'core/local/reactive/reactive';
|
||||
import DragDrop from 'core/local/reactive/dragdrop';
|
||||
import {initDebug} from 'core/local/reactive/debug';
|
||||
|
||||
export {Reactive, BaseComponent, DragDrop};
|
||||
// Register a debug module if we are in debug mode.
|
||||
let debug;
|
||||
if (M.cfg.developerdebug && M.reactive === undefined) {
|
||||
const debugOBject = initDebug();
|
||||
M.reactive = debugOBject.debuggers;
|
||||
debug = debugOBject.debug;
|
||||
}
|
||||
|
||||
export {Reactive, BaseComponent, DragDrop, debug};
|
||||
|
@ -873,6 +873,7 @@ class core_renderer extends renderer_base {
|
||||
$this->page->debug_summary()) . '</div>';
|
||||
}
|
||||
if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) { // Only in developer mode
|
||||
|
||||
// Add link to profiling report if necessary
|
||||
if (function_exists('profiling_is_running') && profiling_is_running()) {
|
||||
$txt = get_string('profiledscript', 'admin');
|
||||
@ -885,6 +886,9 @@ class core_renderer extends renderer_base {
|
||||
'sesskey' => sesskey(), 'returnurl' => $this->page->url->out_as_local_url(false)));
|
||||
$output .= '<div class="purgecaches">' .
|
||||
html_writer::link($purgeurl, get_string('purgecaches', 'admin')) . '</div>';
|
||||
|
||||
// Reactive module debug panel.
|
||||
$output .= $this->render_from_template('core/local/reactive/debugpanel', []);
|
||||
}
|
||||
if (!empty($CFG->debugvalidators)) {
|
||||
// NOTE: this is not a nice hack, $this->page->url is not always accurate and
|
||||
|
86
lib/templates/local/reactive/debuginstancepanel.mustache
Normal file
86
lib/templates/local/reactive/debuginstancepanel.mustache
Normal file
@ -0,0 +1,86 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/local/reactive/debuginstancepanel
|
||||
|
||||
Template to render the global reactive debug panel.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"id": "instanceid"
|
||||
}
|
||||
}}
|
||||
<div id="{{uniqid}}-reactive-debugpanel-instance" data-id="{{id}}" class="card text-dark">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" data-for="name">
|
||||
{{id}}
|
||||
<button href="#" class="btn btn-secondary" data-for="pin" data-alt="{{#str}} reactive_pin , core_debug {{/str}}">
|
||||
{{#str}} reactive_unpin , core_debug {{/str}}
|
||||
</button>
|
||||
<button href="#" class="btn btn-danger float-right" data-for="close">
|
||||
{{#str}} close , admin {{/str}}
|
||||
</button>
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
data-for="highlight"
|
||||
data-alt="{{#str}} reactive_highlighton , core_debug {{/str}}"
|
||||
>
|
||||
{{#str}} reactive_highlightoff , core_debug {{/str}}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
data-for="readmode"
|
||||
data-alt="{{#str}} reactive_readmodeoff , core_debug {{/str}}"
|
||||
>
|
||||
{{#str}} reactive_readmodeon , core_debug {{/str}}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
data-for="clean"
|
||||
>
|
||||
{{#str}} reactive_resetpanel , core_debug {{/str}}
|
||||
</button>
|
||||
</p>
|
||||
<div>
|
||||
<h6>
|
||||
{{#str}} reactive_statedata , core_debug {{/str}}
|
||||
<button class="btn btn-secondary" data-for="save" disabled>
|
||||
{{#str}} save , admin {{/str}}
|
||||
</button>
|
||||
<span data-for="invalid">{{#str}} reactive_saveingwarning , core_debug {{/str}}</span>
|
||||
</h6>
|
||||
<textarea class="w-100" style="resize:vertical; height: 20em;" data-for="state" draggable="false"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<h6>State events log</h6>
|
||||
<textarea class="w-100" style="resize:vertical; height: 20em;" data-for="log" draggable="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['core/local/reactive/debugpanel'], function(component) {
|
||||
component.initsubpanel('{{uniqid}}-reactive-debugpanel-instance');
|
||||
});
|
||||
{{/js}}
|
45
lib/templates/local/reactive/debugpanel.mustache
Normal file
45
lib/templates/local/reactive/debugpanel.mustache
Normal file
@ -0,0 +1,45 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/local/reactive/debugpanel
|
||||
|
||||
Template to render the global reactive debug panel.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
}
|
||||
}}
|
||||
<div id="{{uniqid}}-reactive-debugpanel" class="py-1">
|
||||
<div>
|
||||
{{#str}} reactive_instances , core_debug {{/str}}
|
||||
<span data-for="loaders">{{#str}} reactive_noinstances , core_debug {{/str}}</span>
|
||||
</div>
|
||||
<div data-for="subpanel">
|
||||
{{> core/loading }}
|
||||
</div>
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['core/local/reactive/debugpanel'], function(component) {
|
||||
component.init('{{uniqid}}-reactive-debugpanel');
|
||||
});
|
||||
{{/js}}
|
Loading…
x
Reference in New Issue
Block a user