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