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 SelectControl extends MenuBase {
6 static properties = {
7 ...MenuBase.properties,
8 options: { type: Array },
9 selectedIndex: { type: Number },
10 x: { type: Number },
11 y: { type: Number },
12 controlId: { type: String },
13 };
14
15 constructor() {
16 super();
17 this.options = [];
18 this.selectedIndex = -1;
19 this.x = 0;
20 this.y = 0;
21 this.controlId = "";
22 this.handleKeyDown = this.handleKeyDown.bind(this);
23 }
24
25 connectedCallback() {
26 super.connectedCallback();
27 }
28
29 disconnectedCallback() {
30 super.disconnectedCallback();
31 this.removeEventListeners();
32 }
33
34 updated(changedProperties) {
35 if (changedProperties.has("open")) {
36 if (this.open) {
37 // Add keyboard listener when menu opens
38 requestAnimationFrame(() => {
39 document.addEventListener("keydown", this.handleKeyDown);
40 });
41 } else {
42 this.removeEventListeners();
43 }
44 }
45 }
46
47 removeEventListeners() {
48 document.removeEventListener("keydown", this.handleKeyDown);
49 }
50
51 handleKeyDown(e) {
52 if (e.key === "Escape") {
53 this.cancel();
54 }
55 }
56
57 handleBackdropClick(e) {
58 if (e.target.classList.contains("backdrop")) {
59 this.cancel();
60 }
61 }
62
63 cancel() {
64 this.dispatchEvent(
65 new CustomEvent("select-cancel", {
66 bubbles: true,
67 composed: true,
68 detail: { controlId: this.controlId },
69 }),
70 );
71 this.close();
72 }
73
74 handleOptionClick(option, index) {
75 if (option.disabled) {
76 return;
77 }
78 this.dispatchEvent(
79 new CustomEvent("select-option", {
80 bubbles: true,
81 composed: true,
82 detail: {
83 controlId: this.controlId,
84 optionId: option.id,
85 index: index,
86 },
87 }),
88 );
89 this.close();
90 }
91
92 static styles = css`
93 @import url(//system.localhost:8888/select_control.css);
94 `;
95
96 render() {
97 // Group options by their group label
98 let currentGroup = null;
99 const renderedItems = [];
100
101 this.options.forEach((option, index) => {
102 // Check if we need to render a group header
103 if (option.group && option.group !== currentGroup) {
104 currentGroup = option.group;
105 renderedItems.push(html`
106 <div class="option-group">${option.group}</div>
107 `);
108 } else if (!option.group && currentGroup !== null) {
109 currentGroup = null;
110 }
111
112 const isSelected = index === this.selectedIndex;
113 renderedItems.push(html`
114 <div
115 class="menu-item ${option.disabled ? "disabled" : ""} ${isSelected
116 ? "selected"
117 : ""}"
118 @click=${() => this.handleOptionClick(option, index)}
119 >
120 <span class="icon-slot">
121 ${isSelected ? html`<lucide-icon name="check"></lucide-icon>` : ""}
122 </span>
123 <span>${option.label}</span>
124 </div>
125 `);
126 });
127
128 return html`
129 <div class="backdrop" @click=${this.handleBackdropClick}></div>
130 <div class="menu" style="left: ${this.x}px; top: ${this.y}px;">
131 ${renderedItems}
132 </div>
133 `;
134 }
135}
136
137customElements.define("select-control", SelectControl);