import type { ReactiveController, ReactiveControllerHost } from 'lit'; /** A reactive controller that determines when slots exist. */ export class HasSlotController implements ReactiveController { host: ReactiveControllerHost & Element; slotNames: string[] = []; constructor(host: ReactiveControllerHost & Element, ...slotNames: string[]) { (this.host = host).addController(this); this.slotNames = slotNames; } private hasDefaultSlot() { return [...this.host.childNodes].some(node => { if (node.nodeType === node.TEXT_NODE && node.textContent!.trim() !== '') { return true; } if (node.nodeType === node.ELEMENT_NODE) { const el = node as HTMLElement; const tagName = el.tagName.toLowerCase(); // Ignore visually hidden elements since they aren't rendered if (tagName === 'sl-visually-hidden') { return false; } // If it doesn't have a slot attribute, it's part of the default slot if (!el.hasAttribute('slot')) { return true; } } return false; }); } private hasNamedSlot(name: string) { return this.host.querySelector(`:scope > [slot="${name}"]`) !== null; } test(slotName: string) { return slotName === '[default]' ? this.hasDefaultSlot() : this.hasNamedSlot(slotName); } hostConnected() { this.host.shadowRoot!.addEventListener('slotchange', this.handleSlotChange); } hostDisconnected() { this.host.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange); } private handleSlotChange = (event: Event) => { const slot = event.target as HTMLSlotElement; if ((this.slotNames.includes('[default]') && !slot.name) || (slot.name && this.slotNames.includes(slot.name))) { this.host.requestUpdate(); } }; } /** * Given a slot, this function iterates over all of its assigned element and text nodes and returns the concatenated * HTML as a string. This is useful because we can't use slot.innerHTML as an alternative. */ export function getInnerHTML(slot: HTMLSlotElement): string { const nodes = slot.assignedNodes({ flatten: true }); let html = ''; [...nodes].forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { html += (node as HTMLElement).outerHTML; } if (node.nodeType === Node.TEXT_NODE) { html += node.textContent; } }); return html; } /** * Given a slot, this function iterates over all of its assigned text nodes and returns the concatenated text as a * string. This is useful because we can't use slot.textContent as an alternative. */ export function getTextContent(slot: HTMLSlotElement | undefined | null): string { if (!slot) { return ''; } const nodes = slot.assignedNodes({ flatten: true }); let text = ''; [...nodes].forEach(node => { if (node.nodeType === Node.TEXT_NODE) { text += node.textContent; } }); return text; }