diff --git a/samples/localities-search/index.njk b/samples/localities-search/index.njk new file mode 100644 index 00000000..62f41209 --- /dev/null +++ b/samples/localities-search/index.njk @@ -0,0 +1,24 @@ +{% extends '../../src/_includes/layout.njk' %} +{% block html %} + +
+
+ {% svgIcon 'search.svg' %} + + + +
+
+
+
+
+
+ +{% endblock %} diff --git a/samples/localities-search/index.ts b/samples/localities-search/index.ts new file mode 100644 index 00000000..c4e04200 --- /dev/null +++ b/samples/localities-search/index.ts @@ -0,0 +1,285 @@ +// [START woosmap_localities_search] +let map: woosmap.map.Map; +let marker: woosmap.map.Marker; +let infoWindow: woosmap.map.InfoWindow; +let localitiesService: woosmap.map.LocalitiesService; +let debouncedLocalitiesSearch: (...args: any[]) => Promise; +let input: string; +let detailsHTML: HTMLElement; +let detailsResultContainer: HTMLElement; + +const woosmap_key = "YOUR_API_KEY"; + +function initMap(): void { + map = new window.woosmap.map.Map( + document.getElementById("map") as HTMLElement, + { + center: { lat: 51.507445, lng: -0.127765 }, + zoom: 8, + styles: [ + { + featureType: "point_of_interest", + elementType: "all", + stylers: [ + { + visibility: "on", + }, + ], + }, + ], + } + ); + infoWindow = new woosmap.map.InfoWindow({}); + localitiesService = new window.woosmap.map.LocalitiesService(); + + debouncedLocalitiesSearch = debouncePromise(fetchLocalitiesSearch, 0); +} + +const fetchLocalitiesSearch = async (input: any): Promise => { + const center = map.getCenter(); + const radius = map.getZoom() > 10 ? (map.getZoom() > 14 ? "1000" : "10000") : "100000"; + try { + const response = await fetch( + ` +https://api.woosmap.com/localities/search?types=point_of_interest|locality|admin_level|postal_code|address&input=${encodeURIComponent(input)}&location=${center.lat()},${center.lng()}&radius=${radius}&key=${woosmap_key}&components=country%3Agb` + ); + return await response.json(); + } catch (error) { + console.error("Error fetching localities:", error); + throw error; + } +}; + +function fillDetailsResult(detailsResult: any) { + const details: string[] = []; + detailsHTML.innerHTML = ""; + detailsHTML.style.display = "block"; + if (detailsResult.formatted_address) { + details.push( + `

Formatted_address:${detailsResult.formatted_address}

` + ); + } else if (detailsResult.name) + details.push( + `

Name:${detailsResult.name}

` + ); + if (detailsResult.types && detailsResult.types[0]) { + details.push( + `

Type: ${detailsResult.types[0]}

` + ); + } + if (detailsResult.categories) + details.push( + `

Categories:${detailsResult.categories[0]}

` + ); + if (detailsResult.geometry) { + details.push( + `
Latitude: ${detailsResult.geometry.location.lat.toFixed(5).toString()}
Longitude: ${detailsResult.geometry.location.lng.toFixed(5).toString()}
` + ); + if (detailsResult.address_components) { + const compoHtml = detailsResult.address_components + .map( + (compo) => + `

${compo.types.find((item) => item.includes("division")) || compo.types[0]}: ${compo.long_name}

