Rewild Your Web
web browser dweb
at main 137 lines 3.3 kB view raw
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);