class UserSelector extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); this.selectedUser = null; this.debounceTimer = null; } connectedCallback() { this.render(); this.setupEventListeners(); } render() { this.shadowRoot.innerHTML = `
`; } setupEventListeners() { const input = this.shadowRoot.getElementById("userInput"); const dropdown = this.shadowRoot.getElementById("suggestionsDropdown"); input.addEventListener("input", (e) => this.handleInput(e)); input.addEventListener("focus", (e) => { if (e.target.value.length >= 4) { this.handleInput(e); } }); // Close dropdown when clicking outside document.addEventListener("click", (e) => { if (!this.contains(e.target)) { this.hideSuggestions(); } }); } handleInput(e) { const query = e.target.value; // Clear previous timer clearTimeout(this.debounceTimer); if (query.length < 4) { this.hideSuggestions(); return; } // Debounce API calls this.debounceTimer = setTimeout(() => { this.fetchSuggestions(query); }, 300); } async fetchSuggestions(query) { const suggestionsList = this.shadowRoot.getElementById("suggestionsList"); const dropdown = this.shadowRoot.getElementById("suggestionsDropdown"); // Show loading state suggestionsList.innerHTML = '
Loading...
'; dropdown.classList.add("show"); try { const response = await fetch( `/api/user/suggestion?query=${encodeURIComponent(query)}`, ); if (!response.ok) { throw new Error("Network response was not ok"); } const data = await response.json(); this.displaySuggestions(data.users); } catch (error) { console.error("Error fetching suggestions:", error); suggestionsList.innerHTML = ` `; } } displaySuggestions(users) { const suggestionsList = this.shadowRoot.getElementById("suggestionsList"); const dropdown = this.shadowRoot.getElementById("suggestionsDropdown"); if (!users || users.length === 0) { suggestionsList.innerHTML = `
No users found
`; return; } suggestionsList.innerHTML = users .map( (user) => `
${this.escapeHtml(user.display_name)}
@${this.escapeHtml(user.username)}
${this.escapeHtml(user.organization.name)}
`, ) .join(""); // Add click handlers to suggestion items suggestionsList.querySelectorAll(".suggestion-item").forEach((item) => { item.addEventListener("click", (e) => { const userData = JSON.parse(e.currentTarget.getAttribute("data-user")); this.selectUser(userData); }); }); dropdown.classList.add("show"); } selectUser(user) { this.selectedUser = user; const input = this.shadowRoot.getElementById("userInput"); input.value = user.displayName || user.display_name; this.hideSuggestions(); // Dispatch custom event this.dispatchEvent( new CustomEvent("user-selected", { detail: { user }, bubbles: true, composed: true, }), ); } hideSuggestions() { const dropdown = this.shadowRoot.getElementById("suggestionsDropdown"); dropdown.classList.remove("show"); } escapeHtml(text) { const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; return text.replace(/[&<>"']/g, (m) => map[m]); } // Public method to get selected user getSelectedUser() { return this.selectedUser; } // Public method to clear selection clear() { this.selectedUser = null; const input = this.shadowRoot.getElementById("userInput"); input.value = ""; this.hideSuggestions(); } } // Register the custom element customElements.define("user-selector", UserSelector);