Proyectos de Subversion Moodle

Rev

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 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 <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.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    /**\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    setElementLocked(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(target) {\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","unregisterComponent","dispatchRegistrationSuccess","parentNode","dispatchRegistrationFail","locked","setElementLocked","getElementLocked","dataset","style","pointerEvents","userSelect","hasAttribute","setAttribute","definition","_overlay","removeOverlay","content","css","classes","removeAllOverlays"],"mappings":"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"}