import { APIConfiguration } from "@supersoniks/concorde/core/utils/api"; import DataBindObserver from "@supersoniks/concorde/core/utils/DataBindObserver"; import HTML from "@supersoniks/concorde/core/utils/HTML"; import Objects from "@supersoniks/concorde/core/utils/Objects"; import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy"; import { LitElement, PropertyValues } from "lit"; import { property, state } from "lit/decorators.js"; import WordingDirective from "../directives/Wording"; import { PublisherInterface, TypeAndRecordOfType, ConcordeWindow, MixinArgsType, CoreJSType, } from "@supersoniks/concorde/core/_types/types"; declare const window: ConcordeWindow; type Constructor = new (...args: MixinArgsType[]) => T; let keepDebugOnMouseOut = false; let debugs = new Set(); export interface SubscriberInterface { props: PropsType | null; propertyMap: object; isConnected: boolean; children: HTMLCollection; appendChild(node: Node): Node; getAncestorAttributeValue(attributeName: string): string; hasAncestorAttribute(attributeName: string): boolean; querySelectorAll(selector: string): NodeListOf; publisher: TypeAndRecordOfType>; dataProvider: string | null; noShadowDom: string | null; debug: HTMLElement | null; defferedDebug: boolean | null; displayContents: boolean; shadowRoot?: ShadowRoot; shouldRenderLazy: boolean; dispatchEvent(event: Event): void; setAttribute(name: string, value: string): void; addEventListener( type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions ): void; removeEventListener( type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions ): void; removeAttribute(name: string): void; initPublisher(): void; getApiConfiguration(): APIConfiguration; connectedCallback(): void; requestUpdate(): void; getAttribute(name: string): string; hasAttribute(attributeName: string): boolean; disconnectedCallback(): void; getBoundingClientRect(): DOMRect; } const Subscriber = < PropsType = CoreJSType, T extends Constructor = Constructor >( superClass: T, type?: PropsType ) => { /** * La mixin Subscriber permet lier un composant à un publisher. * La liaison à un publisher se fait via l'attribut *dataProvider* du composant qui représente ce que l'on obtient en appellant PublisherManager.get(dataProvider). * les propriétés du composant sont automatiquement remplies avec les propriétés du même nom dans les données du publisher. * Le composant est automatiquement mis à jour lorsque les données du publisher sont mises à jour. */ type; class SubscriberElement extends superClass { static instanceCounter = 0; publisher?: TypeAndRecordOfType>; constructor(...args: MixinArgsType[]) { super(); args; } @property({ type: Number }) collectDependenciesVersion = 0; @property({ type: Boolean }) displayContents = false; /** * noAutoFill permet de désactiver le remplissage automatique des propriétés par le publisher dans le cas ou on utilise "props" seulement ou le dataBinding par exemple */ @property({ type: Boolean }) noAutoFill = false; @property({ type: Boolean }) forceAutoFill = false; /** * Passer ce paramètre à true permet de ne pas mettre à jour le composant lors d'un changement de interne de la propriété nommé props. */ renderOnPropsInternalChange = false; /** * Par défaut on chée un shadow dom mais on peut demander à ne pas en avoir via cette propriété et un attribut associé. * Cela se fait à l'initialisation uniquement et n'est pas modifiable lors de la vie du composant. */ noShadowDom: string | null = null; /** * Permet de mapper un nom de propriété de donnée source vars une propriété du subscriber à la volée */ @property({ type: Object }) propertyMap: object | null = null; @property({ type: String, attribute: "data-title" }) title = ""; /** * va de parent en parent pour trouver un attribut * @param attributeName nom de l'attribut * @returns true si l'attribut est trouvé */ hasAncestorAttribute(attributeName: string) { return this.getAncestorAttributeValue(attributeName) != null; } /** * Va de parent en parent pour trouver un attribut * @param attributeName nom de l'attribut * @returns valeur de l'attribut ou null si l'attribut n'est pas trouvé */ getAncestorAttributeValue(attributeName: string) { return HTML.getAncestorAttributeValue(this, attributeName); } /** * L'id / l'adresse du publisher accessible via PublisherManager.get(dataProvider) */ @property({ reflect: true }) dataProvider: string | null = null; /** * On peut utiliser cette fonction pour lier un publisher spécifique au composant si besoin. * voir l'utilisation dans list.ts */ @property() bindPublisher: (() => PublisherInterface) | null = null; /** * Les props du composant. * Elles sont injectées en profondeur dans le publisher pour permettre la mutualisation des données entre les composants. * Par conséquent l'assignation de ces propriété avec une chaine json (html classic), ou un objet / un tableaun remplie les données du publisher. * Les propriétés des subscribers associés au même dataProvider sont donc "remplies" avec ces données, voir aussi le dataBinding à ce sujet. */ protected _props: PropsType | null = null; @property() get props() { if (this._props !== null || !this.publisher) return this._props; return this.publisher.get(); } set props(value) { if ( typeof value == "string" && ["{", "["].includes(value.trim().charAt(0)) ) { value = JSON.parse(value); } if (value == this._props) return; this._props = value; // if (!this.publisher) this.initPublisher(); if (this.publisher && this.publisher.get() != value) { this.publisher.set(value); } this.requestUpdate(); } protected updated(_changedProperties: PropertyValues): void { super.updated(_changedProperties); const ref = this.shadowRoot || this; const children = [...ref.children].filter( (child) => child.tagName != "STYLE" ); const display = this.displayContents ? "contents" : children.length == 0 ? "none" : null; if (display) this.style.display = display; else this.style.removeProperty("display"); } @state() shouldRenderLazy = true; connectedCallback() { SubscriberElement.instanceCounter++; if (this.hasAttribute("lazyRendering")) { const options: IntersectionObserverInit = { root: null, // rootMargin: Math.max(window.innerWidth, window.innerHeight) + "px", threshold: 0.9, }; let firstView = true; const iObserver = new IntersectionObserver((entries) => { for (const e of entries) { if (firstView && e.isIntersecting) { firstView = false; iObserver.disconnect(); this.initWording(); this.shouldRenderLazy = false; this.startPublisher(); break; } } }, options); iObserver.observe(this); } else { this.initWording(); this.shouldRenderLazy = false; } this.initPublisher(); this.addDebugger(); super.connectedCallback(); } disconnectedCallback() { this.removeDebugger(); super.disconnectedCallback(); if (this.publisher) { this.publisher.stopTemplateFilling(this); this.publisher.offInternalMutation(this.requestUpdate); } WordingDirective.publisher.stopTemplateFilling(this); if (this.onAssign) this.publisher?.offAssign(this.onAssign); } defferedDebug: true | null = null; /** * Ajoute un debugger au composant pour afficher en temps réel les données présentes dans sont publisher */ debug: HTMLElement | null = null; addDebugger() { if (this.hasAttribute("debug") && !this.defferedDebug) { if (!this.debug) { this.debug = document.createElement("div"); const style = this.debug.style; style.position = "fixed"; style.top = "0"; style.right = "0"; style.margin = "auto"; style.borderRadius = ".7rem"; style.backgroundColor = "#0f1729"; style.color = "#c5d4f9"; style.padding = "16px 16px"; style.margin = "16px 16px"; style.boxShadow = "0 10px 30px -18px rgba(0,0,0,.3)"; style.overflowY = "auto"; style.zIndex = "99999999"; style.maxHeight = "calc(100vh - 32px)"; style.fontFamily = "Consolas, monospace"; style.maxWidth = "min(50vw,25rem)"; style.fontSize = "12px"; style.minWidth = "300px"; style.overflowWrap = "break-word"; style.resize = "vertical"; } this.addEventListener("click", (e: MouseEvent) => { if (!e.ctrlKey) return; e.preventDefault(); keepDebugOnMouseOut = !keepDebugOnMouseOut; }); if (this.dataProvider) { window[this.dataProvider] = this.publisher; } this.addEventListener("mouseover", () => { if (!keepDebugOnMouseOut) this.removeDebugger(); document.body.appendChild(this.debug as Node); debugs.add(this.debug as HTMLElement); }); this.addEventListener("mouseout", () => { if (!keepDebugOnMouseOut) this.removeDebugger(); }); this.publisher?.onInternalMutation(() => { ( this.debug as HTMLElement ).innerHTML = `🤖 DataProvider : "${ this.dataProvider }"
Variable disponible dans la console
ctrl + Clique : épingler / désépingler
${JSON.stringify(
            this.publisher?.get(),
            null,
            "  "
          )}
`; }); } } removeDebugger() { debugs.forEach((debug) => { if (document.body.contains(debug)) document.body.removeChild(debug); }); debugs = new Set(); } /** * Petite fonction utilitaire pour retourner la configuration a passer à l'utilitaire API * Utilisée pour la configuration du wording / de la traduction ainsi que par le mixin fetcher par exemple * A voir si on le bouge dans un utilitaire */ getApiConfiguration(): APIConfiguration { return HTML.getApiConfiguration(this); } /** * Initialise le remplisage automatique des traductions / du wording * Un publisher spécifique est créé pour le composant à l'adresse "sonic-wording" * Le composant recherche la valeur de l'attribute "wordingProvider" que contient le point d'accès à l'api de traduction / libellés * Il utilise ce service et le publisher créé pour remplir automatiquement toutes les propriétés préfixées avec "wording_". */ async initWording() { const propNames = Object.getOwnPropertyNames(this.constructor.prototype); for (const p of propNames) { if (p.indexOf("wording_") == 0) { WordingDirective.callApi(this, p.substring(8)); } } WordingDirective.publisher.startTemplateFilling(this); } /** * * Fonction native de lit surchargée pour la gestion du mode noShadowDom * Le comportement de data binding est également créé ici va l'utilitaire DataBindObserver */ createRenderRoot() { if (this.noShadowDom === "" || this.getAttribute("noShadowDom") === "") { return this; } const shadowRoot = super.createRenderRoot(); DataBindObserver.observe(shadowRoot as HTMLElement); return shadowRoot; } /** * On assign est enregistré car c'est un écouteur du publisher qui doit être délié lorsque l'objet est déconnecté du dom. */ private onAssign = (v: PropsType) => { this.props = v; }; initPublisher() { if (!document) return; if (this.publisher) { this.publisher.stopTemplateFilling(this); this.publisher.offInternalMutation(this.requestUpdate); if (this.onAssign) this.publisher.offAssign(this.onAssign); } const mng = PublisherManager.getInstance(); if (!this.dataProvider) this.dataProvider = this.getAncestorAttributeValue("dataProvider"); let publisherId = this.dataProvider; if (!publisherId && this._props) { this.dataProvider = publisherId = "__subscriber__" + SubscriberElement.instanceCounter; } if (publisherId) { if (this.bindPublisher) { mng.set(publisherId, this.bindPublisher()); } let pub = mng.get(publisherId, { localStorageMode: this.getAttribute("localStorage") || "disabled", invalidateOnPageShow: this.hasAttribute("invalidateOnPageShow"), }); this.dataProvider = publisherId; if (this.hasAttribute("subDataProvider")) { const dataPath: string = this.getAttribute( "subDataProvider" ) as string; this.dataProvider = publisherId + "/" + dataPath; pub = Objects.traverse(pub, dataPath.split(".")); mng.set(this.dataProvider, pub); this.publisher = pub; } this.publisher = pub; } if (this.hasAttribute("lazyRendering")) { return; } else { this.startPublisher(); } } startPublisher() { if (this.publisher) { if (this._props) { this.publisher.set(this._props); } if (!this.noAutoFill) this.publisher.startTemplateFilling(this); if (this.renderOnPropsInternalChange) this.publisher.onInternalMutation(this.requestUpdate); this.publisher.onAssign(this.onAssign); } } } return SubscriberElement as Constructor> & T; }; export default Subscriber;