Rewild Your Web
web
browser
dweb
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3import { MenuBase, css, html } from "./menu_base.js";
4
5export class ContextMenu extends MenuBase {
6 static properties = {
7 ...MenuBase.properties,
8 items: { type: Array },
9 x: { type: Number },
10 y: { type: Number },
11 controlId: { type: String },
12 };
13
14 constructor() {
15 super();
16 this.items = [];
17 this.x = 0;
18 this.y = 0;
19 this.controlId = "";
20 this.handleKeyDown = this.handleKeyDown.bind(this);
21 }
22
23 connectedCallback() {
24 super.connectedCallback();
25 }
26
27 disconnectedCallback() {
28 super.disconnectedCallback();
29 this.removeEventListeners();
30 }
31
32 updated(changedProperties) {
33 if (changedProperties.has("open")) {
34 if (this.open) {
35 // Add keyboard listener when menu opens
36 document.addEventListener("keydown", this.handleKeyDown);
37 } else {
38 this.removeEventListeners();
39 }
40 }
41 }
42
43 removeEventListeners() {
44 document.removeEventListener("keydown", this.handleKeyDown);
45 }
46
47 handleKeyDown(e) {
48 if (e.key === "Escape") {
49 this.cancel();
50 }
51 }
52
53 handleBackdropClick(e) {
54 // Only cancel if the click was directly on the backdrop, not on the menu
55 if (e.target.classList.contains("backdrop")) {
56 this.cancel();
57 }
58 }
59
60 cancel() {
61 this.dispatchEvent(
62 new CustomEvent("menu-cancel", {
63 bubbles: true,
64 composed: true,
65 detail: { controlId: this.controlId },
66 }),
67 );
68 this.close();
69 }
70
71 handleItemClick(item) {
72 if (item.disabled) {
73 return;
74 }
75 this.dispatchEvent(
76 new CustomEvent("menu-action", {
77 bubbles: true,
78 composed: true,
79 detail: { action: item.id, controlId: this.controlId },
80 }),
81 );
82 this.close();
83 }
84
85 static styles = css`
86 @import url(//system.localhost:8888/context_menu.css);
87 `;
88
89 render() {
90 return html`
91 <div class="backdrop" @click=${this.handleBackdropClick}></div>
92 <div class="menu" style="left: ${this.x}px; top: ${this.y}px;">
93 ${this.items.map(
94 (item) => html`
95 <div
96 class="menu-item ${item.disabled ? "disabled" : ""}"
97 @click=${() => this.handleItemClick(item)}
98 >
99 <span class="icon-slot">
100 ${item.icon
101 ? html`<lucide-icon name="${item.icon}"></lucide-icon>`
102 : ""}
103 </span>
104 <span>${item.label}</span>
105 ${item.checked
106 ? html`<lucide-icon name="check"></lucide-icon>`
107 : ""}
108 </div>
109 `,
110 )}
111 </div>
112 `;
113 }
114}
115
116customElements.define("context-menu", ContextMenu);