diff --git a/lib/amd/build/local/reactive/basecomponent.min.js b/lib/amd/build/local/reactive/basecomponent.min.js index 47a1df0af24..de4b8f20738 100644 --- a/lib/amd/build/local/reactive/basecomponent.min.js +++ b/lib/amd/build/local/reactive/basecomponent.min.js @@ -1,2 +1,2 @@ -define ("core/local/reactive/basecomponent",["exports","core/templates"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);function c(a,b){return h(a)||g(a,b)||e(a,b)||d()}function d(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function e(a,b){if(!a)return;if("string"==typeof a)return f(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return f(a,b)}function f(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);ca.length)b=a.length;for(var c=0,d=Array(b);c.\n\nimport Templates from 'core/templates';\n\n/**\n * Reactive UI component base class.\n *\n * Each UI reactive component should extend this class to interact with a reactive state.\n *\n * @module core/local/reactive/basecomponent\n * @class core/local/reactive/basecomponent\n * @copyright 2020 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n /**\n * The component descriptor data structure.\n *\n * This structure is used by any component and init method to define the way the component will interact\n * with the interface and whith reactive instance operates. The logic behind this object is to avoid\n * unnecessary dependancies between the final interface and the state logic.\n *\n * Any component interacts with a single main DOM element (description.element) but it can use internal\n * selector to select elements within this main element (descriptor.selectors). By default each component\n * will provide it's own default selectors, but those can be overridden by the \"descriptor.selectors\"\n * property in case the mustache wants to reuse the same component logic but with a different interface.\n *\n * @typedef {object} descriptor\n * @property {Reactive} reactive mandatory reactive module to register in\n * @property {DOMElement} element all components needs an element to anchor events\n * @property {object} [selectors] an optional object to override query selectors\n */\n\n /**\n * The class constructor.\n *\n * The only param this method gets is a constructor with all the mandatory\n * and optional component data. Component will receive the same descriptor\n * as create method param.\n *\n * This method will call the \"create\" method before registering the component into\n * the reactive module. This way any component can add default selectors and events.\n *\n * @param {descriptor} descriptor data to create the object.\n */\n constructor(descriptor) {\n\n if (descriptor.element === undefined || !(descriptor.element instanceof HTMLElement)) {\n throw Error(`Reactive components needs a main DOM element to dispatch events`);\n }\n\n if (descriptor.reactive === undefined) {\n throw Error(`Reactive components needs a reactive module to work with`);\n }\n\n this.reactive = descriptor.reactive;\n this.element = descriptor.element;\n\n // Variable to track event listeners.\n this.eventHandlers = new Map([]);\n this.eventListeners = [];\n\n // Empty default component selectors.\n this.selectors = {};\n\n // Empty default event list from the static method.\n this.events = this.constructor.getEvents();\n\n // Call create function to get the component defaults.\n this.create(descriptor);\n\n // Overwrite the components selectors if necessary.\n if (descriptor.selectors !== undefined) {\n this.addSelectors(descriptor.selectors);\n }\n\n // Register the component.\n this.reactive.registerComponent(this);\n }\n\n /**\n * Return the component custom event names.\n *\n * Components may override this method to provide their own events.\n *\n * Component custom events is an important part of component reusability. This function\n * is static because is part of the component definition and should be accessible from\n * outsite the instances. However, values will be available at instance level in the\n * this.events object.\n *\n * @returns {Object} the component events.\n */\n static getEvents() {\n return {};\n }\n\n /**\n * Component create function.\n *\n * Default init method will call \"create\" when all internal attributes are set\n * but before the component is not yet registered in the reactive module.\n *\n * In this method any component can define its own defaults such as:\n * - this.selectors {object} the default query selectors of this component.\n * - this.events {object} a list of event names this component dispatch\n * - extract any data from the main dom element (this.element)\n * - set any other data the component uses\n *\n * @param {descriptor} descriptor the component descriptor\n */\n // eslint-disable-next-line no-unused-vars\n create(descriptor) {\n // Components may override this method to initialize selects, events or other data.\n }\n\n /**\n * Component destroy hook.\n *\n * BaseComponent call this method when a component is unregistered or removed.\n *\n * Components may override this method to clean the HTML or do some action when the\n * component is unregistered or removed.\n */\n destroy() {\n // Components can override this method.\n }\n\n /**\n * Return the list of watchers that component has.\n *\n * Each watcher is represented by an object with two attributes:\n * - watch (string) the specific state event to watch. Example 'section.visible:updated'\n * - handler (function) the function to call when the watching state change happens\n *\n * Any component shoudl override this method to define their state watchers.\n *\n * @returns {array} array of watchers.\n */\n getWatchers() {\n return [];\n }\n\n /**\n * Reactive module will call this method when the state is ready.\n *\n * Component can override this method to update/load the component HTML or to bind\n * listeners to HTML entities.\n */\n stateReady() {\n // Components can override this method.\n }\n\n /**\n * Get the main DOM element of this component or a subelement.\n *\n * @param {string|undefined} query optional subelement query\n * @param {string|undefined} dataId optional data-id value\n * @returns {element|undefined} the DOM element (if any)\n */\n getElement(query, dataId) {\n if (query === undefined && dataId === undefined) {\n return this.element;\n }\n const dataSelector = (dataId) ? `[data-id='${dataId}']` : '';\n const selector = `${query ?? ''}${dataSelector}`;\n return this.element.querySelector(selector);\n }\n\n /**\n * Get the all subelement that match a query selector.\n *\n * @param {string|undefined} query optional subelement query\n * @param {string|undefined} dataId optional data-id value\n * @returns {NodeList} the DOM elements\n */\n getElements(query, dataId) {\n const dataSelector = (dataId) ? `[data-id='${dataId}']` : '';\n const selector = `${query ?? ''}${dataSelector}`;\n return this.element.querySelectorAll(selector);\n }\n\n /**\n * Add or update the component selectors.\n *\n * @param {Object} newSelectors an object of new selectors.\n */\n addSelectors(newSelectors) {\n for (const [selectorName, selector] of Object.entries(newSelectors)) {\n this.selectors[selectorName] = selector;\n }\n }\n\n /**\n * Return a component selector.\n *\n * @param {string} selectorName the selector name\n * @return {string|undefined} the query selector\n */\n getSelector(selectorName) {\n return this.selectors[selectorName];\n }\n\n /**\n * Dispatch a custom event on this.element.\n *\n * This is just a convenient method to dispatch custom events from within a component.\n * Components are free to use an alternative function to dispatch custom\n * events. The only restriction is that it should be dispatched on this.element\n * and specify \"bubbles:true\" to alert any component listeners.\n *\n * @param {string} eventName the event name\n * @param {*} detail event detail data\n */\n dispatchEvent(eventName, detail) {\n this.element.dispatchEvent(new CustomEvent(eventName, {\n bubbles: true,\n detail: detail,\n }));\n }\n\n /**\n * Render a new Component using a mustache file.\n *\n * It is important to note that this method should NOT be used for loading regular mustache files\n * as it returns a Promise that will only be resolved if the mustache registers a component instance.\n *\n * @param {element} target the DOM element that contains the component\n * @param {string} file the component mustache file to render\n * @param {*} data the mustache data\n * @return {Promise} a promise of the resulting component instance\n */\n renderComponent(target, file, data) {\n return new Promise((resolve, reject) => {\n target.addEventListener('ComponentRegistration:Success', ({detail}) => {\n resolve(detail.component);\n });\n target.addEventListener('ComponentRegistration:Fail', () => {\n reject(`Registration of ${file} fails.`);\n });\n Templates.renderForPromise(\n file,\n data\n ).then(({html, js}) => {\n Templates.replaceNodeContents(target, html, js);\n return true;\n }).catch(error => {\n reject(`Rendering of ${file} throws an error.`);\n throw error;\n });\n });\n }\n\n /**\n * Add and bind an event listener to a target and keep track of all event listeners.\n *\n * The native element.addEventListener method is not object oriented friently as the\n * \"this\" represents the element that triggers the event and not the listener class.\n * As components can be unregister and removed at any time, the BaseComponent provides\n * this method to keep track of all component listeners and do all of the bind stuff.\n *\n * @param {Element} target the event target\n * @param {string} type the event name\n * @param {function} listener the class method that recieve the event\n */\n addEventListener(target, type, listener) {\n\n // Check if we have the bind version of that listener.\n let bindListener = this.eventHandlers.get(listener);\n\n if (bindListener === undefined) {\n bindListener = listener.bind(this);\n this.eventHandlers.set(listener, bindListener);\n }\n\n target.addEventListener(type, bindListener);\n\n // Keep track of all component event listeners in case we need to remove them.\n this.eventListeners.push({\n target,\n type,\n bindListener,\n });\n\n }\n\n /**\n * Remove an event listener from a component.\n *\n * This method allows components to remove listeners without keeping track of the\n * listeners bind versions of the method. Both addEventListener and removeEventListener\n * keeps internally the relation between the original class method and the bind one.\n *\n * @param {Element} target the event target\n * @param {string} type the event name\n * @param {function} listener the class method that recieve the event\n */\n removeEventListener(target, type, listener) {\n // Check if we have the bind version of that listener.\n let bindListener = this.eventHandlers.get(listener);\n\n if (bindListener === undefined) {\n // This listener has not been added.\n return;\n }\n\n target.removeEventListener(type, bindListener);\n }\n\n /**\n * Remove all event listeners from this component.\n *\n * This method is called also when the component is unregistered or removed.\n *\n * Note that only listeners registered with the addEventListener method\n * will be removed. Other manual listeners will keep active.\n */\n removeAllEventListeners() {\n this.eventListeners.forEach(({target, type, bindListener}) => {\n target.removeEventListener(type, bindListener);\n });\n this.eventListeners = [];\n }\n\n /**\n * Remove a previously rendered component instance.\n *\n * This method will remove the component HTML and unregister it from the\n * reactive module.\n */\n remove() {\n this.unregister();\n this.element.remove();\n }\n\n /**\n * Unregister the component from the reactive module.\n *\n * This method will disable the component logic, event listeners and watchers\n * but it won't remove any HTML created by the component. However, it will trigger\n * the destroy hook to allow the component to clean parts of the interface.\n */\n unregister() {\n this.reactive.unregisterComponent(this);\n this.removeAllEventListeners();\n this.destroy();\n }\n\n /**\n * Dispatch a component registration event to inform the parent node.\n *\n * The registration event is different from the rest of the component events because\n * is the only way in which components can communicate its existence to a possible parent.\n * Most components will be created by including a mustache file, child components\n * must emit a registration event to the parent DOM element to alert about the registration.\n */\n dispatchRegistrationSuccess() {\n // The registration event does not bubble because we just want to comunicate with the parentNode.\n // Otherwise, any component can get multiple registrations events and could not differentiate\n // between child components and grand child components.\n if (this.element.parentNode === undefined) {\n return;\n }\n // This custom element is captured by renderComponent method.\n this.element.parentNode.dispatchEvent(new CustomEvent(\n 'ComponentRegistration:Success',\n {\n bubbles: false,\n detail: {component: this},\n }\n ));\n }\n\n /**\n * Dispatch a component registration fail event to inform the parent node.\n *\n * As dispatchRegistrationSuccess, this method will communicate the registration fail to the\n * parent node to inform the possible parent component.\n */\n dispatchRegistrationFail() {\n if (this.element.parentNode === undefined) {\n return;\n }\n // This custom element is captured only by renderComponent method.\n this.element.parentNode.dispatchEvent(new CustomEvent(\n 'ComponentRegistration:Fail',\n {\n bubbles: false,\n detail: {component: this},\n }\n ));\n }\n}\n"],"file":"basecomponent.min.js"} \ No newline at end of file +{"version":3,"sources":["../../../src/local/reactive/basecomponent.js"],"names":["descriptor","element","HTMLElement","Error","eventHandlers","Map","eventListeners","selectors","events","constructor","getEvents","create","addSelectors","reactive","dispatchEvent","CustomEvent","bubbles","detail","component","registerComponent","addEventListener","event","stopPropagation","registerChildComponent","query","dataId","dataSelector","selector","querySelector","querySelectorAll","newSelectors","Object","entries","selectorName","eventName","target","file","data","Promise","resolve","reject","Templates","renderForPromise","then","html","js","replaceNodeContents","catch","error","type","listener","bindListener","get","bind","set","push","removeEventListener","forEach","unregister","remove","unregisterComponent","removeAllEventListeners","destroy","parentNode"],"mappings":"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","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\nimport Templates from 'core/templates';\n\n/**\n * Reactive UI component base class.\n *\n * Each UI reactive component should extend this class to interact with a reactive state.\n *\n * @module core/local/reactive/basecomponent\n * @class core/local/reactive/basecomponent\n * @copyright 2020 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n /**\n * The component descriptor data structure.\n *\n * This structure is used by any component and init method to define the way the component will interact\n * with the interface and whith reactive instance operates. The logic behind this object is to avoid\n * unnecessary dependancies between the final interface and the state logic.\n *\n * Any component interacts with a single main DOM element (description.element) but it can use internal\n * selector to select elements within this main element (descriptor.selectors). By default each component\n * will provide it's own default selectors, but those can be overridden by the \"descriptor.selectors\"\n * property in case the mustache wants to reuse the same component logic but with a different interface.\n *\n * @typedef {object} descriptor\n * @property {Reactive} reactive an optional reactive module to register in\n * @property {DOMElement} element all components needs an element to anchor events\n * @property {object} [selectors] an optional object to override query selectors\n */\n\n /**\n * The class constructor.\n *\n * The only param this method gets is a constructor with all the mandatory\n * and optional component data. Component will receive the same descriptor\n * as create method param.\n *\n * This method will call the \"create\" method before registering the component into\n * the reactive module. This way any component can add default selectors and events.\n *\n * @param {descriptor} descriptor data to create the object.\n */\n constructor(descriptor) {\n\n if (descriptor.element === undefined || !(descriptor.element instanceof HTMLElement)) {\n throw Error(`Reactive components needs a main DOM element to dispatch events`);\n }\n\n this.element = descriptor.element;\n\n // Variable to track event listeners.\n this.eventHandlers = new Map([]);\n this.eventListeners = [];\n\n // Empty default component selectors.\n this.selectors = {};\n\n // Empty default event list from the static method.\n this.events = this.constructor.getEvents();\n\n // Call create function to get the component defaults.\n this.create(descriptor);\n\n // Overwrite the components selectors if necessary.\n if (descriptor.selectors !== undefined) {\n this.addSelectors(descriptor.selectors);\n }\n\n // Register into a reactive instance.\n if (descriptor.reactive === undefined) {\n // Ask parent components for registration.\n this.element.dispatchEvent(new CustomEvent(\n 'core/reactive:requestRegistration',\n {\n bubbles: true,\n detail: {component: this},\n }\n ));\n } else {\n this.reactive = descriptor.reactive;\n this.reactive.registerComponent(this);\n // Add a listener to register child components.\n this.addEventListener(\n this.element,\n 'core/reactive:requestRegistration',\n (event) => {\n if (event?.detail?.component) {\n event.stopPropagation();\n this.registerChildComponent(event.detail.component);\n }\n }\n );\n }\n }\n\n /**\n * Return the component custom event names.\n *\n * Components may override this method to provide their own events.\n *\n * Component custom events is an important part of component reusability. This function\n * is static because is part of the component definition and should be accessible from\n * outsite the instances. However, values will be available at instance level in the\n * this.events object.\n *\n * @returns {Object} the component events.\n */\n static getEvents() {\n return {};\n }\n\n /**\n * Component create function.\n *\n * Default init method will call \"create\" when all internal attributes are set\n * but before the component is not yet registered in the reactive module.\n *\n * In this method any component can define its own defaults such as:\n * - this.selectors {object} the default query selectors of this component.\n * - this.events {object} a list of event names this component dispatch\n * - extract any data from the main dom element (this.element)\n * - set any other data the component uses\n *\n * @param {descriptor} descriptor the component descriptor\n */\n // eslint-disable-next-line no-unused-vars\n create(descriptor) {\n // Components may override this method to initialize selects, events or other data.\n }\n\n /**\n * Component destroy hook.\n *\n * BaseComponent call this method when a component is unregistered or removed.\n *\n * Components may override this method to clean the HTML or do some action when the\n * component is unregistered or removed.\n */\n destroy() {\n // Components can override this method.\n }\n\n /**\n * Return the list of watchers that component has.\n *\n * Each watcher is represented by an object with two attributes:\n * - watch (string) the specific state event to watch. Example 'section.visible:updated'\n * - handler (function) the function to call when the watching state change happens\n *\n * Any component shoudl override this method to define their state watchers.\n *\n * @returns {array} array of watchers.\n */\n getWatchers() {\n return [];\n }\n\n /**\n * Reactive module will call this method when the state is ready.\n *\n * Component can override this method to update/load the component HTML or to bind\n * listeners to HTML entities.\n */\n stateReady() {\n // Components can override this method.\n }\n\n /**\n * Get the main DOM element of this component or a subelement.\n *\n * @param {string|undefined} query optional subelement query\n * @param {string|undefined} dataId optional data-id value\n * @returns {element|undefined} the DOM element (if any)\n */\n getElement(query, dataId) {\n if (query === undefined && dataId === undefined) {\n return this.element;\n }\n const dataSelector = (dataId) ? `[data-id='${dataId}']` : '';\n const selector = `${query ?? ''}${dataSelector}`;\n return this.element.querySelector(selector);\n }\n\n /**\n * Get the all subelement that match a query selector.\n *\n * @param {string|undefined} query optional subelement query\n * @param {string|undefined} dataId optional data-id value\n * @returns {NodeList} the DOM elements\n */\n getElements(query, dataId) {\n const dataSelector = (dataId) ? `[data-id='${dataId}']` : '';\n const selector = `${query ?? ''}${dataSelector}`;\n return this.element.querySelectorAll(selector);\n }\n\n /**\n * Add or update the component selectors.\n *\n * @param {Object} newSelectors an object of new selectors.\n */\n addSelectors(newSelectors) {\n for (const [selectorName, selector] of Object.entries(newSelectors)) {\n this.selectors[selectorName] = selector;\n }\n }\n\n /**\n * Return a component selector.\n *\n * @param {string} selectorName the selector name\n * @return {string|undefined} the query selector\n */\n getSelector(selectorName) {\n return this.selectors[selectorName];\n }\n\n /**\n * Dispatch a custom event on this.element.\n *\n * This is just a convenient method to dispatch custom events from within a component.\n * Components are free to use an alternative function to dispatch custom\n * events. The only restriction is that it should be dispatched on this.element\n * and specify \"bubbles:true\" to alert any component listeners.\n *\n * @param {string} eventName the event name\n * @param {*} detail event detail data\n */\n dispatchEvent(eventName, detail) {\n this.element.dispatchEvent(new CustomEvent(eventName, {\n bubbles: true,\n detail: detail,\n }));\n }\n\n /**\n * Render a new Component using a mustache file.\n *\n * It is important to note that this method should NOT be used for loading regular mustache files\n * as it returns a Promise that will only be resolved if the mustache registers a component instance.\n *\n * @param {element} target the DOM element that contains the component\n * @param {string} file the component mustache file to render\n * @param {*} data the mustache data\n * @return {Promise} a promise of the resulting component instance\n */\n renderComponent(target, file, data) {\n return new Promise((resolve, reject) => {\n target.addEventListener('ComponentRegistration:Success', ({detail}) => {\n resolve(detail.component);\n });\n target.addEventListener('ComponentRegistration:Fail', () => {\n reject(`Registration of ${file} fails.`);\n });\n Templates.renderForPromise(\n file,\n data\n ).then(({html, js}) => {\n Templates.replaceNodeContents(target, html, js);\n return true;\n }).catch(error => {\n reject(`Rendering of ${file} throws an error.`);\n throw error;\n });\n });\n }\n\n /**\n * Add and bind an event listener to a target and keep track of all event listeners.\n *\n * The native element.addEventListener method is not object oriented friently as the\n * \"this\" represents the element that triggers the event and not the listener class.\n * As components can be unregister and removed at any time, the BaseComponent provides\n * this method to keep track of all component listeners and do all of the bind stuff.\n *\n * @param {Element} target the event target\n * @param {string} type the event name\n * @param {function} listener the class method that recieve the event\n */\n addEventListener(target, type, listener) {\n\n // Check if we have the bind version of that listener.\n let bindListener = this.eventHandlers.get(listener);\n\n if (bindListener === undefined) {\n bindListener = listener.bind(this);\n this.eventHandlers.set(listener, bindListener);\n }\n\n target.addEventListener(type, bindListener);\n\n // Keep track of all component event listeners in case we need to remove them.\n this.eventListeners.push({\n target,\n type,\n bindListener,\n });\n\n }\n\n /**\n * Remove an event listener from a component.\n *\n * This method allows components to remove listeners without keeping track of the\n * listeners bind versions of the method. Both addEventListener and removeEventListener\n * keeps internally the relation between the original class method and the bind one.\n *\n * @param {Element} target the event target\n * @param {string} type the event name\n * @param {function} listener the class method that recieve the event\n */\n removeEventListener(target, type, listener) {\n // Check if we have the bind version of that listener.\n let bindListener = this.eventHandlers.get(listener);\n\n if (bindListener === undefined) {\n // This listener has not been added.\n return;\n }\n\n target.removeEventListener(type, bindListener);\n }\n\n /**\n * Remove all event listeners from this component.\n *\n * This method is called also when the component is unregistered or removed.\n *\n * Note that only listeners registered with the addEventListener method\n * will be removed. Other manual listeners will keep active.\n */\n removeAllEventListeners() {\n this.eventListeners.forEach(({target, type, bindListener}) => {\n target.removeEventListener(type, bindListener);\n });\n this.eventListeners = [];\n }\n\n /**\n * Remove a previously rendered component instance.\n *\n * This method will remove the component HTML and unregister it from the\n * reactive module.\n */\n remove() {\n this.unregister();\n this.element.remove();\n }\n\n /**\n * Unregister the component from the reactive module.\n *\n * This method will disable the component logic, event listeners and watchers\n * but it won't remove any HTML created by the component. However, it will trigger\n * the destroy hook to allow the component to clean parts of the interface.\n */\n unregister() {\n this.reactive.unregisterComponent(this);\n this.removeAllEventListeners();\n this.destroy();\n }\n\n /**\n * Dispatch a component registration event to inform the parent node.\n *\n * The registration event is different from the rest of the component events because\n * is the only way in which components can communicate its existence to a possible parent.\n * Most components will be created by including a mustache file, child components\n * must emit a registration event to the parent DOM element to alert about the registration.\n */\n dispatchRegistrationSuccess() {\n // The registration event does not bubble because we just want to comunicate with the parentNode.\n // Otherwise, any component can get multiple registrations events and could not differentiate\n // between child components and grand child components.\n if (this.element.parentNode === undefined) {\n return;\n }\n // This custom element is captured by renderComponent method.\n this.element.parentNode.dispatchEvent(new CustomEvent(\n 'ComponentRegistration:Success',\n {\n bubbles: false,\n detail: {component: this},\n }\n ));\n }\n\n /**\n * Dispatch a component registration fail event to inform the parent node.\n *\n * As dispatchRegistrationSuccess, this method will communicate the registration fail to the\n * parent node to inform the possible parent component.\n */\n dispatchRegistrationFail() {\n if (this.element.parentNode === undefined) {\n return;\n }\n // This custom element is captured only by renderComponent method.\n this.element.parentNode.dispatchEvent(new CustomEvent(\n 'ComponentRegistration:Fail',\n {\n bubbles: false,\n detail: {component: this},\n }\n ));\n }\n\n /**\n * Register a child component into the reactive instance.\n *\n * @param {self} component the component to register.\n */\n registerChildComponent(component) {\n component.reactive = this.reactive;\n this.reactive.registerComponent(component);\n }\n}\n"],"file":"basecomponent.min.js"} \ No newline at end of file diff --git a/lib/amd/build/local/reactive/reactive.min.js b/lib/amd/build/local/reactive/reactive.min.js index a8dc5bbdda8..071a6da0be9 100644 --- a/lib/amd/build/local/reactive/reactive.min.js +++ b/lib/amd/build/local/reactive/reactive.min.js @@ -1,2 +1,2 @@ -define ("core/local/reactive/reactive",["exports","core/log","core/local/reactive/statemanager"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}function e(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function f(a){return function(){var b=this,c=arguments;return new Promise(function(d,f){var i=a.apply(b,c);function g(a){e(i,d,f,g,h,"next",a)}function h(a){e(i,d,f,g,h,"throw",a)}g(void 0)})}}function g(a,b){return m(a)||l(a,b)||j(a,b)||h()}function h(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function j(a,b){if(!a)return;if("string"==typeof a)return k(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return k(a,b)}function k(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);ca.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * A generic single state reactive module.\n *\n * @module core/reactive/local/reactive/reactive\n * @class core/reactive/local/reactive/reactive\n * @copyright 2021 Ferran Recio \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';\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 // 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 // Set initial state if we already have it.\n if (description.state !== undefined) {\n this.setInitialState(description.state);\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.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 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 }\n\n /**\n * Return the current state.\n *\n * @return {object}\n */\n get state() {\n return this.stateManager.state;\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 * @property {string} [component.name] the component name to display in warnings and errors.\n * @property {Function} [component.dispatchRegistrationSuccess] method to notify registration success\n * @property {Function} [component.dispatchRegistrationFail] method to notify registration fail\n * @property {Function} [component.getWatchers] getter of the component watchers\n * @property {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 // 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 handler.apply(component, [event.detail]);\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(component.stateReady.bind(component))\n .catch(reason => {\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 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 {*} param 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 const mutationFunction = this.mutations[actionName];\n try {\n await mutationFunction.apply(this.mutations, [this.stateManager, ...params]);\n } catch (error) {\n // Ensure the state is locked.\n this.stateManager.setReadOnly(true);\n throw error;\n }\n }\n}\n"],"file":"reactive.min.js"} \ No newline at end of file +{"version":3,"sources":["../../../src/local/reactive/reactive.js"],"names":["pendingCount","description","eventName","eventDispatch","Error","name","target","document","createTextNode","stateManager","StateManager","watchers","Map","components","Set","mutations","addEventListener","callWatchersHandler","bind","pendingState","Pending","state","setInitialState","event","dispatchEvent","CustomEvent","detail","action","bubbles","stateData","resolve","newFunctions","Object","entries","mutation","mutationFunction","manager","getInitialPromise","component","componentName","dispatchSuccess","dispatchFail","dispatchRegistrationSuccess","dispatchRegistrationFail","has","pendingPromise","listeners","handlers","getWatchers","forEach","watch","handler","listener","apply","push","stateReady","getInitialStatePromise","then","catch","reason","log","error","set","add","delete","get","removeEventListener","actionName","charAt","params","setReadOnly"],"mappings":"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","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 * A generic single state reactive module.\n *\n * @module core/reactive/local/reactive/reactive\n * @class core/reactive/local/reactive/reactive\n * @copyright 2021 Ferran Recio \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\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 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 }\n\n /**\n * Return the current state.\n *\n * @return {object}\n */\n get state() {\n return this.stateManager.state;\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 * @property {string} [component.name] the component name to display in warnings and errors.\n * @property {Function} [component.dispatchRegistrationSuccess] method to notify registration success\n * @property {Function} [component.dispatchRegistrationFail] method to notify registration fail\n * @property {Function} [component.getWatchers] getter of the component watchers\n * @property {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 handler.apply(component, [event.detail]);\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 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 {*} param 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"],"file":"reactive.min.js"} \ No newline at end of file diff --git a/lib/amd/build/local/reactive/statemanager.min.js b/lib/amd/build/local/reactive/statemanager.min.js index 008e2d86242..ff027e5adea 100644 --- a/lib/amd/build/local/reactive/statemanager.min.js +++ b/lib/amd/build/local/reactive/statemanager.min.js @@ -1,2 +1,2 @@ -define ("core/local/reactive/statemanager",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function b(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){b=function(a){return typeof a}}else{b=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return b(a)}function c(a,b,e){if("undefined"!=typeof Reflect&&Reflect.get){c=Reflect.get}else{c=function(a,b,c){var e=d(a,b);if(!e)return;var f=Object.getOwnPropertyDescriptor(e,b);if(f.get){return f.get.call(c)}return f.value}}return c(a,b,e||a)}function d(a,b){while(!Object.prototype.hasOwnProperty.call(a,b)){a=n(a);if(null===a)break}return a}function e(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)m(a,b)}function f(a){return function(){var b=n(a),c;if(k()){var d=n(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return g(this,c)}}function g(a,c){if(c&&("object"===b(c)||"function"==typeof c)){return c}return h(a)}function h(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function i(a){var b="function"==typeof Map?new Map:void 0;i=function(a){if(null===a||!l(a))return a;if("function"!=typeof a){throw new TypeError("Super expression must either be null or a function")}if("undefined"!=typeof b){if(b.has(a))return b.get(a);b.set(a,c)}function c(){return j(a,arguments,n(this).constructor)}c.prototype=Object.create(a.prototype,{constructor:{value:c,enumerable:!1,writable:!0,configurable:!0}});return m(c,a)};return i(a)}function j(){if(k()){j=Reflect.construct}else{j=function(b,c,d){var e=[null];e.push.apply(e,c);var a=Function.bind.apply(b,e),f=new a;if(d)m(f,d.prototype);return f}}return j.apply(null,arguments)}function k(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function l(a){return-1!==Function.toString.call(a).indexOf("[native code]")}function m(a,b){m=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return m(a,b)}function n(a){n=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return n(a)}function o(a,b){return t(a)||s(a,b)||q(a,b)||p()}function p(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function q(a,b){if(!a)return;if("string"==typeof a)return r(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return r(a,b)}function r(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);ca.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * Reactive simple state manager.\n *\n * The state manager contains the state data, trigger update events and\n * can lock and unlock the state data.\n *\n * This file contains the three main elements of the state manager:\n * - State manager: the public class to alter the state, dispatch events and process update messages.\n * - Proxy handler: a private class to keep track of the state object changes.\n * - StateMap class: a private class extending Map class that triggers event when a state list is modifed.\n *\n * @module core/local/reactive/stateManager\n * @class core/local/reactive/stateManager\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * State manager class.\n *\n * This class handle the reactive state and ensure only valid mutations can modify the state.\n * It also provide methods to apply batch state update messages (see processUpdates function doc\n * for more details on update messages).\n *\n * Implementing a deep state manager is complex and will require many frontend resources. To keep\n * the state fast and simple, the state can ONLY store two kind of data:\n * - Object with attributes\n * - Sets of objects with id attributes.\n *\n * This is an example of a valid state:\n *\n * {\n * course: {\n * name: 'course name',\n * shortname: 'courseshort',\n * sectionlist: [21, 34]\n * },\n * sections: [\n * {id: 21, name: 'Topic 1', visible: true},\n * {id: 34, name: 'Topic 2', visible: false,\n * ],\n * }\n *\n * The following cases are NOT allowed at a state ROOT level (throws an exception if they are assigned):\n * - Simple values (strings, boolean...).\n * - Arrays of simple values.\n * - Array of objects without ID attribute (all arrays will be converted to maps and requires an ID).\n *\n * Thanks to those limitations it can simplify the state update messages and the event names. If You\n * need to store simple data, just group them in an object.\n *\n * To grant any state change triggers the proper events, the class uses two private structures:\n * - proxy handler: any object stored in the state is proxied using this class.\n * - StateMap class: any object set in the state will be converted to StateMap using the\n * objects id attribute.\n */\nexport default class StateManager {\n\n /**\n * Create a basic reactive state store.\n *\n * The state manager is meant to work with native JS events. To ensure each reactive module can use\n * it in its own way, the parent element must provide a valid event dispatcher function and an optional\n * DOM element to anchor the event.\n *\n * @param {function} dispatchEvent the function to dispatch the custom event when the state changes.\n * @param {element} target the state changed custom event target (document if none provided)\n */\n constructor(dispatchEvent, target) {\n\n // The dispatch event function.\n /** @package */\n this.dispatchEvent = dispatchEvent;\n\n // The DOM container to trigger events.\n /** @package */\n this.target = target ?? document;\n\n // State can be altered freely until initial state is set.\n /** @package */\n this.readonly = false;\n\n // List of state changes pending to be published as events.\n /** @package */\n this.eventsToPublish = [];\n\n // The update state types functions.\n /** @package */\n this.updateTypes = {\n \"create\": this.defaultCreate.bind(this),\n \"update\": this.defaultUpdate.bind(this),\n \"delete\": this.defaultDelete.bind(this),\n };\n\n // The state_loaded event is special because it only happens one but all components\n // may react to that state, even if they are registered after the setIinitialState.\n // For these reason we use a promise for that event.\n this.initialPromise = new Promise((resolve) => {\n const initialStateDone = (event) => {\n resolve(event.detail.state);\n };\n this.target.addEventListener('state:loaded', initialStateDone);\n });\n }\n\n /**\n * Loads the initial state.\n *\n * Note this method will trigger a state changed event with \"state:loaded\" actionname.\n *\n * The state mode will be set to read only when the initial state is loaded.\n *\n * @param {object} initialState\n */\n setInitialState(initialState) {\n\n if (this.state !== undefined) {\n throw Error('Initial state can only be initialized ones');\n }\n\n // Create the state object.\n const state = new Proxy({}, new Handler('state', this, true));\n for (const [prop, propValue] of Object.entries(initialState)) {\n state[prop] = propValue;\n }\n this.state = state;\n\n // When the state is loaded we can lock it to prevent illegal changes.\n this.readonly = true;\n\n this.dispatchEvent({\n action: 'state:loaded',\n state: this.state,\n }, this.target);\n }\n\n /**\n * Generate a promise that will be resolved when the initial state is loaded.\n *\n * In most cases the final state will be loaded using an ajax call. This is the reason\n * why states manager are created unlocked and won't be reactive until the initial state is set.\n *\n * @return {Promise} the resulting promise\n */\n getInitialPromise() {\n return this.initialPromise;\n }\n\n /**\n * Locks or unlocks the state to prevent illegal updates.\n *\n * Mutations use this method to modify the state. Once the state is updated, they must\n * block again the state.\n *\n * All changes done while the state is writable will be registered using registerStateAction.\n * When the state is set again to read only the method will trigger _publishEvents to communicate\n * changes to all watchers.\n *\n * @param {bool} readonly if the state is in read only mode enabled\n */\n setReadOnly(readonly) {\n\n this.readonly = readonly;\n\n // When the state is in readonly again is time to publish all pending events.\n if (this.readonly) {\n this._publishEvents();\n }\n }\n\n /**\n * Add methods to process update state messages.\n *\n * The state manager provide a default update, create and delete methods. However,\n * some applications may require to override the default methods or even add new ones\n * like \"refresh\" or \"error\".\n *\n * @param {Object} newFunctions the new update types functions.\n */\n addUpdateTypes(newFunctions) {\n for (const [updateType, updateFunction] of Object.entries(newFunctions)) {\n if (typeof updateFunction === 'function') {\n this.updateTypes[updateType] = updateFunction.bind(newFunctions);\n }\n }\n }\n\n /**\n * Process a state updates array and do all the necessary changes.\n *\n * Note this method unlocks the state while it is executing and relocks it\n * when finishes.\n *\n * @param {array} updates\n * @param {Object} updateTypes optional functions to override the default update types.\n */\n processUpdates(updates, updateTypes) {\n if (!Array.isArray(updates)) {\n throw Error('State updates must be an array');\n }\n this.setReadOnly(false);\n updates.forEach((update) => {\n if (update.name === undefined) {\n throw Error('Missing state update name');\n }\n this.processUpdate(\n update.name,\n update.action,\n update.fields,\n updateTypes\n );\n });\n this.setReadOnly(true);\n }\n\n /**\n * Process a single state update.\n *\n * Note this method will not lock or unlock the state by itself.\n *\n * @param {string} updateName the state element to update\n * @param {string} action to action to perform\n * @param {object} fields the new data\n * @param {Object} updateTypes optional functions to override the default update types.\n */\n processUpdate(updateName, action, fields, updateTypes) {\n\n if (!fields) {\n throw Error('Missing state update fields');\n }\n\n if (updateTypes === undefined) {\n updateTypes = {};\n }\n\n action = action ?? 'update';\n\n const method = updateTypes[action] ?? this.updateTypes[action];\n\n if (method === undefined) {\n throw Error(`Unkown update action ${action}`);\n }\n\n method(this, updateName, fields);\n }\n\n /**\n * Process a create state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultCreate(stateManager, updateName, fields) {\n\n let state = stateManager.state;\n\n // Create can be applied only to lists, not to objects.\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n\n /**\n * Process a delete state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultDelete(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n throw Error(`Inexistent ${updateName} ${fields.id}`);\n }\n\n // Process deletion.\n let state = stateManager.state;\n\n if (state[updateName] instanceof StateMap) {\n state[updateName].delete(fields.id);\n return;\n }\n delete state[updateName];\n }\n\n /**\n * Process a update state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultUpdate(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n throw Error(`Inexistent ${updateName} ${fields.id}`);\n }\n\n // Execute updates.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n }\n\n /**\n * Get an element from the state or form an alternative state object.\n *\n * The altstate param is used by external update functions that gets the current\n * state as param.\n *\n * @param {String} name the state object name\n * @param {*} id and object id for state maps.\n * @return {Object|undefined} the state object found\n */\n get(name, id) {\n const state = this.state;\n\n let current = state[name];\n if (current instanceof StateMap) {\n if (id === undefined) {\n throw Error(`Missing id for ${name} state update`);\n }\n current = state[name].get(id);\n }\n\n return current;\n }\n\n /**\n * Register a state modification and generate the necessary events.\n *\n * This method is used mainly by proxy helpers to dispatch state change event.\n * However, mutations can use it to inform components about non reactive changes\n * in the state (only the two first levels of the state are reactive).\n *\n * Each action can produce several events:\n * - The specific attribute updated, created or deleter (example: \"cm.visible:updated\")\n * - The general state object updated, created or deleted (example: \"cm:updated\")\n * - If the element has an ID attribute, the specific event with id (example: \"cm[42].visible:updated\")\n * - If the element has an ID attribute, the general event with id (example: \"cm[42]:updated\")\n * - A generic state update event \"state:update\"\n *\n * @param {string} field the affected state field name\n * @param {string|null} prop the affecter field property (null if affect the full object)\n * @param {string} action the action done (created/updated/deleted)\n * @param {*} data the affected data\n */\n registerStateAction(field, prop, action, data) {\n\n let parentAction = 'updated';\n\n if (prop !== null) {\n this.eventsToPublish.push({\n eventName: `${field}.${prop}:${action}`,\n eventData: data,\n action,\n });\n } else {\n parentAction = action;\n }\n\n // Trigger extra events if the element has an ID attribute.\n if (data.id !== undefined) {\n if (prop !== null) {\n this.eventsToPublish.push({\n eventName: `${field}[${data.id}].${prop}:${action}`,\n eventData: data,\n action,\n });\n }\n this.eventsToPublish.push({\n eventName: `${field}[${data.id}]:${parentAction}`,\n eventData: data,\n action: parentAction,\n });\n }\n\n // Register the general change.\n this.eventsToPublish.push({\n eventName: `${field}:${parentAction}`,\n eventData: data,\n action: parentAction,\n });\n\n // Register state updated event.\n this.eventsToPublish.push({\n eventName: `state:updated`,\n eventData: data,\n action: 'updated',\n });\n }\n\n /**\n * Internal method to publish events.\n *\n * This is a private method, it will be invoked when the state is set back to read only mode.\n */\n _publishEvents() {\n const fieldChanges = this.eventsToPublish;\n this.eventsToPublish = [];\n\n // Dispatch a transaction start event.\n this.dispatchEvent({\n action: 'transaction:start',\n state: this.state,\n element: null,\n }, this.target);\n\n // State changes can be registered in any order. However it will avoid many\n // components errors if they are sorted to have creations-updates-deletes in case\n // some component needs to create or destroy DOM elements before updating them.\n fieldChanges.sort((a, b) => {\n const weights = {\n created: 0,\n updated: 1,\n deleted: 2,\n };\n const aweight = weights[a.action] ?? 0;\n const bweight = weights[b.action] ?? 0;\n // In case both have the same weight, the eventName length decide.\n if (aweight === bweight) {\n return a.eventName.length - b.eventName.length;\n }\n return aweight - bweight;\n });\n\n // List of the published events to prevent redundancies.\n let publishedEvents = new Set();\n\n fieldChanges.forEach((event) => {\n\n const eventkey = `${event.eventName}.${event.eventData.id ?? 0}`;\n\n if (!publishedEvents.has(eventkey)) {\n this.dispatchEvent({\n action: event.eventName,\n state: this.state,\n element: event.eventData\n }, this.target);\n\n publishedEvents.add(eventkey);\n }\n });\n\n // Dispatch a transaction end event.\n this.dispatchEvent({\n action: 'transaction:end',\n state: this.state,\n element: null,\n }, this.target);\n }\n}\n\n// Proxy helpers.\n\n/**\n * The proxy handler.\n *\n * This class will inform any value change directly to the state manager.\n *\n * The proxied variable will throw an error if it is altered when the state manager is\n * in read only mode.\n */\nclass Handler {\n\n /**\n * Class constructor.\n *\n * @param {string} name the variable name used for identify triggered actions\n * @param {StateManager} stateManager the state manager object\n * @param {boolean} proxyValues if new values must be proxied (used only at state root level)\n */\n constructor(name, stateManager, proxyValues) {\n this.name = name;\n this.stateManager = stateManager;\n this.proxyValues = proxyValues ?? false;\n }\n\n /**\n * Set trap to trigger events when the state changes.\n *\n * @param {object} obj the source object (not proxied)\n * @param {string} prop the attribute to set\n * @param {*} value the value to save\n * @param {*} receiver the proxied element to be attached to events\n * @returns {boolean} if the value is set\n */\n set(obj, prop, value, receiver) {\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${prop} value in ${this.name}.`);\n }\n\n // Check any data change.\n if (JSON.stringify(obj[prop]) === JSON.stringify(value)) {\n return true;\n }\n\n const action = (obj[prop] !== undefined) ? 'updated' : 'created';\n\n // Proxy value if necessary (used at state root level).\n if (this.proxyValues) {\n if (Array.isArray(value)) {\n obj[prop] = new StateMap(prop, this.stateManager).loadValues(value);\n } else {\n obj[prop] = new Proxy(value, new Handler(prop, this.stateManager));\n }\n } else {\n obj[prop] = value;\n }\n\n // If the state is not ready yet means the initial state is not yet loaded.\n if (this.stateManager.state === undefined) {\n return true;\n }\n\n this.stateManager.registerStateAction(this.name, prop, action, receiver);\n\n return true;\n }\n\n /**\n * Delete property trap to trigger state change events.\n *\n * @param {*} obj the affected object (not proxied)\n * @param {*} prop the prop to delete\n * @returns {boolean} if prop is deleted\n */\n deleteProperty(obj, prop) {\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to delete ${prop} in ${this.name}.`);\n }\n if (prop in obj) {\n\n delete obj[prop];\n\n this.stateManager.registerStateAction(this.name, prop, 'deleted', obj);\n }\n return true;\n }\n}\n\n/**\n * Class to add events dispatching to the JS Map class.\n *\n * When the state has a list of objects (with IDs) it will be converted into a StateMap.\n * StateMap is used almost in the same way as a regular JS map. Because all elements have an\n * id attribute, it has some specific methods:\n * - add: a convenient method to add an element without specifying the key (\"id\" attribute will be used as a key).\n * - loadValues: to add many elements at once wihout specifying keys (\"id\" attribute will be used).\n *\n * Apart, the main difference between regular Map and MapState is that this one will inform any change to the\n * state manager.\n */\nclass StateMap extends Map {\n\n /**\n * Create a reactive Map.\n *\n * @param {string} name the property name\n * @param {StateManager} stateManager the state manager\n * @param {iterable} iterable an iterable object to create the Map\n */\n constructor(name, stateManager, iterable) {\n // We don't have any \"this\" until be call super.\n super(iterable);\n this.name = name;\n this.stateManager = stateManager;\n }\n\n /**\n * Set an element into the map.\n *\n * Each value needs it's own id attribute. Objects without id will be rejected.\n * The function will throw an error if the value id and the key are not the same.\n *\n * @param {*} key the key to store\n * @param {*} value the value to store\n * @returns {Map} the resulting Map object\n */\n set(key, value) {\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n }\n\n // Normalize keys as string to prevent json decoding errors.\n key = this.normalizeKey(key);\n\n this.checkValue(value);\n\n if (key === undefined || key === null) {\n throw Error('State lists keys cannot be null or undefined');\n }\n\n // ID is mandatory and should be the same as the key.\n if (this.normalizeKey(value.id) !== key) {\n throw new Error(`State error: ${this.name} list element ID (${value.id}) and key (${key}) mismatch`);\n }\n\n const action = (super.has(key)) ? 'updated' : 'created';\n\n // Save proxied data into the list.\n const result = super.set(key, new Proxy(value, new Handler(this.name, this.stateManager)));\n\n // If the state is not ready yet means the initial state is not yet loaded.\n if (this.stateManager.state === undefined) {\n return result;\n }\n\n this.stateManager.registerStateAction(this.name, null, action, super.get(key));\n\n return result;\n }\n\n /**\n * Check if a value is valid to be stored in a a State List.\n *\n * Only objects with id attribute can be stored in State lists.\n *\n * This method throws an error if the value is not valid.\n *\n * @param {object} value (with ID)\n */\n checkValue(value) {\n if (!typeof value === 'object' && value !== null) {\n throw Error('State lists can contain objects only');\n }\n\n if (value.id === undefined) {\n throw Error('State lists elements must contain at least an id attribute');\n }\n }\n\n /**\n * Return a normalized key value for state map.\n *\n * Regular maps uses strict key comparissons but state maps are indexed by ID.JSON conversions\n * and webservices sometimes do unexpected types conversions so we convert any integer key to string.\n *\n * @param {*} key the provided key\n * @returns {string}\n */\n normalizeKey(key) {\n return String(key).valueOf();\n }\n\n /**\n * Insert a new element int a list.\n *\n * Each value needs it's own id attribute. Objects withouts id will be rejected.\n *\n * @param {object} value the value to add (needs an id attribute)\n * @returns {Map} the resulting Map object\n */\n add(value) {\n this.checkValue(value);\n return this.set(value.id, value);\n }\n\n /**\n * Return a state map element.\n *\n * @param {*} key the element id\n * @return {Object}\n */\n get(key) {\n return super.get(this.normalizeKey(key));\n }\n\n /**\n * Check whether an element with the specified key exists or not.\n *\n * @param {*} key the key to find\n * @return {boolean}\n */\n has(key) {\n return super.has(this.normalizeKey(key));\n }\n\n /**\n * Delete an element from the map.\n *\n * @param {*} key\n * @returns {boolean}\n */\n delete(key) {\n // State maps uses only string keys to avoid strict comparisons.\n key = this.normalizeKey(key);\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n }\n\n const previous = super.get(key);\n\n const result = super.delete(key);\n if (!result) {\n return result;\n }\n\n this.stateManager.registerStateAction(this.name, null, 'deleted', previous);\n\n return result;\n }\n\n /**\n * Return a suitable structure for JSON conversion.\n *\n * This function is needed because new values are compared in JSON. StateMap has Private\n * attributes which cannot be stringified (like this.stateManager which will produce an\n * infinite recursivity).\n *\n * @returns {array}\n */\n toJSON() {\n let result = [];\n this.forEach((value) => {\n result.push(value);\n });\n return result;\n }\n\n /**\n * Insert a full list of values using the id attributes as keys.\n *\n * This method is used mainly to initialize the list. Note each element is indexed by its \"id\" attribute.\n * This is a basic restriction of StateMap. All elements need an id attribute, otherwise it won't be saved.\n *\n * @param {iterable} values the values to load\n * @returns {StateMap} return the this value\n */\n loadValues(values) {\n values.forEach((data) => {\n this.checkValue(data);\n let key = data.id;\n let newvalue = new Proxy(data, new Handler(this.name, this.stateManager));\n this.set(key, newvalue);\n });\n return this;\n }\n}\n"],"file":"statemanager.min.js"} \ No newline at end of file +{"version":3,"sources":["../../../src/local/reactive/statemanager.js"],"names":["StateManager","dispatchEvent","target","document","readonly","eventsToPublish","updateTypes","defaultCreate","bind","defaultUpdate","defaultDelete","defaultPut","defaultOverride","initialPromise","Promise","resolve","addEventListener","initialStateDone","event","detail","state","initialState","Error","Proxy","Handler","Object","entries","prop","propValue","action","_publishEvents","newFunctions","updateType","updateFunction","updates","Array","isArray","setReadOnly","forEach","update","name","processUpdate","fields","updateName","method","stateManager","StateMap","add","current","get","id","delete","fieldName","fieldValue","field","data","parentAction","push","eventName","eventData","fieldChanges","element","sort","a","b","weights","created","updated","deleted","aweight","bweight","length","publishedEvents","Set","eventkey","has","proxyValues","obj","value","receiver","JSON","stringify","loadValues","registerStateAction","iterable","key","normalizeKey","checkValue","result","valueOf","set","previous","values","newvalue","Map"],"mappings":"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","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 simple state manager.\n *\n * The state manager contains the state data, trigger update events and\n * can lock and unlock the state data.\n *\n * This file contains the three main elements of the state manager:\n * - State manager: the public class to alter the state, dispatch events and process update messages.\n * - Proxy handler: a private class to keep track of the state object changes.\n * - StateMap class: a private class extending Map class that triggers event when a state list is modifed.\n *\n * @module core/local/reactive/stateManager\n * @class core/local/reactive/stateManager\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * State manager class.\n *\n * This class handle the reactive state and ensure only valid mutations can modify the state.\n * It also provide methods to apply batch state update messages (see processUpdates function doc\n * for more details on update messages).\n *\n * Implementing a deep state manager is complex and will require many frontend resources. To keep\n * the state fast and simple, the state can ONLY store two kind of data:\n * - Object with attributes\n * - Sets of objects with id attributes.\n *\n * This is an example of a valid state:\n *\n * {\n * course: {\n * name: 'course name',\n * shortname: 'courseshort',\n * sectionlist: [21, 34]\n * },\n * sections: [\n * {id: 21, name: 'Topic 1', visible: true},\n * {id: 34, name: 'Topic 2', visible: false,\n * ],\n * }\n *\n * The following cases are NOT allowed at a state ROOT level (throws an exception if they are assigned):\n * - Simple values (strings, boolean...).\n * - Arrays of simple values.\n * - Array of objects without ID attribute (all arrays will be converted to maps and requires an ID).\n *\n * Thanks to those limitations it can simplify the state update messages and the event names. If You\n * need to store simple data, just group them in an object.\n *\n * To grant any state change triggers the proper events, the class uses two private structures:\n * - proxy handler: any object stored in the state is proxied using this class.\n * - StateMap class: any object set in the state will be converted to StateMap using the\n * objects id attribute.\n */\nexport default class StateManager {\n\n /**\n * Create a basic reactive state store.\n *\n * The state manager is meant to work with native JS events. To ensure each reactive module can use\n * it in its own way, the parent element must provide a valid event dispatcher function and an optional\n * DOM element to anchor the event.\n *\n * @param {function} dispatchEvent the function to dispatch the custom event when the state changes.\n * @param {element} target the state changed custom event target (document if none provided)\n */\n constructor(dispatchEvent, target) {\n\n // The dispatch event function.\n /** @package */\n this.dispatchEvent = dispatchEvent;\n\n // The DOM container to trigger events.\n /** @package */\n this.target = target ?? document;\n\n // State can be altered freely until initial state is set.\n /** @package */\n this.readonly = false;\n\n // List of state changes pending to be published as events.\n /** @package */\n this.eventsToPublish = [];\n\n // The update state types functions.\n /** @package */\n this.updateTypes = {\n \"create\": this.defaultCreate.bind(this),\n \"update\": this.defaultUpdate.bind(this),\n \"delete\": this.defaultDelete.bind(this),\n \"put\": this.defaultPut.bind(this),\n \"override\": this.defaultOverride.bind(this),\n };\n\n // The state_loaded event is special because it only happens one but all components\n // may react to that state, even if they are registered after the setIinitialState.\n // For these reason we use a promise for that event.\n this.initialPromise = new Promise((resolve) => {\n const initialStateDone = (event) => {\n resolve(event.detail.state);\n };\n this.target.addEventListener('state:loaded', initialStateDone);\n });\n }\n\n /**\n * Loads the initial state.\n *\n * Note this method will trigger a state changed event with \"state:loaded\" actionname.\n *\n * The state mode will be set to read only when the initial state is loaded.\n *\n * @param {object} initialState\n */\n setInitialState(initialState) {\n\n if (this.state !== undefined) {\n throw Error('Initial state can only be initialized ones');\n }\n\n // Create the state object.\n const state = new Proxy({}, new Handler('state', this, true));\n for (const [prop, propValue] of Object.entries(initialState)) {\n state[prop] = propValue;\n }\n this.state = state;\n\n // When the state is loaded we can lock it to prevent illegal changes.\n this.readonly = true;\n\n this.dispatchEvent({\n action: 'state:loaded',\n state: this.state,\n }, this.target);\n }\n\n /**\n * Generate a promise that will be resolved when the initial state is loaded.\n *\n * In most cases the final state will be loaded using an ajax call. This is the reason\n * why states manager are created unlocked and won't be reactive until the initial state is set.\n *\n * @return {Promise} the resulting promise\n */\n getInitialPromise() {\n return this.initialPromise;\n }\n\n /**\n * Locks or unlocks the state to prevent illegal updates.\n *\n * Mutations use this method to modify the state. Once the state is updated, they must\n * block again the state.\n *\n * All changes done while the state is writable will be registered using registerStateAction.\n * When the state is set again to read only the method will trigger _publishEvents to communicate\n * changes to all watchers.\n *\n * @param {bool} readonly if the state is in read only mode enabled\n */\n setReadOnly(readonly) {\n\n this.readonly = readonly;\n\n // When the state is in readonly again is time to publish all pending events.\n if (this.readonly) {\n this._publishEvents();\n }\n }\n\n /**\n * Add methods to process update state messages.\n *\n * The state manager provide a default update, create and delete methods. However,\n * some applications may require to override the default methods or even add new ones\n * like \"refresh\" or \"error\".\n *\n * @param {Object} newFunctions the new update types functions.\n */\n addUpdateTypes(newFunctions) {\n for (const [updateType, updateFunction] of Object.entries(newFunctions)) {\n if (typeof updateFunction === 'function') {\n this.updateTypes[updateType] = updateFunction.bind(newFunctions);\n }\n }\n }\n\n /**\n * Process a state updates array and do all the necessary changes.\n *\n * Note this method unlocks the state while it is executing and relocks it\n * when finishes.\n *\n * @param {array} updates\n * @param {Object} updateTypes optional functions to override the default update types.\n */\n processUpdates(updates, updateTypes) {\n if (!Array.isArray(updates)) {\n throw Error('State updates must be an array');\n }\n this.setReadOnly(false);\n updates.forEach((update) => {\n if (update.name === undefined) {\n throw Error('Missing state update name');\n }\n this.processUpdate(\n update.name,\n update.action,\n update.fields,\n updateTypes\n );\n });\n this.setReadOnly(true);\n }\n\n /**\n * Process a single state update.\n *\n * Note this method will not lock or unlock the state by itself.\n *\n * @param {string} updateName the state element to update\n * @param {string} action to action to perform\n * @param {object} fields the new data\n * @param {Object} updateTypes optional functions to override the default update types.\n */\n processUpdate(updateName, action, fields, updateTypes) {\n\n if (!fields) {\n throw Error('Missing state update fields');\n }\n\n if (updateTypes === undefined) {\n updateTypes = {};\n }\n\n action = action ?? 'update';\n\n const method = updateTypes[action] ?? this.updateTypes[action];\n\n if (method === undefined) {\n throw Error(`Unkown update action ${action}`);\n }\n\n method(this, updateName, fields);\n }\n\n /**\n * Process a create state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultCreate(stateManager, updateName, fields) {\n\n let state = stateManager.state;\n\n // Create can be applied only to lists, not to objects.\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n\n /**\n * Process a delete state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultDelete(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n throw Error(`Inexistent ${updateName} ${fields.id}`);\n }\n\n // Process deletion.\n let state = stateManager.state;\n\n if (state[updateName] instanceof StateMap) {\n state[updateName].delete(fields.id);\n return;\n }\n delete state[updateName];\n }\n\n /**\n * Process a update state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultUpdate(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n throw Error(`Inexistent ${updateName} ${fields.id}`);\n }\n\n // Execute updates.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n }\n\n /**\n * Process a put state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultPut(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (current) {\n // Update attributes.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n } else {\n // Create new object.\n let state = stateManager.state;\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n }\n\n /**\n * Process an override state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultOverride(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (current) {\n // Remove any unnecessary fields.\n for (const [fieldName] of Object.entries(current)) {\n if (fields[fieldName] === undefined) {\n delete current[fieldName];\n }\n }\n // Update field.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n } else {\n // Create the element if not exists.\n let state = stateManager.state;\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n }\n\n /**\n * Get an element from the state or form an alternative state object.\n *\n * The altstate param is used by external update functions that gets the current\n * state as param.\n *\n * @param {String} name the state object name\n * @param {*} id and object id for state maps.\n * @return {Object|undefined} the state object found\n */\n get(name, id) {\n const state = this.state;\n\n let current = state[name];\n if (current instanceof StateMap) {\n if (id === undefined) {\n throw Error(`Missing id for ${name} state update`);\n }\n current = state[name].get(id);\n }\n\n return current;\n }\n\n /**\n * Register a state modification and generate the necessary events.\n *\n * This method is used mainly by proxy helpers to dispatch state change event.\n * However, mutations can use it to inform components about non reactive changes\n * in the state (only the two first levels of the state are reactive).\n *\n * Each action can produce several events:\n * - The specific attribute updated, created or deleter (example: \"cm.visible:updated\")\n * - The general state object updated, created or deleted (example: \"cm:updated\")\n * - If the element has an ID attribute, the specific event with id (example: \"cm[42].visible:updated\")\n * - If the element has an ID attribute, the general event with id (example: \"cm[42]:updated\")\n * - A generic state update event \"state:update\"\n *\n * @param {string} field the affected state field name\n * @param {string|null} prop the affecter field property (null if affect the full object)\n * @param {string} action the action done (created/updated/deleted)\n * @param {*} data the affected data\n */\n registerStateAction(field, prop, action, data) {\n\n let parentAction = 'updated';\n\n if (prop !== null) {\n this.eventsToPublish.push({\n eventName: `${field}.${prop}:${action}`,\n eventData: data,\n action,\n });\n } else {\n parentAction = action;\n }\n\n // Trigger extra events if the element has an ID attribute.\n if (data.id !== undefined) {\n if (prop !== null) {\n this.eventsToPublish.push({\n eventName: `${field}[${data.id}].${prop}:${action}`,\n eventData: data,\n action,\n });\n }\n this.eventsToPublish.push({\n eventName: `${field}[${data.id}]:${parentAction}`,\n eventData: data,\n action: parentAction,\n });\n }\n\n // Register the general change.\n this.eventsToPublish.push({\n eventName: `${field}:${parentAction}`,\n eventData: data,\n action: parentAction,\n });\n\n // Register state updated event.\n this.eventsToPublish.push({\n eventName: `state:updated`,\n eventData: data,\n action: 'updated',\n });\n }\n\n /**\n * Internal method to publish events.\n *\n * This is a private method, it will be invoked when the state is set back to read only mode.\n */\n _publishEvents() {\n const fieldChanges = this.eventsToPublish;\n this.eventsToPublish = [];\n\n // Dispatch a transaction start event.\n this.dispatchEvent({\n action: 'transaction:start',\n state: this.state,\n element: null,\n }, this.target);\n\n // State changes can be registered in any order. However it will avoid many\n // components errors if they are sorted to have creations-updates-deletes in case\n // some component needs to create or destroy DOM elements before updating them.\n fieldChanges.sort((a, b) => {\n const weights = {\n created: 0,\n updated: 1,\n deleted: 2,\n };\n const aweight = weights[a.action] ?? 0;\n const bweight = weights[b.action] ?? 0;\n // In case both have the same weight, the eventName length decide.\n if (aweight === bweight) {\n return a.eventName.length - b.eventName.length;\n }\n return aweight - bweight;\n });\n\n // List of the published events to prevent redundancies.\n let publishedEvents = new Set();\n\n fieldChanges.forEach((event) => {\n\n const eventkey = `${event.eventName}.${event.eventData.id ?? 0}`;\n\n if (!publishedEvents.has(eventkey)) {\n this.dispatchEvent({\n action: event.eventName,\n state: this.state,\n element: event.eventData\n }, this.target);\n\n publishedEvents.add(eventkey);\n }\n });\n\n // Dispatch a transaction end event.\n this.dispatchEvent({\n action: 'transaction:end',\n state: this.state,\n element: null,\n }, this.target);\n }\n}\n\n// Proxy helpers.\n\n/**\n * The proxy handler.\n *\n * This class will inform any value change directly to the state manager.\n *\n * The proxied variable will throw an error if it is altered when the state manager is\n * in read only mode.\n */\nclass Handler {\n\n /**\n * Class constructor.\n *\n * @param {string} name the variable name used for identify triggered actions\n * @param {StateManager} stateManager the state manager object\n * @param {boolean} proxyValues if new values must be proxied (used only at state root level)\n */\n constructor(name, stateManager, proxyValues) {\n this.name = name;\n this.stateManager = stateManager;\n this.proxyValues = proxyValues ?? false;\n }\n\n /**\n * Set trap to trigger events when the state changes.\n *\n * @param {object} obj the source object (not proxied)\n * @param {string} prop the attribute to set\n * @param {*} value the value to save\n * @param {*} receiver the proxied element to be attached to events\n * @returns {boolean} if the value is set\n */\n set(obj, prop, value, receiver) {\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${prop} value in ${this.name}.`);\n }\n\n // Check any data change.\n if (JSON.stringify(obj[prop]) === JSON.stringify(value)) {\n return true;\n }\n\n const action = (obj[prop] !== undefined) ? 'updated' : 'created';\n\n // Proxy value if necessary (used at state root level).\n if (this.proxyValues) {\n if (Array.isArray(value)) {\n obj[prop] = new StateMap(prop, this.stateManager).loadValues(value);\n } else {\n obj[prop] = new Proxy(value, new Handler(prop, this.stateManager));\n }\n } else {\n obj[prop] = value;\n }\n\n // If the state is not ready yet means the initial state is not yet loaded.\n if (this.stateManager.state === undefined) {\n return true;\n }\n\n this.stateManager.registerStateAction(this.name, prop, action, receiver);\n\n return true;\n }\n\n /**\n * Delete property trap to trigger state change events.\n *\n * @param {*} obj the affected object (not proxied)\n * @param {*} prop the prop to delete\n * @returns {boolean} if prop is deleted\n */\n deleteProperty(obj, prop) {\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to delete ${prop} in ${this.name}.`);\n }\n if (prop in obj) {\n\n delete obj[prop];\n\n this.stateManager.registerStateAction(this.name, prop, 'deleted', obj);\n }\n return true;\n }\n}\n\n/**\n * Class to add events dispatching to the JS Map class.\n *\n * When the state has a list of objects (with IDs) it will be converted into a StateMap.\n * StateMap is used almost in the same way as a regular JS map. Because all elements have an\n * id attribute, it has some specific methods:\n * - add: a convenient method to add an element without specifying the key (\"id\" attribute will be used as a key).\n * - loadValues: to add many elements at once wihout specifying keys (\"id\" attribute will be used).\n *\n * Apart, the main difference between regular Map and MapState is that this one will inform any change to the\n * state manager.\n */\nclass StateMap extends Map {\n\n /**\n * Create a reactive Map.\n *\n * @param {string} name the property name\n * @param {StateManager} stateManager the state manager\n * @param {iterable} iterable an iterable object to create the Map\n */\n constructor(name, stateManager, iterable) {\n // We don't have any \"this\" until be call super.\n super(iterable);\n this.name = name;\n this.stateManager = stateManager;\n }\n\n /**\n * Set an element into the map.\n *\n * Each value needs it's own id attribute. Objects without id will be rejected.\n * The function will throw an error if the value id and the key are not the same.\n *\n * @param {*} key the key to store\n * @param {*} value the value to store\n * @returns {Map} the resulting Map object\n */\n set(key, value) {\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n }\n\n // Normalize keys as string to prevent json decoding errors.\n key = this.normalizeKey(key);\n\n this.checkValue(value);\n\n if (key === undefined || key === null) {\n throw Error('State lists keys cannot be null or undefined');\n }\n\n // ID is mandatory and should be the same as the key.\n if (this.normalizeKey(value.id) !== key) {\n throw new Error(`State error: ${this.name} list element ID (${value.id}) and key (${key}) mismatch`);\n }\n\n const action = (super.has(key)) ? 'updated' : 'created';\n\n // Save proxied data into the list.\n const result = super.set(key, new Proxy(value, new Handler(this.name, this.stateManager)));\n\n // If the state is not ready yet means the initial state is not yet loaded.\n if (this.stateManager.state === undefined) {\n return result;\n }\n\n this.stateManager.registerStateAction(this.name, null, action, super.get(key));\n\n return result;\n }\n\n /**\n * Check if a value is valid to be stored in a a State List.\n *\n * Only objects with id attribute can be stored in State lists.\n *\n * This method throws an error if the value is not valid.\n *\n * @param {object} value (with ID)\n */\n checkValue(value) {\n if (!typeof value === 'object' && value !== null) {\n throw Error('State lists can contain objects only');\n }\n\n if (value.id === undefined) {\n throw Error('State lists elements must contain at least an id attribute');\n }\n }\n\n /**\n * Return a normalized key value for state map.\n *\n * Regular maps uses strict key comparissons but state maps are indexed by ID.JSON conversions\n * and webservices sometimes do unexpected types conversions so we convert any integer key to string.\n *\n * @param {*} key the provided key\n * @returns {string}\n */\n normalizeKey(key) {\n return String(key).valueOf();\n }\n\n /**\n * Insert a new element int a list.\n *\n * Each value needs it's own id attribute. Objects withouts id will be rejected.\n *\n * @param {object} value the value to add (needs an id attribute)\n * @returns {Map} the resulting Map object\n */\n add(value) {\n this.checkValue(value);\n return this.set(value.id, value);\n }\n\n /**\n * Return a state map element.\n *\n * @param {*} key the element id\n * @return {Object}\n */\n get(key) {\n return super.get(this.normalizeKey(key));\n }\n\n /**\n * Check whether an element with the specified key exists or not.\n *\n * @param {*} key the key to find\n * @return {boolean}\n */\n has(key) {\n return super.has(this.normalizeKey(key));\n }\n\n /**\n * Delete an element from the map.\n *\n * @param {*} key\n * @returns {boolean}\n */\n delete(key) {\n // State maps uses only string keys to avoid strict comparisons.\n key = this.normalizeKey(key);\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n }\n\n const previous = super.get(key);\n\n const result = super.delete(key);\n if (!result) {\n return result;\n }\n\n this.stateManager.registerStateAction(this.name, null, 'deleted', previous);\n\n return result;\n }\n\n /**\n * Return a suitable structure for JSON conversion.\n *\n * This function is needed because new values are compared in JSON. StateMap has Private\n * attributes which cannot be stringified (like this.stateManager which will produce an\n * infinite recursivity).\n *\n * @returns {array}\n */\n toJSON() {\n let result = [];\n this.forEach((value) => {\n result.push(value);\n });\n return result;\n }\n\n /**\n * Insert a full list of values using the id attributes as keys.\n *\n * This method is used mainly to initialize the list. Note each element is indexed by its \"id\" attribute.\n * This is a basic restriction of StateMap. All elements need an id attribute, otherwise it won't be saved.\n *\n * @param {iterable} values the values to load\n * @returns {StateMap} return the this value\n */\n loadValues(values) {\n values.forEach((data) => {\n this.checkValue(data);\n let key = data.id;\n let newvalue = new Proxy(data, new Handler(this.name, this.stateManager));\n this.set(key, newvalue);\n });\n return this;\n }\n}\n"],"file":"statemanager.min.js"} \ No newline at end of file diff --git a/lib/amd/src/local/reactive/basecomponent.js b/lib/amd/src/local/reactive/basecomponent.js index 583b79587cc..c56b76237fd 100644 --- a/lib/amd/src/local/reactive/basecomponent.js +++ b/lib/amd/src/local/reactive/basecomponent.js @@ -40,7 +40,7 @@ export default class { * property in case the mustache wants to reuse the same component logic but with a different interface. * * @typedef {object} descriptor - * @property {Reactive} reactive mandatory reactive module to register in + * @property {Reactive} reactive an optional reactive module to register in * @property {DOMElement} element all components needs an element to anchor events * @property {object} [selectors] an optional object to override query selectors */ @@ -63,11 +63,6 @@ export default class { throw Error(`Reactive components needs a main DOM element to dispatch events`); } - if (descriptor.reactive === undefined) { - throw Error(`Reactive components needs a reactive module to work with`); - } - - this.reactive = descriptor.reactive; this.element = descriptor.element; // Variable to track event listeners. @@ -88,8 +83,31 @@ export default class { this.addSelectors(descriptor.selectors); } - // Register the component. - this.reactive.registerComponent(this); + // Register into a reactive instance. + if (descriptor.reactive === undefined) { + // Ask parent components for registration. + this.element.dispatchEvent(new CustomEvent( + 'core/reactive:requestRegistration', + { + bubbles: true, + detail: {component: this}, + } + )); + } else { + this.reactive = descriptor.reactive; + this.reactive.registerComponent(this); + // Add a listener to register child components. + this.addEventListener( + this.element, + 'core/reactive:requestRegistration', + (event) => { + if (event?.detail?.component) { + event.stopPropagation(); + this.registerChildComponent(event.detail.component); + } + } + ); + } } /** @@ -403,4 +421,14 @@ export default class { } )); } + + /** + * Register a child component into the reactive instance. + * + * @param {self} component the component to register. + */ + registerChildComponent(component) { + component.reactive = this.reactive; + this.reactive.registerComponent(component); + } } diff --git a/lib/amd/src/local/reactive/reactive.js b/lib/amd/src/local/reactive/reactive.js index fa481f7ece7..3460838b26d 100644 --- a/lib/amd/src/local/reactive/reactive.js +++ b/lib/amd/src/local/reactive/reactive.js @@ -24,6 +24,10 @@ import log from 'core/log'; import StateManager from 'core/local/reactive/statemanager'; +import Pending from 'core/pending'; + +// Count the number of pending operations done to ensure we have a unique id for each one. +let pendingCount = 0; /** * Set up general reactive class to create a single state application with components. @@ -72,6 +76,10 @@ export default class { throw new Error(`Reactivity event required`); } + if (description.name !== undefined) { + this.name = description.name; + } + // Each reactive instance has its own element anchor to propagate state changes internally. // By default the module will create a fake DOM element to target custom events but // if all reactive components is constrait to a single element, this can be passed as @@ -94,6 +102,9 @@ export default class { // Register the event to alert watchers when specific state change happens. this.target.addEventListener(this.eventName, this.callWatchersHandler.bind(this)); + // Add a pending operation waiting for the initial state. + this.pendingState = new Pending(`core/reactive:registerInstance${pendingCount++}`); + // Set initial state if we already have it. if (description.state !== undefined) { this.setInitialState(description.state); @@ -126,6 +137,7 @@ export default class { * @param {object} stateData the initial state data. */ setInitialState(stateData) { + this.pendingState.resolve(); this.stateManager.setInitialState(stateData); } @@ -225,6 +237,9 @@ export default class { return component; } + // Components are fully registered only when the state ready promise is resolved. + const pendingPromise = new Pending(`core/reactive:registerComponent${pendingCount++}`); + // Keep track of the event listeners. let listeners = []; @@ -262,8 +277,13 @@ export default class { // is loaded. For those cases we have a state promise to handle this specific state change. if (component.stateReady !== undefined) { this.getInitialStatePromise() - .then(component.stateReady.bind(component)) + .then(state => { + component.stateReady(state); + pendingPromise.resolve(); + return true; + }) .catch(reason => { + pendingPromise.resolve(); log.error(`Initial state in ${componentName} rejected due to: ${reason}`); log.error(reason); }); @@ -328,12 +348,17 @@ export default class { if (this.mutations[actionName] === undefined) { throw new Error(`Unkown ${actionName} mutation`); } + + const pendingPromise = new Pending(`core/reactive:${actionName}${pendingCount++}`); + const mutationFunction = this.mutations[actionName]; try { await mutationFunction.apply(this.mutations, [this.stateManager, ...params]); + pendingPromise.resolve(); } catch (error) { // Ensure the state is locked. this.stateManager.setReadOnly(true); + pendingPromise.resolve(); throw error; } } diff --git a/lib/amd/src/local/reactive/statemanager.js b/lib/amd/src/local/reactive/statemanager.js index 00628b0380a..bdbf15dde96 100644 --- a/lib/amd/src/local/reactive/statemanager.js +++ b/lib/amd/src/local/reactive/statemanager.js @@ -105,6 +105,8 @@ export default class StateManager { "create": this.defaultCreate.bind(this), "update": this.defaultUpdate.bind(this), "delete": this.defaultDelete.bind(this), + "put": this.defaultPut.bind(this), + "override": this.defaultOverride.bind(this), }; // The state_loaded event is special because it only happens one but all components @@ -324,6 +326,66 @@ export default class StateManager { } } + /** + * Process a put state message. + * + * @param {Object} stateManager the state manager + * @param {String} updateName the state element to update + * @param {Object} fields the new data + */ + defaultPut(stateManager, updateName, fields) { + + // Get the current value. + let current = stateManager.get(updateName, fields.id); + if (current) { + // Update attributes. + for (const [fieldName, fieldValue] of Object.entries(fields)) { + current[fieldName] = fieldValue; + } + } else { + // Create new object. + let state = stateManager.state; + if (state[updateName] instanceof StateMap) { + state[updateName].add(fields); + return; + } + state[updateName] = fields; + } + } + + /** + * Process an override state message. + * + * @param {Object} stateManager the state manager + * @param {String} updateName the state element to update + * @param {Object} fields the new data + */ + defaultOverride(stateManager, updateName, fields) { + + // Get the current value. + let current = stateManager.get(updateName, fields.id); + if (current) { + // Remove any unnecessary fields. + for (const [fieldName] of Object.entries(current)) { + if (fields[fieldName] === undefined) { + delete current[fieldName]; + } + } + // Update field. + for (const [fieldName, fieldValue] of Object.entries(fields)) { + current[fieldName] = fieldValue; + } + } else { + // Create the element if not exists. + let state = stateManager.state; + if (state[updateName] instanceof StateMap) { + state[updateName].add(fields); + return; + } + state[updateName] = fields; + } + } + /** * Get an element from the state or form an alternative state object. *