` + ) + .join(""); + details.push( + `
Address components
${compoHtml}
` + ); + } + } + detailsHTML.innerHTML = details.join(""); +} + +const inputElement = document.getElementById( + "autocomplete-input" +) as HTMLInputElement; +const suggestionsList = document.getElementById( + "suggestions-list" +) as HTMLUListElement; +const clearSearchBtn = document.getElementsByClassName( + "clear-searchButton" +)[0] as HTMLButtonElement; +detailsResultContainer = document.querySelector( + ".detailsResult" +) as HTMLElement; +detailsHTML = document.querySelector( + ".detailsResult .detailsOptions" +) as HTMLElement; +if (inputElement && suggestionsList) { + inputElement.addEventListener("input", handleAutocomplete); + inputElement.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + const firstLi = suggestionsList.querySelector("li"); + if (firstLi) { + firstLi.click(); + } + } + }); +} +clearSearchBtn.addEventListener("click", () => { + inputElement.value = ""; + suggestionsList.style.display = "none"; + clearSearchBtn.style.display = "none"; + if (marker) { + marker.setMap(null); + infoWindow.close(); + } + inputElement.focus(); +}); + +function handleAutocomplete(): void { + if (inputElement && suggestionsList) { + input = inputElement.value; + if (input) { + debouncedLocalitiesSearch(input) + .then((results) => displaySuggestions(results)) + .catch((error) => + console.error("Error autocomplete localities:", error) + ); + } else { + suggestionsList.style.display = "none"; + clearSearchBtn.style.display = "none"; + } + } +} + +function handleDetails(publicId: string) { + localitiesService + .getDetails({ publicId }) + .then((response) => displayResult(response.result)) + .catch((error) => console.error("Error getting locality details:", error)); +} + +function displaySection(section: HTMLElement, mode = "block"): void { + section.style.display = mode; +} + +function displayResult(result: woosmap.map.localities.LocalitiesDetailsResult) { + fillDetailsResult(result); + displaySection(detailsResultContainer); + if (marker) { + marker.setMap(null); + infoWindow.close(); + } + if (result?.geometry) { + marker = new woosmap.map.Marker({ + position: result.geometry.location, + icon: { + url: "https://images.woosmap.com/marker.png", + scaledSize: { + height: 50, + width: 32, + }, + }, + }); + marker.setMap(map); + infoWindow.setContent( + `${result.formatted_address ?? result.name}` + ); + infoWindow.open(map, marker); + map.flyTo({ center: result.geometry.location, zoom: 14 }); + } +} + +function displaySuggestions(localitiesPredictions: any) { + if (inputElement && suggestionsList) { + suggestionsList.innerHTML = ""; + if (localitiesPredictions.results.length > 0 && input) { + localitiesPredictions.results.forEach((result) => { + const li = document.createElement("li"); + const title = document.createElement("div"); + const desc = document.createElement("span"); + title.textContent = result.title ?? ""; + title.className = "localities-search-title"; + desc.textContent = result.description ?? ""; + desc.className = "localities-search-description"; + li.addEventListener("click", () => { + inputElement.value = result.title ?? ""; + suggestionsList.style.display = "none"; + handleDetails(result.public_id); + }); + suggestionsList.appendChild(li); + li.appendChild(title); + title.appendChild(desc); + if (result.categories) { + const category = document.createElement("span"); + category.textContent = result.categories[0]; + category.className = "localities-search-category"; + title.appendChild(category); + } else { + const type = document.createElement("span"); + type.textContent = result.types[0]; + type.className = "localities-search-type"; + title.appendChild(type); + } + }); + suggestionsList.style.display = "block"; + clearSearchBtn.style.display = "block"; + } else { + suggestionsList.style.display = "none"; + } + } +} + +document.addEventListener("click", (event) => { + const targetElement = event.target as Element; + const isClickInsideAutocomplete = targetElement.closest( + "#autocomplete-container" + ); + + if (!isClickInsideAutocomplete && suggestionsList) { + suggestionsList.style.display = "none"; + } +}); + +// [START woosmap_localities_search_debounce_promise] +let PRESERVE_COMMENT_ABOVE; // force tsc to maintain the comment above eslint-disable-line + +type DebouncePromiseFunction = ( + ...args: Args +) => Promise; + +function debouncePromise( + fn: (...args: Args) => Promise, + delay: number +): DebouncePromiseFunction { + let timeoutId: ReturnType | null = null; + let latestResolve: ((value: T | PromiseLike) => void) | null = null; + let latestReject: ((reason?: any) => void) | null = null; + + return function (...args: Args): Promise { + return new Promise((resolve, reject) => { + if (timeoutId !== null) { + clearTimeout(timeoutId); + } + latestResolve = resolve; + latestReject = reject; + timeoutId = setTimeout(() => { + fn(...args) + .then((result) => { + if (latestResolve === resolve && latestReject === reject) { + resolve(result); + } + }) + .catch((error) => { + if (latestResolve === resolve && latestReject === reject) { + reject(error); + } + }); + }, delay); + }); + }; +} + +// [END woosmap_localities_search_debounce_promise] */ +PRESERVE_COMMENT_ABOVE; // force tsc to maintain the comment above eslint-disable-line + +declare global { + interface Window { + initMap: () => void; + } +} +window.initMap = initMap; +// [END woosmap_localities_search] + +export {}; diff --git a/samples/localities-search/localities-search.json b/samples/localities-search/localities-search.json new file mode 100644 index 00000000..25dc5f24 --- /dev/null +++ b/samples/localities-search/localities-search.json @@ -0,0 +1,14 @@ +{ + "title": "Localities Search", + "description": "Use the LocalitiesService to autocomplete and get details of localities, postal codes or addresses.", + "category": "Localities", + "callback": "initMap", + "tag": "localities_search", + "name": "localities-search", + "pagination": { + "data": "mode", + "size": 1, + "alias": "mode" + }, + "permalink": "samples/{{ page.fileSlug }}/{{mode}}/{% if mode == 'jsfiddle' %}demo{% else %}index{% endif %}.{{ page.outputFileExtension }}" +} \ No newline at end of file diff --git a/samples/localities-search/style.scss b/samples/localities-search/style.scss new file mode 100644 index 00000000..7b7368b1 --- /dev/null +++ b/samples/localities-search/style.scss @@ -0,0 +1,92 @@ +@use 'sass:meta'; // To enable @use via meta.load-css and keep comments in order + +/* [START woosmap_localities_search] */ +@include meta.load-css("../../shared/scss/_default.scss"); +@include meta.load-css("../../shared/scss/_autocomplete_input.scss"); +@include meta.load-css("../../shared/scss/_autocomplete_list.scss"); + +#app { + height: 100%; +} + +.localities-search-title { + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.localities-search-description { + font-weight: lighter; + color: #333; + font-size: 0.8rem; +} + +.localities-search-category { + color: #c46500; + font-size: 0.7rem; + font-style: italic; + align-self: flex-end; +} + +.localities-search-type { + color: #000000; + font-size: 0.7rem; + font-style: italic; + align-self: flex-end; +} + + +.detailsResult { + display: none; + position: absolute; + right: 10px; + bottom: 25px; + border-radius: 6px; + max-width: 240px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(0, 0, 0, 0.02); + z-index: 1; + overflow: hidden; + + .info { + padding: 12px 16px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + background-color: #fff; + } + + .detailsOptions { + overflow-y: auto; + max-height: 240px; + font-size: 12px; + padding-top: 12px; + background-color: #fff; + + .option-detail { + display: flex; + flex-wrap: wrap; + padding: 0 12px 8px 12px; + margin: 0; + + &-label { + color: rgba(0, 0, 0, 0.5); + margin-right: 4px; + } + } + } + } + + .address-components { + padding: 0 0 18px 0; + background-color: rgba(0, 0, 0, 0.03); + } + + .address-components .title { + color: rgba(0, 0, 0, 0.5); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1px; + padding: 16px 12px 10px 12px; + } + + +/* [END woosmap_localities_search] */