UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

323 lines (322 loc) 11.2 kB
import { render as uRender, html as uHtml } from 'uhtml'; import I18nManager from '../tools/i18n/i18nmanager'; import ConfigManager from '../tools/configuration/configmanager'; import StateManager from '../tools/state/statemanager'; import ComponentManager from '../tools/state/componentManager'; import UserInteractionManager from '../tools/state/userInteractionManager'; import PluginManager from '../tools/auth/pluginmanager'; class GirafeHTMLElement extends HTMLElement { constructor(name) { super(); Object.defineProperty(this, "templateUrl", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "styleUrl", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "template", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "shadow", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "displayStyle", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "timeoutId", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "rendered", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "callbacks", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "configManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "stateManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "componentManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "userInteractionManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "unsafeCache", { enumerable: true, configurable: true, writable: true, value: new Map() }); this.name = name; this.configManager = ConfigManager.getInstance(); this.stateManager = StateManager.getInstance(); this.componentManager = ComponentManager.getInstance(); this.userInteractionManager = UserInteractionManager.getInstance(); this.componentManager.registerComponent(this); this.shadow = this.attachShadow({ mode: 'open' }); this.subscribe('language', () => this.girafeTranslate()); this.subscribe('oauth.userInfo', () => this.userInfoChanged()); } get state() { return this.stateManager.state; } getById(id) { return this.shadow.querySelector('#' + id); } async loadConfig() { await this.configManager.loadConfig(); } girafeTranslate() { I18nManager.getInstance().translate(this.shadow); } userInfoChanged() { PluginManager.getInstance().filterPlugins(this.shadow); } /** * NOTE REG: We cannot just use truthy here, because javascript comparaison table is really problematic. * For example: * 0 == false * [] == false * "" == false * And there are cases where we want to check null or undefined, because 0 can be a right value. * More here : https://dorey.github.io/JavaScript-Equality-Table/ * @param val * @returns */ isNullOrUndefined(val) { return val === undefined || val === null; } isNullOrUndefinedOrBlank(val) { return val === undefined || val === null || val === ''; } getParentOfType(parentNodeName, elem, initialElem = elem) { // Stop case : we found null or an object of the right type if (elem === null || (elem !== initialElem && elem.nodeName === parentNodeName)) { return elem; } // Otherwise, we try to find a parent recursively let parent = null; if (elem instanceof ShadowRoot) { parent = elem.host; } else { parent = elem.parentNode; } return this.getParentOfType(parentNodeName, parent, elem); } /** * Render the component's template. */ render() { // TODO REG : Reactivate this check and fix all the code. /*if (this.rendered) { throw Error('Component already rendered. Please call refreshRender() instead.'); }*/ if (this.template) { this.defineDisplayStyle(); this.show(); uRender(this.shadow, this.template); this.rendered = true; this.userInfoChanged(); } else { console.warn(`Cannot render: no template has been defined for component ${this.name}.`); } } /** * Re-Render the component. * The method should be called when the component * has already been rendered and needs to be updated. */ refreshRender() { if (!this.rendered) { throw Error('Component cannot be re-rendered. Please call render() first.'); } // Use a debouncing to prevent multiple execution of this method // If multiple refresh at the same time are called. if (this.timeoutId) { clearTimeout(this.timeoutId); } this.timeoutId = setTimeout(() => { uRender(this.shadow, this.template); this.girafeTranslate(); this.userInfoChanged(); }); } /** * Renders a hidden span with the name of the component. * Useful to render a placeholder for not visible component. */ renderEmpty() { this.defineDisplayStyle(); this.hide(); uRender(this.shadow, uHtml `<span style="display: none">${this.name}</span>`); this.rendered = false; } /** * Convert the string in parameter with uHtml and return it. * This allows to convert a string with html in a right html object. * For example, htmlUnsafe('<div></div>') will return an html div object. */ htmlUnsafe(str) { // NOTE REG: If this method is used much more in the future, we will have to take care of memory leaks // see discussion here: https://github.com/WebReflection/uhtml/issues/126 const template = this.getUnsafeTemplate(str); return uHtml(template); } /** * Convert a string to TemplateStringsArray * Manage a cache of all the created objects to limit memory usage */ getUnsafeTemplate(str) { let template = this.unsafeCache.get(str); if (!template) { template = [str]; this.unsafeCache.set(str, template); } return template; } /** * Remember the initial display configuration of the component * To be able to restore it */ defineDisplayStyle() { if (!this.displayStyle) { // Remember the initial display style this.displayStyle = getComputedStyle(this).display ?? 'block'; if (this.displayStyle === 'none') { this.displayStyle = 'block'; } } } /** * In the templates, sometimes for accessibility reasons, we have to support the KeyDown Event * In those case, we often juste want to do the same as the click event when Enter or Space is pressed * Then this method can be used : it just calls the click event on the same element */ simulateClick(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const target = e.target; if (target) { target.click(); } } } /** * Hide the component (display: none). */ hide() { this.style.display = 'none'; this.unregisterInteractionListeners(); } /** * Show the component (display: block). */ show() { if (this.displayStyle) { this.style.display = this.displayStyle; } } /** * Returns the serialization of the current element. This method should be * overwritten by child classes * @returns An object describing the current element serialized */ serialize() { return {}; } /** * Deserialize an element and set the current element state to the deserialized one * @param _serializedElement The element serialization as returned by the serialize method */ deserialize(_serializedElement) { } subscribe(path, callback) { // @ts-expect-error The call would have succeeded against this implementation, // but implementation signatures of overloads are not externally visible. const subscription = this.stateManager.subscribe(path, callback); this.callbacks.push(subscription); return subscription; } unsubscribe(callbacks) { (Array.isArray(callbacks) ? callbacks : [callbacks]).forEach((callback) => { const index = this.callbacks.findIndex((c) => c === callback); if (index >= 0) { this.stateManager.unsubscribe(callback); this.callbacks.splice(index, 1); } }); } /** * When the component is disconnected from the DOM * all the callbacks will be unregistered */ disconnectedCallback() { for (const callback of this.callbacks) { this.stateManager.unsubscribe(callback); } this.callbacks.length = 0; this.unregisterInteractionListeners(); } registerInteractionListener(eventName, isExclusive) { return this.userInteractionManager.registerListener(eventName, isExclusive, this.name); } unregisterInteractionListeners(eventNames) { if (!eventNames) { this.userInteractionManager.unregisterAllListenersOfTool(this.name); } if (!Array.isArray(eventNames)) { eventNames = [eventNames]; } eventNames.forEach((event) => this.userInteractionManager.unregisterListener(event, this.name)); } canExecute(eventName) { return this.userInteractionManager.canListenerExecute(eventName, this.name); } } export default GirafeHTMLElement;