Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
/**
17
 * Reactive module debug tools.
18
 *
19
 * @module     core/local/reactive/debug
20
 * @copyright  2021 Ferran Recio <ferran@moodle.com>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import Reactive from 'core/local/reactive/reactive';
25
import log from 'core/log';
26
 
27
// The list of reactives instances.
28
const reactiveInstances = {};
29
 
30
// The reactive debugging objects.
31
const reactiveDebuggers = {};
32
 
33
/**
34
 * Reactive module debug tools.
35
 *
36
 * If debug is enabled, this reactive module will spy all the reactive instances and keep a record
37
 * of the changes and components they have.
38
 *
39
 * It is important to note that the Debug class is also a Reactive module. The debug instance keeps
40
 * the reactive instances data as its own state. This way it is possible to implement development tools
41
 * that whatches this data.
42
 *
43
 * @class      core/reactive/local/reactive/debug/Debug
44
 * @copyright  2021 Ferran Recio <ferran@moodle.com>
45
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46
 */
47
class Debug extends Reactive {
48
 
49
    /**
50
     * Set the initial state.
51
     *
52
     * @param {object} stateData the initial state data.
53
     */
54
    setInitialState(stateData) {
55
        super.setInitialState(stateData);
56
        log.debug(`Debug module "M.reactive" loaded.`);
57
    }
58
 
59
    /**
60
     * List the currents page reactives instances.
61
     */
62
    get list() {
63
        return JSON.parse(JSON.stringify(this.state.reactives));
64
    }
65
 
66
    /**
67
     * Register a new Reactive instance.
68
     *
69
     * This method is called every time a "new Reactive" is executed.
70
     *
71
     * @param {Reactive} instance the reactive instance
72
     */
73
    registerNewInstance(instance) {
74
 
75
        // Generate a valid variable name for that instance.
76
        let name = instance.name ?? `instance${this.state.reactives.length}`;
77
        name = name.replace(/\W/g, '');
78
 
79
        log.debug(`Registering new reactive instance "M.reactive.${name}"`);
80
 
81
        reactiveInstances[name] = instance;
82
        reactiveDebuggers[name] = new DebugInstance(reactiveInstances[name]);
83
        // Register also in the state.
84
        this.dispatch('putInstance', name, instance);
85
        // Add debug watchers to instance.
86
        const refreshMethod = () => {
87
            this.dispatch('putInstance', name, instance);
88
        };
89
        instance.target.addEventListener('readmode:on', refreshMethod);
90
        instance.target.addEventListener('readmode:off', refreshMethod);
91
        instance.target.addEventListener('registerComponent:success', refreshMethod);
92
        instance.target.addEventListener('transaction:end', refreshMethod);
93
        // We store the last transaction into the state.
94
        const storeTransaction = ({detail}) => {
95
            const changes = detail?.changes;
96
            this.dispatch('lastTransaction', name, changes);
97
        };
1441 ariadna 98
        instance.target.addEventListener('transaction:end', storeTransaction);
1 efrain 99
    }
100
 
101
    /**
102
     * Returns a debugging object for a specific Reactive instance.
103
     *
104
     * A debugging object is a class that wraps a Reactive instance to quick access some of the
105
     * reactive methods using the browser JS console.
106
     *
107
     * @param {string} name the Reactive instance name
108
     * @returns {DebugInstance} a debug object wrapping the Reactive instance
109
     */
110
    debug(name) {
111
        return reactiveDebuggers[name];
112
    }
113
}
114
 
115
/**
116
 * The debug state mutations class.
117
 *
118
 * @class core/reactive/local/reactive/debug/Mutations
119
 */
120
class Mutations {
121
 
122
    /**
123
     * Insert or update a new instance into the debug state.
124
     *
125
     * @param {StateManager} stateManager the debug state manager
126
     * @param {string} name the instance name
127
     * @param {Reactive} instance the reactive instance
128
     */
129
    putInstance(stateManager, name, instance) {
130
        const state = stateManager.state;
131
 
132
        stateManager.setReadOnly(false);
133
 
134
        if (state.reactives.has(name)) {
135
            state.reactives.get(name).countcomponents = instance.components.length;
136
            state.reactives.get(name).readOnly = instance.stateManager.readonly;
137
            state.reactives.get(name).modified = new Date().getTime();
138
        } else {
139
            state.reactives.add({
140
                id: name,
141
                countcomponents: instance.components.length,
142
                readOnly: instance.stateManager.readonly,
143
                lastChanges: [],
144
                modified: new Date().getTime(),
145
            });
146
        }
147
        stateManager.setReadOnly(true);
148
    }
149
 
150
    /**
151
     * Update the lastChanges attribute with a list of changes
152
     *
153
     * @param {StateManager} stateManager the debug reactive state
1441 ariadna 154
     * @param {string} name the instance name
1 efrain 155
     * @param {array} changes the list of changes
156
     */
157
    lastTransaction(stateManager, name, changes) {
158
        if (!changes || changes.length === 0) {
159
            return;
160
        }
161
 
162
        const state = stateManager.state;
163
        const lastChanges = ['transaction:start'];
164
 
165
        changes.forEach(change => {
166
            lastChanges.push(change.eventName);
167
        });
168
 
169
        lastChanges.push('transaction:end');
170
 
171
        stateManager.setReadOnly(false);
172
 
1441 ariadna 173
        // Dirty hack to force the lastChanges:updated event to be dispatched.
174
        state.reactives.get(name).lastChanges = [];
175
 
176
        // Assign the actual value.
1 efrain 177
        state.reactives.get(name).lastChanges = lastChanges;
178
 
179
        stateManager.setReadOnly(true);
180
    }
181
}
182
 
