egroupware_official/api/js/etemplate/Et2Widget/slot.ts
nathan a9fcf05fab Home: Fix portlet broken by Shoelace update to 1.8
They made HasSlotController internal only
2023-11-15 09:02:12 -07:00

130 lines
2.8 KiB
TypeScript

import type {ReactiveController, ReactiveControllerHost} from 'lit';
/**
* A reactive controller that determines when slots exist.
*
* Copied from Shoelace
* /src/internal/slot.ts
*/
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;
}