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;
}
}
}