183
/**
184
 * Class used to debug a specific instance and manipulate the state from the JS console.
185
 *
186
 * @class      core/reactive/local/reactive/debug/DebugInstance
187
 * @copyright  2021 Ferran Recio <ferran@moodle.com>
188
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
189
 */
190
class DebugInstance {
191
 
192
    /**
193
     * Constructor.
194
     *
195
     * @param {Reactive} instance the reactive instance
196
     */
197
    constructor(instance) {
198
        this.instance = instance;
199
        // Add some debug data directly into the instance. This way we avoid having attributes
200
        // that will confuse the console aoutocomplete.
201
        if (instance._reactiveDebugData === undefined) {
202
            instance._reactiveDebugData = {
203
                highlighted: false,
204
            };
205
        }
206
    }
207
 
208
    /**
209
     * Set the read only mode.
210
     *
211
     * Quick access to the instance setReadOnly method.
212
     *
213
     * @param {bool} value the new read only value
214
     */
215
    set readOnly(value) {
216
        this.instance.stateManager.setReadOnly(value);
217
    }
218
 
219
    /**
220
     * Get the read only value
221
     *
222
     * @returns {bool}
223
     */
224
    get readOnly() {
225
        return this.instance.stateManager.readonly;
226
    }
227
 
228
    /**
229
     * Return the current state object.
230
     *
231
     * @returns {object}
232
     */
233
    get state() {
234
        return this.instance.state;
235
    }
236
 
237
    /**
238
     * Tooggle the reactive HTML element highlight registered in this reactive instance.
239
     *
240
     * @param {bool} value the highlight value
241
     */
242
    set highlight(value) {
243
        this.instance._reactiveDebugData.highlighted = value;
244
        this.instance.components.forEach(({element}) => {
245
            const border = (value) ? `thick solid #0000FF` : '';
246
            element.style.border = border;
247
        });
248
    }
249
 
250
    /**
251
     * Get the current highligh value.
252
     *
253
     * @returns {bool}
254
     */
255
    get highlight() {
256
        return this.instance._reactiveDebugData.highlighted;
257
    }
258
 
259
    /**
260
     * List all the components registered in this instance.
261
     *
262
     * @returns {array}
263
     */
264
    get components() {
265
        return [...this.instance.components];
266
    }
267
 
268
    /**
269
     * List all the state changes evenet pending to dispatch.
270
     *
271
     * @returns {array}
272
     */
273
    get changes() {
274
        const result = [];
275
        this.instance.stateManager.eventsToPublish.forEach(
276
            (element) => {
277
                result.push(element.eventName);
278
            }
279
        );
280
        return result;
281
    }
282
 
283
    /**
284
     * Dispatch a change in the state.
285
     *
286
     * Usually reactive modules throw an error directly to the components when something
287
     * goes wrong. However, course editor can directly display a notification.
288
     *
289
     * @method dispatch
290
     * @param {*} args
291
     */
292
    async dispatch(...args) {
293
        this.instance.dispatch(...args);
294
    }
295
 
296
    /**
297
     * Return all the HTML elements registered in the instance components.
298
     *
299
     * @returns {array}
300
     */
301
    get elements() {
302
        const result = [];
303
        this.instance.components.forEach(({element}) => {
304
            result.push(element);
305
        });
306
        return result;
307
    }
308
 
309
    /**
310
     * Return a plain copy of the state data.
311
     *
312
     * @returns {object}
313
     */
314
    get stateData() {
315
        return JSON.parse(JSON.stringify(this.state));
316
    }
317
 
318
    /**
319
     * Process an update state array.
320
     *
321
     * @param {array} updates an array of update state messages
322
     */
323
    processUpdates(updates) {
324
        this.instance.stateManager.processUpdates(updates);
325
    }
326
}
327
 
328
const stateChangedEventName = 'core_reactive_debug:stateChanged';
329
 
330
/**
331
 * Internal state changed event.
332
 *
333
 * @method dispatchStateChangedEvent
334
 * @param {object} detail the full state
335
 * @param {object} target the custom event target (document if none provided)
336
 */
337
function dispatchStateChangedEvent(detail, target) {
338
    if (target === undefined) {
339
        target = document;
340
    }
341
    target.dispatchEvent(
342
        new CustomEvent(
343
            stateChangedEventName,
344
            {
345
                bubbles: true,
346
                detail: detail,
347
            }
348
        )
349
    );
350
}
351
 
352
/**
353
 * The main init method to initialize the reactive debug.
354
 * @returns {object}
355
 */
356
export const initDebug = () => {
357
    const debug = new Debug({
358
        name: 'CoreReactiveDebug',
359
        eventName: stateChangedEventName,
360
        eventDispatch: dispatchStateChangedEvent,
361
        mutations: new Mutations(),
362
        state: {
363
            reactives: [],
364
        },
365
    });
366
 
367
    // The reactiveDebuggers will be used as a way of access the debug instances but also to register every new
368
    // instance. To ensure this will update the reactive debug state we add the registerNewInstance method to it.
369
    reactiveDebuggers.registerNewInstance = debug.registerNewInstance.bind(debug);
370
 
371
    return {
372
        debug,
373
        debuggers: reactiveDebuggers,
374
    };
375
};