2024-08-07 21:18:10 +02:00
|
|
|
import {css, html, LitElement, nothing, PropertyValues} from "lit";
|
|
|
|
import {SlIcon, SlMenu, SlMenuItem} from "@shoelace-style/shoelace";
|
2024-06-15 00:22:31 +02:00
|
|
|
import {egwMenuItem} from "./egw_menu";
|
|
|
|
import {customElement} from "lit/decorators/custom-element.js";
|
|
|
|
import {repeat} from "lit/directives/repeat.js";
|
|
|
|
import {classMap} from "lit/directives/class-map.js";
|
2024-08-09 14:28:14 +02:00
|
|
|
import {state} from "lit/decorators/state.js";
|
2024-06-15 00:22:31 +02:00
|
|
|
|
|
|
|
@customElement("egw-menu-shoelace")
|
|
|
|
export class EgwMenuShoelace extends LitElement
|
|
|
|
{
|
|
|
|
static get styles()
|
|
|
|
{
|
|
|
|
return [
|
|
|
|
css`
|
|
|
|
:host {
|
|
|
|
display: block;
|
|
|
|
}
|
|
|
|
|
|
|
|
.default-item::part(label) {
|
|
|
|
font-weight: var(--sl-font-weight-bold, bold);
|
|
|
|
}
|
|
|
|
|
2024-07-17 18:10:24 +02:00
|
|
|
sl-menu {
|
|
|
|
box-shadow: var(--sl-shadow-x-large);
|
|
|
|
}
|
|
|
|
|
2024-07-11 19:27:31 +02:00
|
|
|
sl-menu-item::part(base) {
|
|
|
|
height: 1.6em;
|
|
|
|
line-height: var(--sl-line-height-dense);
|
|
|
|
align-items: center;
|
|
|
|
padding: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
sl-menu-item::part(prefix) {
|
|
|
|
min-width: var(--sl-spacing-2x-large);
|
|
|
|
}
|
|
|
|
|
2024-08-07 21:18:10 +02:00
|
|
|
/* Customise checkbox menuitem */
|
|
|
|
|
|
|
|
sl-menu-item[type="checkbox"]::part(checked-icon) {
|
2024-08-09 14:28:14 +02:00
|
|
|
visibility: hidden;
|
2024-08-07 21:18:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sl-menu-item[type="checkbox"]:not([checked])::part(checked-icon) {
|
|
|
|
color: var(--sl-color-neutral-300);
|
|
|
|
}
|
|
|
|
|
2024-06-15 00:22:31 +02:00
|
|
|
et2-image {
|
2024-07-11 19:27:31 +02:00
|
|
|
line-height: normal;
|
2024-06-15 00:22:31 +02:00
|
|
|
width: 1.5em;
|
|
|
|
}
|
|
|
|
`
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
private structure = [];
|
|
|
|
private popup = null;
|
|
|
|
private removeCallback = null;
|
|
|
|
|
|
|
|
private get menu() : SlMenu { return this.shadowRoot?.querySelector("sl-menu");}
|
|
|
|
|
|
|
|
constructor(_structure : egwMenuItem[])
|
|
|
|
{
|
|
|
|
super();
|
|
|
|
this.structure = _structure;
|
2024-07-09 23:35:46 +02:00
|
|
|
|
|
|
|
this.handleDocumentClick = this.handleDocumentClick.bind(this);
|
|
|
|
this.handleKeypress = this.handleKeypress.bind(this);
|
2024-06-15 00:22:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
connectedCallback()
|
|
|
|
{
|
|
|
|
super.connectedCallback();
|
2024-07-09 23:35:46 +02:00
|
|
|
|
|
|
|
document.addEventListener("click", this.handleDocumentClick);
|
|
|
|
document.addEventListener("keydown", this.handleKeypress);
|
2024-06-15 00:22:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
disconnectedCallback()
|
|
|
|
{
|
|
|
|
super.disconnectedCallback();
|
2024-07-09 23:35:46 +02:00
|
|
|
document.removeEventListener("click", this.handleDocumentClick);
|
|
|
|
document.removeEventListener("keydown", this.handleKeypress);
|
2024-06-15 00:22:31 +02:00
|
|
|
if(this.popup)
|
|
|
|
{
|
|
|
|
this.popup.remove();
|
|
|
|
this.popup = null;
|
|
|
|
}
|
|
|
|
if(this.removeCallback)
|
|
|
|
{
|
|
|
|
this.removeCallback.call();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-07 21:18:10 +02:00
|
|
|
protected updated(_changedProperties : PropertyValues)
|
|
|
|
{
|
|
|
|
super.updated(_changedProperties);
|
|
|
|
|
|
|
|
// Checkbox indicators
|
|
|
|
this.shadowRoot.querySelectorAll("sl-menu-item[type=checkbox]").forEach(async(item : SlMenuItem) =>
|
|
|
|
{
|
|
|
|
await item.updateComplete;
|
|
|
|
const icon : SlIcon = item.shadowRoot.querySelector("[part=\"checked-icon\"] sl-icon");
|
|
|
|
if(!icon)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
icon.name = item.checked ? "check-square" : "square";
|
|
|
|
icon.library = "default";
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-06-15 00:22:31 +02:00
|
|
|
public showAt(_x, _y, _onHide)
|
|
|
|
{
|
|
|
|
this.removeCallback = _onHide;
|
|
|
|
if(this.popup == null)
|
|
|
|
{
|
2024-07-11 19:27:31 +02:00
|
|
|
this.popup = Object.assign(document.createElement("sl-popup"), {
|
|
|
|
placement: "top",
|
|
|
|
autoSize: "vertical",
|
|
|
|
flip: true,
|
|
|
|
flipFallbackPlacements: "right bottom",
|
|
|
|
flipFallbackStrategy: "initial",
|
|
|
|
shift: true
|
|
|
|
});
|
2024-06-15 00:22:31 +02:00
|
|
|
this.popup.append(this);
|
2024-07-11 19:27:31 +02:00
|
|
|
this.popup.classList.add("egw_menu");
|
2024-06-15 00:22:31 +02:00
|
|
|
}
|
2024-07-11 19:27:31 +02:00
|
|
|
let menu = this;
|
2024-06-15 00:22:31 +02:00
|
|
|
this.popup.anchor = {
|
|
|
|
getBoundingClientRect()
|
|
|
|
{
|
|
|
|
return {
|
|
|
|
x: _x,
|
|
|
|
y: _y,
|
2024-07-11 19:27:31 +02:00
|
|
|
width: menu.clientWidth,
|
|
|
|
height: menu.clientHeight,
|
2024-06-15 00:22:31 +02:00
|
|
|
top: _y,
|
|
|
|
left: _x,
|
|
|
|
right: _x,
|
|
|
|
bottom: _y
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this.popup.active = true;
|
2024-07-11 19:27:31 +02:00
|
|
|
document.body.append(this.popup);
|
2024-06-15 00:22:31 +02:00
|
|
|
Promise.all([this.updateComplete, this.popup.updateComplete]).then(() =>
|
|
|
|
{
|
|
|
|
// Causes scroll issues if we don't position
|
|
|
|
this.popup.popup.style = "top: 0px";
|
|
|
|
(<SlMenuItem>this.menu.querySelector('sl-menu-item')).focus();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public hide()
|
|
|
|
{
|
2024-07-09 23:35:46 +02:00
|
|
|
if(this.popup)
|
|
|
|
{
|
|
|
|
this.popup.active = false;
|
|
|
|
}
|
2024-06-17 17:26:10 +02:00
|
|
|
|
|
|
|
// egw_menu always creates a new menu
|
|
|
|
this.remove();
|
2024-06-15 00:22:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
handleSelect(event)
|
|
|
|
{
|
2024-08-07 15:38:23 +02:00
|
|
|
// If not open, skip
|
|
|
|
if(!this.popup)
|
2024-06-15 00:22:31 +02:00
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(event.detail.item.value)
|
|
|
|
{
|
|
|
|
const item = <egwMenuItem>event.detail.item.value;
|
2024-06-17 17:26:10 +02:00
|
|
|
if(item.checkbox)
|
2024-06-15 00:22:31 +02:00
|
|
|
{
|
2024-08-07 21:18:10 +02:00
|
|
|
// Update our internal data
|
|
|
|
item.data.checked = item.checked = event.detail.item.checked;
|
|
|
|
|
2024-08-09 14:28:14 +02:00
|
|
|
// Update image of a checkbox item to be toggle on or off
|
|
|
|
// this happens by requesting an update because item.checked has changed
|
|
|
|
this.requestUpdate("structure")
|
2024-06-15 00:22:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if(typeof item.onClick == "function")
|
|
|
|
{
|
|
|
|
this.hide();
|
|
|
|
item.onClick.call(event.detail.item, item, event);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-07 21:18:10 +02:00
|
|
|
handleCheckboxClick(event)
|
|
|
|
{
|
|
|
|
const check = event.target.closest("sl-menu-item");
|
|
|
|
if(!check || check.parentElement == this)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure sub-menu does not close
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
// Normal select event
|
|
|
|
check.checked = !check.checked;
|
|
|
|
check.dispatchEvent(new CustomEvent("sl-select", {
|
|
|
|
bubbles: true,
|
|
|
|
cancelable: false,
|
|
|
|
composed: true,
|
|
|
|
detail: {item: check}
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2024-07-09 23:35:46 +02:00
|
|
|
handleDocumentClick(event)
|
|
|
|
{
|
|
|
|
if(!event.composedPath().includes(this))
|
|
|
|
{
|
|
|
|
this.hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleKeypress(event : KeyboardEvent)
|
|
|
|
{
|
|
|
|
if(event.key == "Escape")
|
|
|
|
{
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
this.hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-15 00:22:31 +02:00
|
|
|
private itemTemplate(item : egwMenuItem)
|
|
|
|
{
|
|
|
|
if(item.caption == "-")
|
|
|
|
{
|
|
|
|
return html`
|
|
|
|
<sl-divider></sl-divider>`;
|
|
|
|
}
|
|
|
|
|
2024-08-09 14:28:14 +02:00
|
|
|
//if we have a checkbox, change the icon to be a toggle slider. Either on or off
|
|
|
|
if (item.checkbox)
|
|
|
|
{
|
|
|
|
item.iconUrl = item.checked ? "toggle-on" : "toggle-off";
|
|
|
|
}
|
|
|
|
|
2024-06-15 00:22:31 +02:00
|
|
|
return html`
|
|
|
|
<sl-menu-item
|
|
|
|
class=${classMap({
|
|
|
|
"default-item": item.default
|
|
|
|
})}
|
|
|
|
id=${item.id}
|
|
|
|
type="${item.checkbox ? "checkbox" : "normal"}"
|
|
|
|
?checked=${item.checkbox && item.checked}
|
|
|
|
?disabled=${!item.enabled}
|
|
|
|
.value=${item}
|
2024-08-07 21:18:10 +02:00
|
|
|
@click=${item.checkbox ? this.handleCheckboxClick : nothing}
|
2024-06-15 00:22:31 +02:00
|
|
|
>
|
|
|
|
${item.iconUrl ? html`
|
|
|
|
<et2-image slot="prefix" src="${item.iconUrl}"></et2-image>` : nothing}
|
|
|
|
${item.caption}
|
|
|
|
${item.shortcutCaption ? html`<span slot="suffix"
|
|
|
|
class="keyboard_shortcut">
|
|
|
|
${item.shortcutCaption}
|
|
|
|
</span>` : nothing}
|
|
|
|
${item.children.length == 0 ? nothing : html`
|
|
|
|
<sl-menu slot="submenu">
|
|
|
|
${repeat(item.children, i => this.itemTemplate(i))}
|
|
|
|
</sl-menu>
|
|
|
|
`}
|
|
|
|
</sl-menu-item>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
render()
|
|
|
|
{
|
|
|
|
return html`
|
|
|
|
<sl-menu
|
|
|
|
@sl-select=${this.handleSelect}
|
|
|
|
>
|
|
|
|
${repeat(this.structure, i => this.itemTemplate(i))}
|
|
|
|
</sl-menu>`;
|
|
|
|
}
|
|
|
|
}
|