import { LitElement, html } from "lit"; import { property } from "lit/decorators.js"; import { IRegion, regionManager } from "@uxland/regions"; import { PrimariaRegionHost } from "../../../api/api"; import { regionsProperty } from "@uxland/regions/region-decorator"; /** * PrimariaRegion Web Component * * A plug-and-play region component that allows dynamic region creation without * needing to extend PrimariaRegionHost or use the @region decorator manually. * * Renders in light DOM (no shadow-root) to allow region content to be properly * displayed in the parent's DOM tree. * * @example * ```html * * ``` * * This component internally handles: * - Creating a container with the appropriate ID * - Setting up the region definition * - Managing the region lifecycle */ export class PrimariaRegion extends PrimariaRegionHost(LitElement) { /** * The name of the region to create. * This will be used both as the region name in the region manager * and to generate the container ID. */ @property({ type: String }) name = ""; /** * Rendering mode for the region. * - "multi" (default): all registered views are shown simultaneously (MultipleActiveAdapter). * - "single": only one view is shown at a time (SelectableAdapter via primaria-content-switcher). * Each plugin activates its own view; the adapter deactivates the rest automatically. */ @property({ type: String }) mode: "multi" | "single" = "multi"; /** * Render in light DOM instead of shadow DOM. * This allows the region content to be visible in the parent's DOM tree. */ createRenderRoot() { return this; } /** * Override shadowRoot getter to return this (light DOM) instead of null. * The region mixin tries to find elements using shadowRoot.querySelector(), * so we need to make it work with light DOM. */ get shadowRoot(): ShadowRoot | null { return this as any; } /** * The region instance created by the region host mixin. * Plugins can inject content into this region using the region manager. */ region: IRegion | undefined; /** * Virtual constructor for this instance to hold its own region definition. * This prevents conflicts when multiple primaria-region instances exist. */ private _instanceConstructor: any; constructor() { super(); // Create a unique constructor object for this instance // This allows each instance to have its own region definition // without affecting other instances of the same component this._instanceConstructor = Object.create(this.constructor); // Override the constructor property to return our instance constructor Object.defineProperty(this, "constructor", { get: () => this._instanceConstructor, configurable: true, }); } /** * Called when the component is connected to the DOM. * Sets up the region definition before the parent connectedCallback runs. */ connectedCallback(): void { // First, call the base LitElement connectedCallback but NOT the mixin yet // We need to do this in a special order LitElement.prototype.connectedCallback.call(this); if (this.name) { const targetId = `${this.name}-container`; // Create the container element based on mode. // "single" uses primaria-content-switcher so the region mixin picks up // selectableAdapterFactory (already registered in UI bootstrapper), giving // SingleActiveAdapter behaviour: activating one view auto-deactivates the rest. const container = this.mode === "single" ? document.createElement("primaria-content-switcher") : document.createElement("div"); container.id = targetId; if (this.mode !== "single") { (container as HTMLElement).style.cssText = "width: 100%; height: 100%; min-height: 1px"; } this.appendChild(container); // Set the region metadata directly on the instance constructor this._instanceConstructor[regionsProperty] = { ...this._instanceConstructor[regionsProperty], region: { targetId, name: this.name }, }; // The mixin creates regions in the `updated()` lifecycle, not in connectedCallback // So we need to trigger the updated lifecycle or call createRegions directly // Let's check if the component has the create method from the mixin if ((this as any).create) { (this as any).create(); } // Also manually call the mixin's connectedCallback if it exists const litProto = Object.getPrototypeOf(this); const mixinProto = Object.getPrototypeOf(litProto); if (mixinProto.connectedCallback) { mixinProto.connectedCallback.call(this); } } } /** * Mixin hook fired after `createRegions` finishes. * * Hydrates the freshly-created region with every view that has already been * registered for this region name via `regionManager.registerViewWithRegion`. * `registerViewWithRegion` only forwards to regions that exist at the moment * of the call, so a plugin that registers once at `initialize` would lose * its view every time the region is destroyed and re-created (e.g. when a * drawer that hosts the region is closed and reopened). Pulling from the * registry here makes injection transparent: any plugin can register once * and any region with that name auto-populates. */ regionsCreated(_regions: unknown): void { if (!this.region || !this.name) return; const registered = regionManager.getRegisteredViews(this.name) ?? []; for (const { key, view } of registered) { if (this.region.containsView(key)) continue; this.region.addView(key, view).catch((e) => { console.warn(`primaria-region(${this.name}): failed to addView "${key}"`, e); }); } } /** * Called when the component is removed from the DOM. * * The base mixin's `disconnectedCallback` removes the region from the manager * but does NOT clear `this.region`, so on a later reconnect the mixin's * `createRegions` sees `isNil(this.region) === false` and skips re-creating * the region. That breaks the "drawer opens, closes, opens again" flow: * the second open would render the host but never receive the plugin's * view. Clear the reference (and drop the orphan container) so the next * connect rebuilds everything from scratch. */ disconnectedCallback(): void { super.disconnectedCallback(); this.region = undefined; const targetId = `${this.name}-container`; this.querySelector(`#${targetId}`)?.remove(); } /** * Called before the component updates. * Updates the region definition if the name changes. */ protected willUpdate(changedProperties: Map): void { super.willUpdate(changedProperties); // If the name changed after connection, update the region definition if (this.name && changedProperties.has("name")) { const targetId = `${this.name}-container`; this._instanceConstructor[regionsProperty] = { ...this._instanceConstructor[regionsProperty], region: { targetId, name: this.name }, }; } } /** * Renders nothing because we create the container div manually in connectedCallback. * This is necessary because the mixin needs the div to exist before it tries to create the region. */ render() { // Don't render anything - the container is created in connectedCallback return html``; } }