/** * Copyright Aquera Inc 2023 * * This source code is licensed under the BSD-3-Clause license found in the * LICENSE file in the root directory of this source tree. */ import type { ReactiveController, ReactiveControllerHost } from 'lit'; import type { NileAppShell } from './nile-app-shell'; /** Events on the shell that change what a region component renders. */ const SHELL_EVENTS = [ 'nile-nav-toggle', 'nile-panel-toggle', 'nile-panel-mode-change', ]; /** A reactive controller that links a region component to its shell, re-rendering the host when the shell's nav or panel state changes. */ export class AppShellController implements ReactiveController { host: ReactiveControllerHost & HTMLElement; /** Id of the shell to link to. Defaults to the nearest ancestor. */ shellId?: string; private linked?: NileAppShell; private observer?: MutationObserver; constructor(host: ReactiveControllerHost & HTMLElement) { (this.host = host).addController(this); } /** The linked shell, resolving it first if it has not been found yet. */ get shell(): NileAppShell | null { if (!this.linked) this.link(); return this.linked ?? null; } hostConnected(): void { // The shell can upgrade after its children; give it a microtask. Promise.resolve().then(() => { if (this.link()) this.host.requestUpdate(); }); } hostDisconnected(): void { this.unlink(); } /** Resolve the shell and subscribe to it. Returns true when newly linked. */ private link(): boolean { const found = this.find(); if (!found || found === this.linked) return false; this.unlink(); this.linked = found; SHELL_EVENTS.forEach(name => found.addEventListener(name, this.handleShellChange) ); /* Not every state a region renders from has an event, so watch the shell's reflected attributes to cover the whole surface. */ this.observer = new MutationObserver(this.handleShellChange); this.observer.observe(found, { attributes: true }); return true; } private unlink(): void { const shell = this.linked; if (!shell) return; SHELL_EVENTS.forEach(name => shell.removeEventListener(name, this.handleShellChange) ); this.observer?.disconnect(); this.observer = undefined; this.linked = undefined; } private find(): NileAppShell | null { if (this.shellId) { const root = this.host.getRootNode() as Document | ShadowRoot; return root.querySelector(`nile-app-shell#${this.shellId}`); } return this.host.closest('nile-app-shell'); } private handleShellChange = (): void => { this.host.requestUpdate(); }; } export default AppShellController;