AutorÃa | Ultima modificación | Ver Log |
{"version":3,"file":"reactive.min.js","sources":["../../../src/local/reactive/reactive.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 <http://www.gnu.org/licenses/>.\n\n/**\n * A generic single state reactive module.\n *\n * @module core/local/reactive/reactive\n * @class core/local/reactive/reactive\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license h
ttp://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport log from 'core/log';\nimport StateManager from 'core/local/reactive/statemanager';\nimport Pending from 'core/pending';\n\n// Count the number of pending operations done to ensure we have a unique id for each one.\nlet pendingCount = 0;\n\n/**\n * Set up general reactive class to create a single state application with components.\n *\n * The reactive class is used for registering new UI components and manage the access to the state values\n * and mutations.\n *\n * When a new reactive instance is created, it will contain an empty state and and empty mutations\n * lists. When the state data is ready, the initial state can be loaded using the \"setInitialState\"\n * method. This will protect the state from writing and will trigger all the components \"stateReady\"\n * methods.\n *\n * State can only be altered by mutations. To replace all the mutations with a specific class,\n * use \"setMutations\" method. If you need to just add some new
mutation methods, use \"addMutations\".\n *\n * To register new components into a reactive instance, use \"registerComponent\".\n *\n * Inside a component, use \"dispatch\" to invoke a mutation on the state (components can only access\n * the state in read only mode).\n */\nexport default class {\n\n /**\n * The component descriptor data structure.\n *\n * @typedef {object} description\n * @property {string} eventName the custom event name used for state changed events\n * @property {Function} eventDispatch the state update event dispatch function\n * @property {Element} [target] the target of the event dispatch. If not passed a fake element will be created\n * @property {Object} [mutations] an object with state mutations functions\n * @property {Object} [state] an object to initialize the state.\n */\n\n /**\n * Create a basic reactive manager.\n *\n * Note that if your state is not async loaded, you can pass directly on creation by using the\n * des
cription.state attribute. However, this will initialize the state, this means\n * setInitialState will throw an exception because the state is already defined.\n *\n * @param {description} description reactive manager description.\n */\n constructor(description) {\n\n if (description.eventName === undefined || description.eventDispatch === undefined) {\n throw new Error(`Reactivity event required`);\n }\n\n if (description.name !== undefined) {\n this.name = description.name;\n }\n\n // Each reactive instance has its own element anchor to propagate state changes internally.\n // By default the module will create a fake DOM element to target custom events but\n // if all reactive components is constrait to a single element, this can be passed as\n // target in the description.\n this.target = description.target ?? document.createTextNode(null);\n\n this.eventName = description.eventName;\n thi
s.eventDispatch = description.eventDispatch;\n\n // State manager is responsible for dispatch state change events when a mutation happens.\n this.stateManager = new StateManager(this.eventDispatch, this.target);\n\n // An internal registry of watchers and components.\n this.watchers = new Map([]);\n this.components = new Set([]);\n\n // Mutations can be overridden later using setMutations method.\n this.mutations = description.mutations ?? {};\n\n // Register the event to alert watchers when specific state change happens.\n this.target.addEventListener(this.eventName, this.callWatchersHandler.bind(this));\n\n // Add a pending operation waiting for the initial state.\n this.pendingState = new Pending(`core/reactive:registerInstance${pendingCount++}`);\n\n // Set initial state if we already have it.\n if (description.state !== undefined) {\n this.setInitialState(description.state);\n }\n\n // Che
ck if we have a debug instance to register the instance.\n if (M.reactive !== undefined) {\n M.reactive.registerNewInstance(this);\n }\n }\n\n /**\n * State changed listener.\n *\n * This function take any state change and send it to the proper watchers.\n *\n * To prevent internal state changes from colliding with other reactive instances, only the\n * general \"state changed\" is triggered at document level. All the internal changes are\n * triggered at private target level without bubbling. This way any reactive instance can alert\n * only its own watchers.\n *\n * @param {CustomEvent} event\n */\n callWatchersHandler(event) {\n // Execute any registered component watchers.\n this.target.dispatchEvent(new CustomEvent(event.detail.action, {\n bubbles: false,\n detail: event.detail,\n }));\n }\n\n /**\n * Set the initial state.\n *\n * @param {object} stateData the initi
al state data.\n */\n setInitialState(stateData) {\n this.pendingState.resolve();\n this.stateManager.setInitialState(stateData);\n }\n\n /**\n * Add individual functions to the mutations.\n *\n * Note new mutations will be added to the existing ones. To replace the full mutation\n * object with a new one, use setMutations method.\n *\n * @method addMutations\n * @param {Object} newFunctions an object with new mutation functions.\n */\n addMutations(newFunctions) {\n // Mutations can provide an init method to do some setup in the statemanager.\n if (newFunctions.init !== undefined) {\n newFunctions.init(this.stateManager);\n }\n // Save all mutations.\n for (const [mutation, mutationFunction] of Object.entries(newFunctions)) {\n this.mutations[mutation] = mutationFunction.bind(newFunctions);\n }\n }\n\n /**\n * Replace the current mutations with a new object.\n *\n *
This method is designed to override the full mutations class, for example by extending\n * the original one. To add some individual mutations, use addMutations instead.\n *\n * @param {object} manager the new mutations intance\n */\n setMutations(manager) {\n this.mutations = manager;\n // Mutations can provide an init method to do some setup in the statemanager.\n if (manager.init !== undefined) {\n manager.init(this.stateManager);\n }\n }\n\n /**\n * Return the current state.\n *\n * @return {object}\n */\n get state() {\n return this.stateManager.state;\n }\n\n /**\n * Get state data.\n *\n * Components access the state frequently. This convenience method is a shortcut to\n * this.reactive.state.stateManager.get() method.\n *\n * @param {String} name the state object name\n * @param {*} id an optional object id for state maps.\n * @return {Object|undefined} the state object foun
d\n */\n get(name, id) {\n return this.stateManager.get(name, id);\n }\n\n /**\n * Return the initial state promise.\n *\n * Typically, components do not require to use this promise because registerComponent\n * will trigger their stateReady method automatically. But it could be useful for complex\n * components that require to combine state, template and string loadings.\n *\n * @method getState\n * @return {Promise}\n */\n getInitialStatePromise() {\n return this.stateManager.getInitialPromise();\n }\n\n /**\n * Register a new component.\n *\n * Component can provide some optional functions to the reactive module:\n * - getWatchers: returns an array of watchers\n * - stateReady: a method to call when the initial state is loaded\n *\n * It can also provide some optional attributes:\n * - name: the component name (default value: \"Unkown component\") to customize debug messages.\n *\n * The method
will also use dispatchRegistrationSuccess and dispatchRegistrationFail. Those\n * are BaseComponent methods to inform parent components of the registration status.\n * Components should not override those methods.\n *\n * @method registerComponent\n * @param {object} component the new component\n * @param {string} [component.name] the component name to display in warnings and errors.\n * @param {Function} [component.dispatchRegistrationSuccess] method to notify registration success\n * @param {Function} [component.dispatchRegistrationFail] method to notify registration fail\n * @param {Function} [component.getWatchers] getter of the component watchers\n * @param {Function} [component.stateReady] method to call when the state is ready\n * @return {object} the registered component\n */\n registerComponent(component) {\n\n // Component name is an optional attribute to customize debug messages.\n const componentName = component.name ?? 'Unkown comp
onent';\n\n // Components can provide special methods to communicate registration to parent components.\n let dispatchSuccess = () => {\n return;\n };\n let dispatchFail = dispatchSuccess;\n if (component.dispatchRegistrationSuccess !== undefined) {\n dispatchSuccess = component.dispatchRegistrationSuccess.bind(component);\n }\n if (component.dispatchRegistrationFail !== undefined) {\n dispatchFail = component.dispatchRegistrationFail.bind(component);\n }\n\n // Components can be registered only one time.\n if (this.components.has(component)) {\n dispatchSuccess();\n return component;\n }\n\n // Components are fully registered only when the state ready promise is resolved.\n const pendingPromise = new Pending(`core/reactive:registerComponent${pendingCount++}`);\n\n // Keep track of the event listeners.\n let listeners = [];\n\n // Register watc
hers.\n let handlers = [];\n if (component.getWatchers !== undefined) {\n handlers = component.getWatchers();\n }\n handlers.forEach(({watch, handler}) => {\n\n if (watch === undefined) {\n dispatchFail();\n throw new Error(`Missing watch attribute in ${componentName} watcher`);\n }\n if (handler === undefined) {\n dispatchFail();\n throw new Error(`Missing handler for watcher ${watch} in ${componentName}`);\n }\n\n const listener = (event) => {\n // Prevent any watcher from losing the page focus.\n const currentFocus = document.activeElement;\n // Execute watcher.\n handler.apply(component, [event.detail]);\n // Restore focus in case it is lost.\n if (document.activeElement === document.body && document.body.contains(currentFocus)) {\n currentFocus.
focus();\n }\n };\n\n // Save the listener information in case the component must be unregistered later.\n listeners.push({target: this.target, watch, listener});\n\n // The state manager triggers a general \"state changed\" event at a document level. However,\n // for the internal watchers, each component can listen to specific state changed custom events\n // in the target element. This way we can use the native event loop without colliding with other\n // reactive instances.\n this.target.addEventListener(watch, listener);\n });\n\n // Register state ready function. There's the possibility a component is registered after the initial state\n // is loaded. For those cases we have a state promise to handle this specific state change.\n if (component.stateReady !== undefined) {\n this.getInitialStatePromise()\n .then(state => {\n compon
ent.stateReady(state);\n pendingPromise.resolve();\n return true;\n })\n .catch(reason => {\n pendingPromise.resolve();\n log.error(`Initial state in ${componentName} rejected due to: ${reason}`);\n log.error(reason);\n });\n }\n\n // Save unregister data.\n this.watchers.set(component, listeners);\n this.components.add(component);\n\n // Dispatch an event to communicate the registration to the debug module.\n this.target.dispatchEvent(new CustomEvent('registerComponent:success', {\n bubbles: false,\n detail: {component},\n }));\n\n dispatchSuccess();\n return component;\n }\n\n /**\n * Unregister a component and its watchers.\n *\n * @param {object} component the object instance to unregister\n * @returns {object} the deleted component\n */\n unregisterCompo
nent(component) {\n if (!this.components.has(component)) {\n return component;\n }\n\n this.components.delete(component);\n\n // Remove event listeners.\n const listeners = this.watchers.get(component);\n if (listeners === undefined) {\n return component;\n }\n\n listeners.forEach(({target, watch, listener}) => {\n target.removeEventListener(watch, listener);\n });\n\n this.watchers.delete(component);\n\n return component;\n }\n\n /**\n * Dispatch a change in the state.\n *\n * This method is the only way for components to alter the state. Watchers will receive a\n * read only state to prevent illegal changes. If some user action require a state change, the\n * component should dispatch a mutation to trigger all the necessary logic to alter the state.\n *\n * @method dispatch\n * @param {string} actionName the action name (usually the mutation name)\n * @para
m {mixed} params any number of params the mutation needs.\n */\n async dispatch(actionName, ...params) {\n if (typeof actionName !== 'string') {\n throw new Error(`Dispatch action name must be a string`);\n }\n // JS does not have private methods yet. However, we prevent any component from calling\n // a method starting with \"_\" because the most accepted convention for private methods.\n if (actionName.charAt(0) === '_') {\n throw new Error(`Illegal Private ${actionName} mutation method dispatch`);\n }\n if (this.mutations[actionName] === undefined) {\n throw new Error(`Unkown ${actionName} mutation`);\n }\n\n const pendingPromise = new Pending(`core/reactive:${actionName}${pendingCount++}`);\n\n const mutationFunction = this.mutations[actionName];\n try {\n await mutationFunction.apply(this.mutations, [this.stateManager, ...params]);\n pendingPromise.resolve();\n
} catch (error) {\n // Ensure the state is locked.\n this.stateManager.setReadOnly(true);\n pendingPromise.resolve();\n throw error;\n }\n }\n}\n"],"names":["pendingCount","constructor","description","undefined","eventName","eventDispatch","Error","name","target","document","createTextNode","stateManager","StateManager","this","watchers","Map","components","Set","mutations","addEventListener","callWatchersHandler","bind","pendingState","Pending","state","setInitialState","M","reactive","registerNewInstance","event","dispatchEvent","CustomEvent","detail","action","bubbles","stateData","resolve","addMutations","newFunctions","init","mutation","mutationFunction","Object","entries","setMutations","manager","get","id","getInitialStatePromise","getInitialPromise","registerComponent","component","componentName","dispatchSuccess","dispatchFail","dispatchRegistrationSuccess","dispatchRegistrationFail","has","pendingPromise","listeners","handlers","getWatchers","
forEach","_ref","watch","handler","listener","currentFocus","activeElement","apply","body","contains","focus","push","stateReady","then","catch","reason","error","set","add","unregisterComponent","delete","_ref2","removeEventListener","actionName","charAt","params","setReadOnly"],"mappings":"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"}