import HTML from "../utils/HTML";
/** Event dispatched when a component's connectedCallback has completed. Can be listened to from anywhere (bubbles). */
export const CONNECTED = "sonic-connected";
const READY_TIMEOUT_MS = 5000;
const CONNECTED_SYMBOL = Symbol.for("sonic-connected");
function isWebComponent(element: Element): boolean {
return element.tagName.includes("-");
}
function waitForAncestorReady(
ancestor: Element,
timeoutMs = READY_TIMEOUT_MS,
): Promise {
if (!isWebComponent(ancestor)) return Promise.resolve();
if ((ancestor as any)[CONNECTED_SYMBOL]) return Promise.resolve();
return new Promise((resolve) => {
const handler = () => {
(ancestor as any)[CONNECTED_SYMBOL] = true;
ancestor.removeEventListener(CONNECTED, handler);
resolve();
};
ancestor.addEventListener(CONNECTED, handler);
setTimeout(() => {
ancestor.removeEventListener(CONNECTED, handler);
resolve();
}, timeoutMs);
});
}
/**
* Décorateur de classe qui retarde le connectedCallback jusqu'à ce que tous les ancêtres
* correspondant aux sélecteurs CSS fournis aient exécuté leur connectedCallback.
* Les nœuds qui ne sont pas des web components sont considérés comme connectés par défaut.
* S'applique uniquement aux web components (classes étendant HTMLElement).
*/
export function awaitConnectedAncestors(...selectors: string[]) {
return function (
constructor: T,
): T {
if (!selectors.length) return constructor;
const originalConnectedCallback = constructor.prototype.connectedCallback;
constructor.prototype.connectedCallback = function (this: HTMLElement) {
const ancestors = HTML.getAncestorsBySelectors(this, selectors);
const webComponentAncestors = ancestors.filter(isWebComponent);
const whenDefinedPromises = [
...new Set(webComponentAncestors.map((a) => a.tagName.toLowerCase())),
].map((tag) => customElements.whenDefined(tag));
const readyPromises = ancestors.map((a) => waitForAncestorReady(a));
Promise.all([...whenDefinedPromises, ...readyPromises]).then(() => {
originalConnectedCallback?.call(this);
});
};
return constructor;
};
}
/**
* Décorateur de classe pour les ancêtres qui veulent signaler qu'ils sont prêts.
* Dispatche l'événement "sonic-connected" à la fin du connectedCallback (bubbles).
*/
export function dispatchConnectedEvent() {
return function (
constructor: T,
): T {
const original = constructor.prototype.connectedCallback;
constructor.prototype.connectedCallback = function (this: HTMLElement) {
original?.call(this);
(this as any)[CONNECTED_SYMBOL] = true;
this.dispatchEvent(new CustomEvent(CONNECTED, { bubbles: true }));
};
return constructor;
};
}