Proyectos de Subversion Moodle

Rev

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    http://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     * description.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        this.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        // Check 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 initial 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 found\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 component';\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 watchers.\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                    component.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    unregisterComponent(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     * @param {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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"}