diff --git a/api/js/etemplate/Et2Portlet/Et2Portlet.ts b/api/js/etemplate/Et2Portlet/Et2Portlet.ts index afad5417ec..f9ccce69c8 100644 --- a/api/js/etemplate/Et2Portlet/Et2Portlet.ts +++ b/api/js/etemplate/Et2Portlet/Et2Portlet.ts @@ -17,7 +17,7 @@ import type {InteractEvent} from "@interactjs/core/InteractEvent"; import {egw} from "../../jsapi/egw_global"; import {css, html, TemplateResult} from "lit"; import {classMap} from "lit/directives/class-map.js"; -import type {HasSlotController} from "../../../../node_modules/@shoelace-style/shoelace/dist/internal/slot"; +import {HasSlotController} from "../Et2Widget/slot"; import shoelace from "../Styles/shoelace"; import {Et2Dialog} from "../Et2Dialog/Et2Dialog"; import {et2_IResizeable} from "../et2_core_interfaces"; @@ -81,8 +81,7 @@ export class Et2Portlet extends Et2Widget(SlCard) font-size: var(--sl-font-size-medium); line-height: var(--sl-line-height-dense); padding: 0px; - padding-left: var(--header-spacing); - padding-right: calc(2em + var(--header-spacing)); + padding-left: 0px; margin: 0px; position: relative; } @@ -111,8 +110,12 @@ export class Et2Portlet extends Et2Widget(SlCard) height: 100% } - .card_header { - margin-right: calc(var(--sl-spacing-medium) + 1em); + .card__header { + display: flex; + width: 100%; + padding: 0px; + padding-left: var(--sl-spacing-medium); + padding-right: calc(2em + var(--header-spacing)); } .card__body { diff --git a/api/js/etemplate/Et2Widget/slot.ts b/api/js/etemplate/Et2Widget/slot.ts new file mode 100644 index 0000000000..ee6f3dd29f --- /dev/null +++ b/api/js/etemplate/Et2Widget/slot.ts @@ -0,0 +1,129 @@ +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; +}