class AddressOrReportInput extends HTMLElement { // make element form-associated static formAssociated = true; constructor() { super(); this.attachShadow({ mode: "open" }); this.internals = this.attachInternals(); this.render(); // Element references this._addresses = []; this._input = this.shadowRoot.querySelector("input"); this._reports = []; this._suggestionsContainer = this.shadowRoot.querySelector( ".suggestions-container", ); // Bind methods this._handleInput = this._handleInput.bind(this); // Debounce timer this._debounceTimer = null; // The suggestion data this._suggestionData = null; } // Lifecycle: when element is added to the DOM connectedCallback() { this._input.addEventListener("input", this._handleInput); } // Lifecycle: when element is removed from the DOM disconnectedCallback() { this._input.removeEventListener("input", this._handleInput); } // Lifecycle: watch these attributes for changes static get observedAttributes() { return ["placeholder", "api-key"]; } // Lifecycle: respond to attribute changes attributeChangedCallback(name, oldValue, newValue) { if (name === "placeholder" && this._input) { this._input.placeholder = newValue; } if (name === "api-key") { this._apiKey = newValue; } } // Properties API get value() { return this._input ? this._input.value : ""; } set value(val) { if (this._input) { this._input.value = val; const entries = new FormData(); entries.append("address", val); this.internals.setFormValue(entries); } } // Private methods _handleInput(event) { const searchText = event.target.value.trim(); // Clear previous timer clearTimeout(this._debounceTimer); // Clear suggestions if input is less than 3 characters if (searchText.length < 3) { this._suggestionsContainer.innerHTML = ""; return; } // Debounce API calls (wait 300ms after typing stops) this._debounceTimer = setTimeout(() => { this._handleSuggestions(searchText); }, 300); } async _fetchAddressSuggestions(text) { try { const url = `https://api.mapbox.com/search/geocode/v6/forward?q=${encodeURIComponent(text)}&access_token=${this._apiKey}`; const response = await fetch(url); const data = await response.json(); return data.features || []; } catch (error) { console.error("Error fetching geocoding suggestions:", error); return []; } } async _fetchReportSuggestions(text) { try { const url = `/report/suggest?r=${text}`; const response = await fetch(url); const data = await response.json(); return data.reports || []; } catch (error) { console.error("Error fetching report suggestions:", error); return []; } } async _handleSuggestions(text) { await Promise.all([ (async () => { this._addresses = await this._fetchAddressSuggestions(text); })(), (async () => { this._reports = await this._fetchReportSuggestions(text); })(), ]); this._renderSuggestions(this._addresses, this._reports); } _renderSuggestions(addresses, reports) { console.log("Rendering suggestions", addresses, reports); const reportElements = reports .map((item, index) => { const formatted_id = _formatReportID(item.id); const type_display = _formatReportType(item.type); return `