AutorÃa | Ultima modificación | Ver Log |
{"version":3,"file":"basecomponent.min.js","sources":["../../../src/local/reactive/basecomponent.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\nimport Templates from 'core/templates';\nimport {addOverlay, removeOverlay, removeAllOverlays} from 'core/local/reactive/overlay';\n\n/**\n * Reactive UI component base class.\n *\n * Each UI reactive co
mponent 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 <ferran@moodle.com>\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.selec
tors\"\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 st
atic 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 op
tional 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 com
ponent 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 l
isteners.\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.removeEventListene
r(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 r
emove 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 /**\n * Set the lock value and locks or unlocks the element.\n *\n * @param {boolean} locked the new locked value\n */\n set locked(locked) {\n this.setElementLocked(this.element, locked);\n }\n\n /**\n * Get the current locked value from the element.\n *\n * @return {boolean}\n */\n get locked() {\n return this.getElementLocked(this.element);\n }\n\n /**\n * Lock/unlock an element.\n *\n * @param {Element} target the event target\n * @param {boolean} locked the new locked value\n */\n setElementLo
cked(target, locked) {\n target.dataset.locked = locked ?? false;\n if (locked) {\n // Disable interactions.\n target.style.pointerEvents = 'none';\n target.style.userSelect = 'none';\n // Check if it is draggable.\n if (target.hasAttribute('draggable')) {\n target.setAttribute('draggable', false);\n }\n target.setAttribute('aria-busy', true);\n } else {\n // Enable interactions.\n target.style.pointerEvents = null;\n target.style.userSelect = null;\n // Check if it was draggable.\n if (target.hasAttribute('draggable')) {\n target.setAttribute('draggable', true);\n }\n target.setAttribute('aria-busy', false);\n }\n }\n\n /**\n * Get the current locked value from the element.\n *\n * @param {Element} target the event target\n * @return {boolean}\n */\n getElementLocked(tar
get) {\n return target.dataset.locked ?? false;\n }\n\n /**\n * Adds an overlay to a specific page element.\n *\n * @param {Object} definition the overlay definition.\n * @param {String} definition.content an optional overlay content.\n * @param {String} definition.classes an optional CSS classes\n * @param {Element} target optional parent object (this.element will be used if none provided)\n */\n async addOverlay(definition, target) {\n if (this._overlay) {\n this.removeOverlay();\n }\n this._overlay = await addOverlay(\n {\n content: definition.content,\n css: definition.classes ?? 'file-drop-zone',\n },\n target ?? this.element\n );\n }\n\n /**\n * Remove the current overlay.\n */\n removeOverlay() {\n if (!this._overlay) {\n return;\n }\n removeOverlay(this._overlay);\n this._overlay = null;\n }\n\n
/**\n * Remove all page overlais.\n */\n removeAllOverlays() {\n removeAllOverlays();\n }\n}\n"],"names":["constructor","descriptor","undefined","element","HTMLElement","Error","eventHandlers","Map","eventListeners","selectors","events","this","getEvents","create","addSelectors","reactive","dispatchEvent","CustomEvent","bubbles","detail","component","registerComponent","addEventListener","event","_event$detail","stopPropagation","registerChildComponent","destroy","getWatchers","stateReady","getElement","query","dataId","dataSelector","selector","querySelector","getElements","querySelectorAll","newSelectors","selectorName","Object","entries","getSelector","eventName","renderComponent","target","file","data","Promise","resolve","reject","_ref","renderForPromise","then","_ref2","html","js","replaceNodeContents","catch","error","type","listener","bindListener","get","bind","set","push","removeEventListener","removeAllEventListeners","forEach","_ref3","remove","unregister","unregisterComponen
t","dispatchRegistrationSuccess","parentNode","dispatchRegistrationFail","locked","setElementLocked","getElementLocked","dataset","style","pointerEvents","userSelect","hasAttribute","setAttribute","definition","_overlay","removeOverlay","content","css","classes","removeAllOverlays"],"mappings":"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"}