UNPKG

@geoapify/geocoder-autocomplete

Version:

A JavaScript address autocomplete input, compatible with Leaflet, MapLibre, OpenLayers, and other map libraries for efficient location search and geocoding.

798 lines (797 loc) 33.3 kB
import { CalculationHelper } from "./helpers/calculation.helper"; import { DomHelper } from "./helpers/dom.helper"; import { BY_CIRCLE, BY_COUNTRYCODE, BY_GEOMETRY, BY_PLACE, BY_PROXIMITY, BY_RECT } from "./helpers/constants"; import { Callbacks } from "./helpers/callbacks"; import { CategoryManager } from "./helpers/category.helper"; import { PlacesApiHelper } from './helpers/places-api.helper'; import { PlacesListManager } from "./helpers/places-list.helper"; export class GeocoderAutocomplete { apiKey; inputElement; inputWrapper; componentWrapper; inputClearButton; autocompleteItemsElement = null; /* Places list manager */ placesListManager; /* Focused item in the autocomplete list. This variable is used to navigate with buttons */ focusedItemIndex; /* Current autocomplete items data (GeoJSON.Feature) */ currentItems; /* Current categories displayed in dropdown */ currentCategories = []; /* Active request promise reject function. To be able to cancel the promise when a new request comes */ currentPromiseReject; /* Active place details request promise reject function */ currentPlaceDetailsPromiseReject; /* We set timeout before sending a request to avoid unnecessary calls */ currentTimeout; callbacks = new Callbacks(); preprocessHook; postprocessHook; suggestionsFilter; sendGeocoderRequestAlt; sendPlaceDetailsRequestAlt; sendPlacesRequestAlt; geocoderUrl = "https://api.geoapify.com/v1/geocode/autocomplete"; placeDetailsUrl = "https://api.geoapify.com/v2/place-details"; placesApiUrl = "https://api.geoapify.com/v2/places"; ipGeolocationUrl = "https://api.geoapify.com/v1/ipinfo"; categoryManager; currentPlacesPromiseReject; lastEscKeyTime; DOUBLE_ESC_THRESHOLD = 500; // 500ms window for double-Esc options = { limit: 5, placesLimit: 20, debounceDelay: 100 }; constructor(container, apiKey, options) { this.apiKey = apiKey; this.constructOptions(options); this.componentWrapper = document.createElement("div"); this.componentWrapper.classList.add("geoapify-geocoder-autocomplete-container"); container.appendChild(this.componentWrapper); const cs = getComputedStyle(container); const parentMaxH = cs.maxHeight; // 'none' | '400px' | ... const parentH = cs.height; // 'auto' | '123px' | ... // Priority: // 1. If parent has a max-height → inherit it // 2. Else if parent has a fixed height → use 100% // 3. Else → fallback to 400px if (parentMaxH && parentMaxH !== 'none') { this.componentWrapper.style.maxHeight = 'inherit'; } else if (parentH && parentH !== 'auto' && parentH !== '0px') { this.componentWrapper.style.maxHeight = '100%'; } else { this.componentWrapper.style.maxHeight = '400px'; } this.inputElement = document.createElement("input"); this.inputWrapper = DomHelper.createInputElement(this.inputElement, this.options, this.componentWrapper); this.addClearButton(); this.addEventListeners(); this.categoryManager = new CategoryManager(); // Initialize places list manager with callbacks const placesCallbacks = { onLoadMore: () => { return this.resendPlacesRequestForMore(true /* append */); }, onPlacesUpdate: (places) => { this.callbacks.notifyPlaces(places); }, onPlaceSelect: (place, index) => { this.callbacks.notifyPlaceSelect(place, index); // Auto-hide places list if option is enabled if (this.options.hidePlacesListAfterSelect) { this.placesListManager.clearPlacesList(); if (place && place.properties && place.properties.formatted) { this.clearCategory(); this.setValue(place.properties.formatted); } } } }; this.placesListManager = new PlacesListManager(this.componentWrapper, this.options, placesCallbacks); } setType(type) { this.options.type = type; } setLang(lang) { this.options.lang = lang; } setAddDetails(addDetails) { this.options.addDetails = addDetails; } setSkipIcons(skipIcons) { this.options.skipIcons = skipIcons; } setAllowNonVerifiedHouseNumber(allowNonVerifiedHouseNumber) { this.options.allowNonVerifiedHouseNumber = allowNonVerifiedHouseNumber; } setAllowNonVerifiedStreet(allowNonVerifiedStreet) { this.options.allowNonVerifiedStreet = allowNonVerifiedStreet; } setCountryCodes(codes) { console.warn("WARNING! Obsolete function called. Function setCountryCodes() has been deprecated, please use the new addFilterByCountry() function instead!"); this.options.countryCodes = codes; } setPosition(position) { console.warn("WARNING! Obsolete function called. Function setPosition() has been deprecated, please use the new addBiasByProximity() function instead!"); this.options.position = position; } setLimit(limit) { this.options.limit = limit; } setPlacesLimit(limit) { this.options.placesLimit = limit; } setValue(value) { if (!value) { this.inputClearButton.classList.remove("visible"); } else { this.inputClearButton.classList.add("visible"); } this.inputElement.value = value; } getValue() { return this.inputElement.value; } addFilterByCountry(codes) { this.options.filter[BY_COUNTRYCODE] = codes; } addFilterByCircle(filterByCircle) { this.options.filter[BY_CIRCLE] = filterByCircle; } addFilterByRect(filterByRect) { this.options.filter[BY_RECT] = filterByRect; } addFilterByPlace(filterByPlace) { this.options.filter[BY_PLACE] = filterByPlace; } clearFilters() { this.options.filter = {}; } addBiasByCountry(codes) { this.options.bias[BY_COUNTRYCODE] = codes; } addBiasByCircle(biasByCircle) { this.options.bias[BY_CIRCLE] = biasByCircle; } addBiasByRect(biasByRect) { this.options.bias[BY_RECT] = biasByRect; } addBiasByProximity(biasByProximity) { this.options.bias[BY_PROXIMITY] = biasByProximity; } clearBias() { this.options.bias = {}; } setPlacesFilterByCircle(filterByCircle) { this.options.placesFilter[BY_CIRCLE] = filterByCircle; this.placesListManager.setCurrentOffset(0); } setPlacesFilterByRect(filterByRect) { this.options.placesFilter[BY_RECT] = filterByRect; this.placesListManager.setCurrentOffset(0); } setPlacesFilterByPlace(filterByPlace) { this.options.placesFilter[BY_PLACE] = filterByPlace; this.placesListManager.setCurrentOffset(0); } setPlacesFilterByGeometry(filterByGeometry) { this.options.placesFilter[BY_GEOMETRY] = filterByGeometry; this.placesListManager.setCurrentOffset(0); } clearPlacesFilters() { this.options.placesFilter = {}; this.placesListManager.setCurrentOffset(0); } setPlacesBiasByCircle(biasByCircle) { this.options.placesBias[BY_CIRCLE] = biasByCircle; this.placesListManager.setCurrentOffset(0); } setPlacesBiasByRect(biasByRect) { this.options.placesBias[BY_RECT] = biasByRect; this.placesListManager.setCurrentOffset(0); } setPlacesBiasByProximity(biasByProximity) { this.options.placesBias[BY_PROXIMITY] = biasByProximity; this.placesListManager.setCurrentOffset(0); } clearPlacesBias() { this.options.placesBias = {}; this.placesListManager.setCurrentOffset(0); } on(operation, callback) { this.callbacks.addCallback(operation, callback); } off(operation, callback) { this.callbacks.removeCallback(operation, callback); } once(operation, callback) { this.on(operation, callback); const current = this; const currentListener = () => { current.off(operation, callback); current.off(operation, currentListener); }; this.on(operation, currentListener); } setSuggestionsFilter(suggestionsFilterFunc) { this.suggestionsFilter = CalculationHelper.returnIfFunction(suggestionsFilterFunc); } setPreprocessHook(preprocessHookFunc) { this.preprocessHook = CalculationHelper.returnIfFunction(preprocessHookFunc); } setPostprocessHook(postprocessHookFunc) { this.postprocessHook = CalculationHelper.returnIfFunction(postprocessHookFunc); } setSendGeocoderRequestFunc(sendGeocoderRequestFunc) { this.sendGeocoderRequestAlt = CalculationHelper.returnIfFunction(sendGeocoderRequestFunc); } setSendPlaceDetailsRequestFunc(sendPlaceDetailsRequestFunc) { this.sendPlaceDetailsRequestAlt = CalculationHelper.returnIfFunction(sendPlaceDetailsRequestFunc); } setSendPlacesRequestFunc(sendPlacesRequestFunc) { if (!this.isCategoryModeEnabled()) { console.warn('Category search is disabled. Enable with addCategorySearch: true'); return; } this.sendPlacesRequestAlt = CalculationHelper.returnIfFunction(sendPlacesRequestFunc); } isOpen() { return !!this.autocompleteItemsElement; } close() { this.closeDropDownList(); } open() { if (!this.isOpen()) { this.openDropdownAgain(); } } sendGeocoderRequestOrAlt(currentValue) { if (this.sendGeocoderRequestAlt) { return this.sendGeocoderRequestAlt(currentValue, this); } else { return this.sendGeocoderRequest(currentValue); } } sendGeocoderRequest(value) { return new Promise((resolve, reject) => { this.currentPromiseReject = reject; let url = CalculationHelper.generateUrl(value, this.geocoderUrl, this.apiKey, this.options); fetch(url) .then((response) => { if (response.ok) { response.json().then(data => resolve(data)); } else { response.json().then(data => reject(data)); } }); }); } sendPlaceDetailsRequest(feature) { return new Promise((resolve, reject) => { if (CalculationHelper.isNotOpenStreetMapData(feature)) { // only OSM data has detailed information; return the original object if the source is different from OSM resolve(feature); return; } this.currentPlaceDetailsPromiseReject = reject; let url = CalculationHelper.generatePlacesUrl(this.placeDetailsUrl, feature.properties.place_id, this.apiKey, this.options); fetch(url) .then((response) => { if (response.ok) { response.json().then(data => { if (!data.features.length) { resolve(feature); } resolve(data.features[0]); }); } else { response.json().then(data => reject(data)); } }); }); } async selectCategory(category) { if (category) { this.categoryManager.setCategory(category); const categoryObj = this.categoryManager.getCategory(); if (!categoryObj) { return; } this.inputElement.value = categoryObj.label; this.showClearButton(); // Close the dropdown when a category is selected this.closeDropDownList(); this.placesListManager.setCategory(categoryObj.keys, categoryObj.label); this.placesListManager.setCurrentOffset(0); await this.sendPlacesRequest(); } else { this.clearCategoryAndNotify(); } } async clearCategory() { this.clearCategoryAndNotify(); } async resendPlacesRequestForMore(appendPlaces) { const categoryObj = this.categoryManager.getCategory(); if (!categoryObj) { console.warn('Cannot resend places request: no category is set'); return; } this.callbacks.notifyPlacesRequestStart(categoryObj); if (!appendPlaces) { this.placesListManager.setCurrentOffset(0); } try { const bias = Object.keys(this.options.placesBias).length ? this.options.placesBias : PlacesApiHelper.convertGeocoderBiasToPlacesApiBias(this.options.bias); const filter = Object.keys(this.options.placesFilter).length ? this.options.placesFilter : PlacesApiHelper.convertGeocoderFilterToPlacesApiFilter(this.options.filter); const data = await this.sendPlacesRequestOrAlt(categoryObj.keys, bias, filter, this.placesListManager.getCurrentOffset(), this.options.placesLimit); const places = data.features || []; this.placesListManager.setPlaces(places, appendPlaces); this.placesListManager.setCurrentOffset(this.placesListManager.getCurrentOffset() + this.options.placesLimit); this.callbacks.notifyPlacesRequestEnd(true, data); } catch (error) { this.callbacks.notifyPlacesRequestEnd(false, null, error); console.error('Places API request failed:', error); } } getCategory() { if (!this.isCategoryModeEnabled()) { return null; } return this.categoryManager.getCategory(); } selectPlace(index) { // Only works with built-in places list if (!this.options.showPlacesList) { return; } // If index is null or negative, clear selection if (index === null || index < 0) { this.placesListManager.clearSelection(); return; } // Select the place (no validation - let the user manage that) this.placesListManager.selectPlace(index); } async sendPlacesRequest() { const categoryObj = this.categoryManager.getCategory(); if (!this.isCategoryModeEnabled()) { return Promise.reject(new Error('Category search is disabled')); } if (!categoryObj) { console.warn('Cannot resend places request: no category is set'); return; } this.callbacks.notifyPlacesRequestStart(categoryObj); let data; try { const bias = Object.keys(this.options.placesBias).length ? this.options.placesBias : PlacesApiHelper.convertGeocoderBiasToPlacesApiBias(this.options.bias); const filter = Object.keys(this.options.placesFilter).length ? this.options.placesFilter : PlacesApiHelper.convertGeocoderFilterToPlacesApiFilter(this.options.filter); data = await this.sendPlacesRequestOrAlt(this.categoryManager.getCategory().keys, bias, filter, 0, this.options.placesLimit); const places = data.features; this.placesListManager.setPlaces(places); this.placesListManager.setCurrentOffset(this.placesListManager.getCurrentOffset() + this.options.placesLimit); this.callbacks.notifyPlacesRequestEnd(true, data); } catch (error) { this.callbacks.notifyPlacesRequestEnd(false, null, error); console.error('Places API request failed:', error); } } /* Execute a function when someone writes in the text field: */ onUserInput(event) { let currentValue = this.inputElement.value; let userEnteredValue = this.inputElement.value; this.callbacks.notifyInputChange(currentValue); /* Reset category mode and clear places list when user types */ this.clearCategoryAndNotify(); /* Close any already open dropdown list */ this.closeDropDownList(); this.focusedItemIndex = -1; this.cancelPreviousRequest(); this.cancelPreviousTimeout(); if (!currentValue) { this.removeClearButton(); return false; } this.showClearButton(); this.currentTimeout = window.setTimeout(() => { /* Create a new promise and send geocoding request */ if (CalculationHelper.returnIfFunction(this.preprocessHook)) { currentValue = this.preprocessHook(currentValue); } this.callbacks.notifyRequestStart(currentValue); let promise = this.sendGeocoderRequestOrAlt(currentValue); promise.then((data) => { this.callbacks.notifyRequestEnd(true, data); this.onDropdownDataLoad(data, userEnteredValue, event); }, (err) => { this.callbacks.notifyRequestEnd(false, null, err); if (!err.canceled) { console.log(err); } }); }, this.options.debounceDelay); } onDropdownDataLoad(data, userEnteredValue, event) { if (CalculationHelper.needToCalculateExtendByNonVerifiedValues(data, this.options)) { CalculationHelper.extendByNonVerifiedValues(this.options, data.features, data?.query?.parsed); } this.currentItems = data.features; if (CalculationHelper.needToFilterDataBySuggestionsFilter(this.currentItems, this.suggestionsFilter)) { this.currentItems = this.suggestionsFilter(this.currentItems); } this.callbacks.notifySuggestions(this.currentItems); this.currentCategories = []; if (this.isCategoryModeEnabled() && data.query?.categories) { this.currentCategories = this.categoryManager.extractCategoriesFromResponse(data); } if (!this.currentItems.length && !this.currentCategories.length) { return; } this.createDropdown(); this.currentCategories.forEach((category) => { this.populateCategoryDropdownItem(category, event); }); this.currentItems.forEach((feature, index) => { this.populateDropdownItem(feature, userEnteredValue, event, index + this.currentCategories.length); }); } populateCategoryDropdownItem(category, event) { const itemElement = DomHelper.createDropdownItem(); itemElement.classList.add('geoapify-category-item'); // Add search icon const iconElement = document.createElement("span"); iconElement.classList.add('icon'); DomHelper.addIcon(iconElement, 'search'); itemElement.appendChild(iconElement); const textElement = DomHelper.createDropdownItemText(); textElement.innerHTML = `<span class="main-part">${category.label}</span>`; itemElement.appendChild(textElement); itemElement.addEventListener("click", (e) => { event.stopPropagation(); this.selectCategory(category); }); this.autocompleteItemsElement?.appendChild(itemElement); } populateDropdownItem(feature, userEnteredValue, event, index) { /* Create a DIV element for each element: */ const itemElement = DomHelper.createDropdownItem(); if (!this.options.skipIcons) { DomHelper.addDropdownIcon(feature, itemElement); } const textElement = DomHelper.createDropdownItemText(); if (CalculationHelper.returnIfFunction(this.postprocessHook)) { const value = this.postprocessHook(feature); textElement.innerHTML = DomHelper.getStyledAddressSingleValue(value, userEnteredValue); } else { textElement.innerHTML = DomHelper.getStyledAddress(feature.properties, userEnteredValue); } itemElement.appendChild(textElement); this.addEventListenerOnDropdownClick(itemElement, event, index); this.autocompleteItemsElement.appendChild(itemElement); } addEventListenerOnDropdownClick(itemElement, event, index) { itemElement.addEventListener("click", (e) => { event.stopPropagation(); let featureIndex = index; if (this.isCategoryModeEnabled()) { const categoryCount = this.autocompleteItemsElement?.querySelectorAll('.geoapify-category-item').length || 0; featureIndex = index - categoryCount; } if (this.currentItems?.[featureIndex]) { this.setValueAndNotify(this.currentItems[featureIndex]); } }); } createDropdown() { /*create a DIV element that will contain the items (values):*/ this.autocompleteItemsElement = document.createElement("div"); this.autocompleteItemsElement.setAttribute("class", "geoapify-autocomplete-items"); this.callbacks.notifyOpened(); /* Append the DIV element as a child of the input wrapper:*/ this.componentWrapper.appendChild(this.autocompleteItemsElement); } cancelPreviousTimeout() { if (this.currentTimeout) { window.clearTimeout(this.currentTimeout); this.currentTimeout = null; } } cancelPreviousRequest() { if (this.currentPromiseReject) { this.currentPromiseReject({ canceled: true }); this.currentPromiseReject = null; } } addEventListeners() { this.inputElement.addEventListener('input', this.onUserInput.bind(this), false); this.inputElement.addEventListener('keydown', this.onUserKeyPress.bind(this), false); document.addEventListener("click", (event) => { if (event.target !== this.inputElement) { this.closeDropDownList(); } else if (!this.autocompleteItemsElement) { // open dropdown list again this.openDropdownAgain(); } }); } showClearButton() { this.inputClearButton.classList.add("visible"); } removeClearButton() { this.inputClearButton.classList.remove("visible"); } onUserKeyPress(event) { if (this.autocompleteItemsElement) { const itemElements = this.autocompleteItemsElement.getElementsByTagName("div"); if (event.code === 'ArrowDown') { this.handleArrowDownEvent(event, itemElements); } else if (event.code === 'ArrowUp') { this.handleArrowUpEvent(event, itemElements); } else if (event.code === "Enter") { this.handleEnterEvent(event); } else if (event.code === "Escape") { /* If the ESC key is pressed, close the list */ this.handleEscapeKey(); } } else { if (event.code == 'ArrowDown') { /* Open dropdown list again */ this.openDropdownAgain(); } else if (event.code === "Escape") { /* Handle Esc when dropdown is closed (for double-Esc category clearing) */ this.handleEscapeKey(); } } } handleEscapeKey() { const now = Date.now(); if (this.autocompleteItemsElement) { // First Esc: Close dropdown this.closeDropDownList(); this.lastEscKeyTime = now; } else if (this.shouldClearCategoryOnDoubleEscape(now)) { // Second Esc within threshold: Clear category and reset input this.inputElement.value = ''; this.removeClearButton(); this.clearCategory(); this.lastEscKeyTime = undefined; } else { // Single Esc when dropdown is already closed this.lastEscKeyTime = now; } } shouldClearCategoryOnDoubleEscape(currentTime) { return this.isCategoryModeEnabled() && this.categoryManager.isCategoryModeActive() && this.lastEscKeyTime !== undefined && (currentTime - this.lastEscKeyTime) < this.DOUBLE_ESC_THRESHOLD; } handleEnterEvent(event) { /* If the ENTER key is pressed and value as selected, close the list*/ event.preventDefault(); if (this.focusedItemIndex > -1) { const categoryCount = this.currentCategories.length; // Check if the focused item is a category if (this.focusedItemIndex < categoryCount) { // It's a category - trigger setCategory const category = this.currentCategories[this.focusedItemIndex]; this.selectCategory(category); } else { // It's a regular geocoder item const itemIndex = this.focusedItemIndex - categoryCount; if (this.options.skipSelectionOnArrowKey) { // select the location if it wasn't selected by navigation this.setValueAndNotify(this.currentItems[itemIndex]); } else { this.closeDropDownList(); } } } } handleArrowUpEvent(event, itemElements) { event.preventDefault(); /*If the arrow UP key is pressed, decrease the focusedItemIndex variable:*/ this.focusedItemIndex--; if (this.focusedItemIndex < 0) this.focusedItemIndex = (itemElements.length - 1); /*and and make the current item more visible:*/ this.setActive(itemElements, this.focusedItemIndex); } handleArrowDownEvent(event, itemElements) { event.preventDefault(); /*If the arrow DOWN key is pressed, increase the focusedItemIndex variable:*/ this.focusedItemIndex++; if (this.focusedItemIndex >= itemElements.length) this.focusedItemIndex = 0; /*and and make the current item more visible:*/ this.setActive(itemElements, this.focusedItemIndex); } setActive(items, index) { if (!items || !items.length) return false; DomHelper.addActiveClassToDropdownItem(items, index); if (!this.options.skipSelectionOnArrowKey) { const categoryCount = this.currentCategories.length; // Check if the focused item is a category if (index < categoryCount) { // It's a category item const category = this.currentCategories[index]; this.inputElement.value = category.label; } else { // It's a regular geocoder item const itemIndex = index - categoryCount; if (CalculationHelper.returnIfFunction(this.postprocessHook)) { this.inputElement.value = this.postprocessHook(this.currentItems[itemIndex]); } else { this.inputElement.value = this.currentItems[itemIndex].properties.formatted; } this.notifyValueSelected(this.currentItems[itemIndex]); } } } setValueAndNotify(feature) { if (CalculationHelper.returnIfFunction(this.postprocessHook)) { this.inputElement.value = this.postprocessHook(feature); } else { this.inputElement.value = feature.properties.formatted; } // Clear category when selecting a regular place from main dropdown this.clearCategoryAndNotify(); this.notifyValueSelected(feature); /* Close the list of autocompleted values: */ this.closeDropDownList(); } clearFieldAndNotify(event) { event.stopPropagation(); this.inputElement.value = ''; this.removeClearButton(); this.cancelPreviousRequest(); this.cancelPreviousTimeout(); this.cancelCurrentPlacesRequest(); this.closeDropDownList(); if (this.isCategoryModeEnabled() && this.categoryManager.isCategoryModeActive()) { this.clearCategoryAndNotify(); } else { this.callbacks.notifyClear('address'); } this.notifyValueSelected(null); } closeDropDownList() { if (this.autocompleteItemsElement) { this.componentWrapper.removeChild(this.autocompleteItemsElement); this.autocompleteItemsElement = null; this.callbacks.notifyClosed(); } } notifyValueSelected(feature) { this.cancelPreviousPlaceDetailsRequest(); if (this.noNeedToRequestPlaceDetails(feature)) { this.callbacks.notifyChange(feature); } else { this.callbacks.notifyPlaceDetailsRequestStart(feature); let promise = this.sendPlaceDetailsRequestOrAlt(feature); promise.then((detailesFeature) => { this.callbacks.notifyPlaceDetailsRequestEnd(true, detailesFeature); this.callbacks.notifyChange(detailesFeature); this.currentPlaceDetailsPromiseReject = null; }, (err) => { if (!err.canceled) { console.log(err); this.callbacks.notifyPlaceDetailsRequestEnd(false, null, err); this.callbacks.notifyChange(feature); this.currentPlaceDetailsPromiseReject = null; } }); } } sendPlaceDetailsRequestOrAlt(feature) { if (this.sendPlaceDetailsRequestAlt) { return this.sendPlaceDetailsRequestAlt(feature, this); } else { return this.sendPlaceDetailsRequest(feature); } } noNeedToRequestPlaceDetails(feature) { return !this.options.addDetails || !feature || feature.properties.nonVerifiedParts?.length; } cancelPreviousPlaceDetailsRequest() { if (this.currentPlaceDetailsPromiseReject) { this.currentPlaceDetailsPromiseReject({ canceled: true }); this.currentPlaceDetailsPromiseReject = null; } } cancelCurrentPlacesRequest() { if (this.currentPlacesPromiseReject) { this.currentPlacesPromiseReject({ canceled: true }); this.currentPlacesPromiseReject = null; } } openDropdownAgain() { const event = document.createEvent('Event'); event.initEvent('input', true, true); this.inputElement.dispatchEvent(event); } constructOptions(options) { this.options = options ? { ...this.options, ...options } : this.options; this.options.filter = this.options.filter || {}; this.options.bias = this.options.bias || {}; this.options.placesFilter = this.options.placesFilter || {}; this.options.placesBias = this.options.placesBias || {}; if (this.options.countryCodes) { this.addFilterByCountry(this.options.countryCodes); } if (this.options.position) { this.addBiasByProximity(this.options.position); } } addClearButton() { this.inputClearButton = document.createElement("div"); this.inputClearButton.classList.add("geoapify-close-button"); DomHelper.addIcon(this.inputClearButton, 'close'); this.inputClearButton.addEventListener("click", this.clearFieldAndNotify.bind(this), false); this.inputWrapper.appendChild(this.inputClearButton); } isCategoryModeEnabled() { return !!this.options.addCategorySearch; } clearCategoryAndNotify() { if (!this.isCategoryModeEnabled()) return; const wasCategoryActive = this.categoryManager.isCategoryModeActive(); this.categoryManager.clearCategory(); this.placesListManager.resetCategory(); if (wasCategoryActive) { this.callbacks.notifyClear('category'); } } async sendPlacesRequestOrAlt(categoryKeys, bias, filter, offset, limit) { if (this.sendPlacesRequestAlt) { return this.sendPlacesRequestAlt(categoryKeys, offset, this); } let url; if (Object.keys(bias || {}).length === 0 && Object.keys(filter || {}).length === 0) { const location = await PlacesApiHelper.getLocationForBias(this.apiKey, this.options, this.ipGeolocationUrl); url = PlacesApiHelper.generatePlacesUrl(categoryKeys, this.apiKey, this.options, this.placesApiUrl, offset, limit, location ? { [BY_PROXIMITY]: location } : undefined); } else { url = PlacesApiHelper.generatePlacesUrl(categoryKeys, this.apiKey, this.options, this.placesApiUrl, offset, limit, bias, filter); } return PlacesApiHelper.sendPlacesRequest(url); } }