2022-01-16 05:47:14 +00:00
|
|
|
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
2022-01-05 23:31:41 +00:00
|
|
|
|
2023-02-06 15:46:37 +00:00
|
|
|
/** A reactive controller that determines when slots exist. */
|
2022-01-05 23:31:41 +00:00
|
|
|
export class HasSlotController implements ReactiveController {
|
|
|
|
host: ReactiveControllerHost & Element;
|
|
|
|
slotNames: string[] = [];
|
|
|
|
|
2022-01-06 14:04:02 +00:00
|
|
|
constructor(host: ReactiveControllerHost & Element, ...slotNames: string[]) {
|
2022-01-05 23:31:41 +00:00
|
|
|
(this.host = host).addController(this);
|
|
|
|
this.slotNames = slotNames;
|
|
|
|
this.handleSlotChange = this.handleSlotChange.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2022-03-16 21:37:42 +00:00
|
|
|
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
|
2022-01-05 23:31:41 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleSlotChange(event: Event) {
|
|
|
|
const slot = event.target as HTMLSlotElement;
|
|
|
|
|
2022-01-19 14:37:07 +00:00
|
|
|
if ((this.slotNames.includes('[default]') && !slot.name) || (slot.name && this.slotNames.includes(slot.name))) {
|
2022-01-05 23:31:41 +00:00
|
|
|
this.host.requestUpdate();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-16 05:47:14 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2020-07-15 21:30:37 +00:00
|
|
|
export function getInnerHTML(slot: HTMLSlotElement): string {
|
|
|
|
const nodes = slot.assignedNodes({ flatten: true });
|
|
|
|
let html = '';
|
|
|
|
|
2022-01-16 05:47:14 +00:00
|
|
|
[...nodes].forEach(node => {
|
2020-07-15 21:30:37 +00:00
|
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
|
|
html += (node as HTMLElement).outerHTML;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
|
|
html += node.textContent;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return html;
|
|
|
|
}
|
|
|
|
|
2022-01-16 05:47:14 +00:00
|
|
|
/**
|
|
|
|
* 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 {
|
2022-01-19 14:37:07 +00:00
|
|
|
if (!slot) {
|
2022-01-16 05:47:14 +00:00
|
|
|
return '';
|
|
|
|
}
|
|
|
|
const nodes = slot.assignedNodes({ flatten: true });
|
2020-07-15 21:30:37 +00:00
|
|
|
let text = '';
|
|
|
|
|
2022-01-16 05:47:14 +00:00
|
|
|
[...nodes].forEach(node => {
|
2020-07-15 21:30:37 +00:00
|
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
|
|
text += node.textContent;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return text;
|
|
|
|
}
|