import default_library from "@supersoniks/concorde/core/components/functional/sdui/default-library.json"; import SDUIDescriptorTransformer from "@supersoniks/concorde/core/components/functional/sdui/SDUIDescriptorTransformer"; import { SDUIDescriptor, SDUINode, SDUITransformDescription, } from "@supersoniks/concorde/core/components/functional/sdui/types"; import { Fetcher, Subscriber } from "@supersoniks/concorde/mixins"; import { HTML, Objects } from "@supersoniks/concorde/utils"; import { LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators.js"; const tagName = "sonic-sdui"; // For Astro.build /** * ### sonic-sdui (Server Driven User Interface) est un fetcher chargant un JSON décrivant une interface utilisateur * * Extends mixins : Fetcher, [Subscriber](./?path=/docs/miscallenous-🔔-subscriber--page) * * * Si le composant possède un attribut *fetch*, il charge un contenu via un appel d'api web.
* Voir [fetcher](./?path=/docs/core-components-functional-fetch--basic) pour la configuration des autres attributs. * * Le format du JSON est décrit par le type SDUIDescriptor * * Un attribut supplémentaire `transformation` permet de transformer le json reçu avant la génération de l'interface utilisateur
* Son format est décrit par le type SDUITransformDescription * * * Si le résultat de la requête est un objet, il est imbriqué dans un tableau pour garantir le fonctionnement.
* * */ @customElement(tagName) export class SonicSDUI extends Fetcher(Subscriber(LitElement)) { connectedCallback(): void { this.noShadowDom = ""; this.displayContents = true; this.isFetchEnabled = this.hasAttribute("fetch"); super.connectedCallback(); } @property() sduiKey?: string; @property() messagesKey?: string; private sduiDescriptor: SDUIDescriptor = {}; /** * On peut passer la description sous form de props, sinon il faut utiliser l'attribut fetch */ willUpdate(changedProperties: PropertyValues): void { if (this.props == null) { this.sduiDescriptor = {}; } { const newSduiDescriptor = this.sduiKey ? (this.props as Record)[this.sduiKey] : (this.props as SDUIDescriptor); if (this.sduiDescriptor == newSduiDescriptor) return; this.sduiDescriptor = newSduiDescriptor; this.updateContents(); } super.willUpdate(changedProperties); } /** * updateContents est déclenché quand les sduiDescriptor sont renseignées * Le contenu du composant est regénéré en fonction du descripteur fourni */ async updateContents() { if (!this.sduiDescriptor) return; const library: Record = {}; Object.assign(library, default_library, this.sduiDescriptor.library); this.sduiDescriptor.library = library; this.loadAssets(); await this.loadLibrary(); await this.transformSDUIDescriptor(); this.parseRootNodes(); } /** * Suppressiond du contenu du composant avant le génération de la nouvelle ui */ private removeChildren() { while ( [...this.children].filter((elt) => elt.nodeName != "SLOT").length > 0 ) { this.removeChild(this.children[0]); } } /** * Chargement de fichiers js et css associés si besoin **/ private loadAssets() { if (!this.sduiDescriptor) return; if (this.sduiDescriptor.js) { for (const src of this.sduiDescriptor.js) HTML.loadJS(src); } if (this.sduiDescriptor.css) { for (const src of this.sduiDescriptor.css) HTML.loadCSS(src); } } /** * Transformation de la data fournie via sduiDescriptor si il y a un attribut transformation * */ private async transformSDUIDescriptor() { if (!this.hasAttribute("transformation")) return; const result = await fetch(this.getAttribute("transformation")); const json: SDUITransformDescription = await result.json(); const transformer = new SDUIDescriptorTransformer(); await transformer.transform(this.sduiDescriptor, json); } /** * Charge la library à utiliser * */ private async loadLibrary() { if (!this.hasAttribute("library")) return; const result = await fetch(this.getAttribute("library")); const json: Record = await result.json(); this.sduiDescriptor.library = json; } /** * Point d'entrée : transformation des noeuds fournis en éléments graphiques **/ private parseRootNodes() { this.removeChildren(); if (!this.sduiDescriptor) return; let nodes = this.sduiDescriptor.nodes; if (!nodes) nodes = []; const messageProvider = { tagName: "sonic-toast-message-subscriber", attributes: {}, }; if (this.messagesKey) { messageProvider.attributes = { subDataProvider: this.messagesKey }; } nodes.push(messageProvider); nodes.forEach((node) => this.appendChild(this.parseChild(node))); } /** * On parse un noeud ce qui crée un éléments graphique et ses enfants par recursivité via `handleChildNodes` */ private parseChild(node: SDUINode) { const tagName = node.tagName || "div"; let { element, contentElement } = this.handleLibrary(node, tagName); this.handleAttributes(node, element); element = this.handleMarkup(node, element); if (!contentElement) contentElement = element; this.handleChildNodes(node, contentElement, element); this.handleInnerHTML(node, contentElement); if (node.prefix || node.suffix) { const container = this.handlePrefixSuffix(node, element); return container; } return element; } /** * Si le noeud courant a des propriétés prefix et ou suffix il est entouré des markups fournis dans ces sduiDescriptor. * Le tout est inclu dans une div en display contents */ private handlePrefixSuffix(node: SDUINode, element: HTMLElement) { const container = document.createElement("div"); container.innerHTML = (node.prefix || "") + element.outerHTML + (node.suffix || ""); container.style.display = "contents"; return container; } /** * Création des enfants du noeud courant * Si l'enfant à un attribut parentElementSelector, il est ajouté dans le noeud correspondant au sélecteur css associé et non pas dans l'élément directement. */ private handleChildNodes( node: SDUINode, contentElement: HTMLElement, element: HTMLElement ) { if (node.nodes) { const children: Array = node.nodes; for (const child of children) { const childElement = this.parseChild(child); let nodeToAppendOn = contentElement; if (child.parentElementSelector) { nodeToAppendOn = element.querySelector(child.parentElementSelector) || contentElement; } if (nodeToAppendOn.shadowRoot) nodeToAppendOn.shadowRoot.appendChild(childElement); else if (nodeToAppendOn.tagName.toLocaleLowerCase() == "template") { const template: HTMLTemplateElement = nodeToAppendOn as HTMLTemplateElement; template.content.appendChild(childElement); } else nodeToAppendOn.appendChild(childElement); } } } /** * Gestion de librarie : * * Si l'élément référence un élément de la librairie, on se sert de cet élément comme model pour créer le composant graphique. * * Sa propriété contentElement retournée vaut element par défaut. * * Si contentElementSelector est definit, alors contentElement correspond à l'élément obtenu par selection css d'après la valeurs de contentElementSelector * * Les éléments enfants seront ensuite ajoutés dans contentElement */ private handleLibrary(node: SDUINode, tagName: string) { let element: HTMLElement; let contentElement: HTMLElement | undefined; if (node.libraryKey && this.sduiDescriptor.library) { element = this.parseChild( this.sduiDescriptor.library[node.libraryKey] || { tagName: "div" } ); const selector = (this.sduiDescriptor.library[node.libraryKey] || {}) .contentElementSelector; if (selector) contentElement = element.querySelector(selector) as HTMLElement; } else element = document.createElement(tagName) as HTMLElement; return { element, contentElement }; } /** * Remplissage des attributs html avec les attributs fournis dans le noeud */ private handleAttributes(node: SDUINode, element: HTMLElement) { const attributes = node.attributes; for (const k in attributes) { const attrData: object | string = attributes[k]; const attr: string = Objects.isObject(attrData) ? JSON.stringify(attrData) : attrData; element.setAttribute(k, attr); } } /** * Si une propriété markup est fournie, l'élément est créé à partir de la chaine html fournie avant d'être configuré */ private handleMarkup(node: SDUINode, element: HTMLElement) { if (node.markup) { element = document.createElement("div"); element.style.display = "contents"; element.innerHTML = node.markup; } return element; } /** * si le noeud à une propriété innerHTML, on l'ajout ay innerHTML de l'élément html en cours de création */ private handleInnerHTML( node: SDUINode, contentElement: HTMLElement | undefined ) { if (!node.innerHTML) return; if (node.innerHTML.indexOf("wording_") != -1) { const wordingProvider = this.getAncestorAttributeValue("wordingProvider"); this.api ?.post(wordingProvider, { labels: [node.innerHTML.substring(8)] }) .then((value) => { if (contentElement) contentElement.innerHTML += value; }); } else if (contentElement) { contentElement.innerHTML += node.innerHTML; } } }