├── .DS_Store
├── .gitignore
├── .npmignore
├── BUILD.md
├── README.md
├── demo
├── index.css
└── index.html
├── img
├── address-autocomplete-example-dark.png
├── address-autocomplete-example-round-borders-dark.png
├── address-autocomplete-example-round-borders.png
└── address-autocomplete-example.png
├── jest.config.js
├── package-lock.json
├── package.json
├── postcss.config.cjs
├── rollup-config.mjs
├── src
├── autocomplete.ts
├── countries.json
├── helpers
│ ├── calculation.helper.ts
│ ├── callbacks.ts
│ ├── constants.ts
│ └── dom.helper.ts
└── index.ts
├── styles
├── minimal-dark.css
├── minimal.css
├── round-borders-dark.css
└── round-borders.css
├── tests
├── geocoder-autocomplete.test.ts
├── test-data.ts
└── test-helper.ts
└── tsconfig.json
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoapify/geocoder-autocomplete/ffa8e5b976898e47c7aa4f171530d80183be0e1b/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /dist
3 | *.tgz
4 | .vscode/*
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | img
3 | tsconfig.json
4 | *.tgz
5 | .vscode/*
6 | rollup-config.mjs
7 | postcss.config.cjs
--------------------------------------------------------------------------------
/BUILD.md:
--------------------------------------------------------------------------------
1 | ## Requirements
2 | Make sure you run at least Node 18.0.0
3 |
4 | ## Demo
5 |
6 | To run the demo:
7 | 1) Put "API_KEY" in demo/index.html
8 | 2) Execute "npm run start:demo"
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Geoapify Geocoder Autocomplete
2 |
3 | The Geoapify Geocoder Autocomplete is a JavaScript (TypeScript) library designed to enhance web applications and HTML pages by adding advanced **address autocomplete** functionality and **address autofill** input fields. It harnesses the power of the [Geoapify Geocoding API](https://www.geoapify.com/geocoding-api/) to provide accurate and efficient address search capabilities, making it an essential tool for enhancing the geolocation services of web-based applications.
4 |
5 | 
6 |
7 | * **Customizable Address Input**: Easily embed address input fields within your web application by adding them to provided HTML containers (e.g., `DIV` elements), allowing for flexible integration and styling.
8 | * **API Integration Flexibility**: By default, the library seamlessly connects to the [Geoapify Address Autocomplete API](https://www.geoapify.com/address-autocomplete/) to retrieve address suggestions. However, developers have the freedom to integrate and combine other third-party Address Search APIs, allowing for extensive customization and the incorporation of multiple data sources.
9 | * **Search Customization**: Tailor your address search with precision by adding filters and bias parameters. This level of customization empowers developers to fine-tune search queries, ensuring more accurate and relevant address suggestions for users.
10 | * **Structured Address Forms**: Utilize the type parameter to craft address input forms that enable users to enter structured addresses, including postal codes, cities, countries, address lines, and more.
11 | * **Place Details Integration**: Optionally, the library can call the [Geoapify Place Details API](https://www.geoapify.com/place-details-api/), providing users with detailed city and building boundaries as part of the search results. This enhances location context and visualization for a richer user experience.
12 | * **Customizable Look-and-Feel**: Tailor the appearance of the address input and autocomplete suggestions effortlessly. The library offers four distinct styles for both light and dark themes, providing design flexibility. Moreover, developers can further fine-tune the visual aspects using CSS classes to achieve a seamless integration with their application's aesthetics.
13 | * **Zero Dependencies**: The library is intentionally built with zero external dependencies. This means that it operates independently and does not rely on external libraries or packages.
14 |
15 | ## [Playground](https://apidocs.geoapify.com/playground/geocoding/#autocomplete)
16 | ## [JSFiddle demo: Address Field + Map](https://jsfiddle.net/Geoapify/jsgw53z8/)
17 | ## [JSFiddle demo: Address Form 1](https://jsfiddle.net/Geoapify/t0eg541k/)
18 | ## [JSFiddle demo: Address Form 2](https://jsfiddle.net/Geoapify/stgek5wf/)
19 |
20 | ## Getting Geoapify API key
21 | In case you decide to use Geoapify API to search addresses, you'll need to obtain an API key.
22 |
23 | Register for free and obtain your API key at [myprojects.geoapify.com](https://myprojects.geoapify.com/). Geoapify offers a flexible [Freemium pricing model](https://www.geoapify.com/pricing/) that allows you to begin using our services at no cost and seamlessly scale your usage as your needs grow.
24 |
25 | ## Installation
26 | Start enhancing your web applications today with `@geoapify/geocoder-autocomplete`:
27 |
28 | ### Option 1
29 | Install the Geocoder Autocomplete package with NPM or Yarn project manager:
30 |
31 | ```
32 | npm install @geoapify/geocoder-autocomplete
33 | # or
34 | yarn add @geoapify/geocoder-autocomplete
35 | ```
36 | ### Option 2
37 | Refer to the Geocoder Autocomplete library as a UMD module (for CMS websites, including WordPress):
38 | ```html
39 |
40 |
41 |
42 |
43 | ...
44 |
45 | ...
46 |
47 | ```
48 |
49 | You can use [UNPKG](https://unpkg.com/) to refer or download the library:
50 |
51 | ```https://unpkg.com/@geoapify/geocoder-autocomplete@^1/dist/index.min.js```
52 |
53 | ```https://unpkg.com/@geoapify/geocoder-autocomplete@^1/styles/minimal.css```
54 |
55 | ## Using `@geoapify/geocoder-autocomplete` in your project
56 | Follow the steps below to seamlessly integrate `@geoapify/geocoder-autocomplete` into your project.
57 |
58 | ### STEP 1. Prepare your webpage
59 | Incorporate a container element into your webpage where the autocomplete input will be seamlessly integrated, utilizing the full width of the specified element:
60 |
61 | ```html
62 |
63 | ```
64 | The container element must have `position: absolute` or `position: relative`
65 | ```css
66 | .autocomplete-container {
67 | position: relative;
68 | }
69 | ```
70 | ### STEP 2. Initialize the autocomplete field
71 |
72 | * **Option 1**. Import the Geocoder Autocomplete types when you use it as a module:
73 | ```javascript
74 | import { GeocoderAutocomplete } from '@geoapify/geocoder-autocomplete';
75 |
76 | const autocomplete = new GeocoderAutocomplete(
77 | document.getElementById("autocomplete"),
78 | 'YOUR_API_KEY',
79 | { /* Geocoder options */ });
80 |
81 | autocomplete.on('select', (location) => {
82 | // check selected location here
83 | });
84 |
85 | autocomplete.on('suggestions', (suggestions) => {
86 | // process suggestions here
87 | });
88 | ```
89 |
90 | * **Option 2**. Refer to the Geocoder Autocomplete as `autocomplete` when you added it as a script:
91 | ```javascript
92 | const autocompleteInput = new autocomplete.GeocoderAutocomplete(
93 | document.getElementById("autocomplete"),
94 | 'YOUR_API_KEY',
95 | { /* Geocoder options */ });
96 |
97 | autocompleteInput.on('select', (location) => {
98 | // check selected location here
99 | });
100 |
101 | autocompleteInput.on('suggestions', (suggestions) => {
102 | // process suggestions here
103 | });
104 | ```
105 | ### STEP 3. Add the Autocomplete Input styles:
106 | We provide several Themes within the library:
107 | * `minimal` and `round-borders` - for webpages with light background color
108 | * `minimal-dark` and `round-borders-dark` for webpages with dark background color.
109 |
110 | You can import the appropriate css-file to your styles:
111 | ```css
112 | @import "~@geoapify/geocoder-autocomplete/styles/minimal.css";
113 | ```
114 | or as a link in a HTML-file:
115 | ```html
116 |
117 | ```
118 |
119 | ## Transitioning from 1.x: Replacing `skipDetails` with `addDetails`
120 |
121 | When transitioning from the 1.x version of the library to the 2.x version, it's important to note that the `skipDetails` option has been replaced by the `addDetails` option. This change enhances the clarity of the parameter, as it now explicitly indicates whether you want to include or exclude additional details in the search results. To maintain compatibility with the updated version, make sure to adjust your code accordingly by using the `addDetails` option when needed for your address search functionality.
122 |
123 | So, if you require place details in your search results, you should set the `addDetails` option to `true`.
124 | ## Documentation
125 |
126 | Below, you'll find `@geoapify/geocoder-autocomplete`'s detailed documentation, usage examples, advanced features, and more. You'll find the information you need to seamlessly integrate address autocomplete and enhance your web-based geolocation services and user experiences.
127 | ### Creation
128 |
129 | | Option | Type | Description |
130 | | ------ | ------ | ------ |
131 | | constructor | *GeocoderAutocomplete( el, geoapifyApiKey, options?)* | *GeocoderAutocomplete(document.getElementById('autocomplete'), 'sdf45dfg68879fhsdgs346dfhdj', { lang: 'it' }*
132 |
133 | ### GeocoderAutocompleteOptions
134 | | Option | Type | Description |
135 | | ------ | ------ | ------ |
136 | | type | `country`, `state`, `city`, `postcode`, `street`, `amenity` | Type of the location |
137 | | lang | LanguageCode | Results language |
138 | | limit | number | The maximal number of returned suggestions |
139 | | placeholder | string | An input field placeholder |
140 | | debounceDelay | number | A delay between user input and the API call to prevent unnecessary calls. The default value is 100ms. |
141 | | skipIcons | boolean | Don't add icons to suggestions |
142 | | addDetails | boolean | Call Place Details API on selection change to get the place details. For example, opening hours or boundary |
143 | | skipSelectionOnArrowKey | boolean | Don't choose the location with the arrow keys |
144 | | filter | FilterOptions | Filter places by country, boundary, circle, place |
145 | | bias | BiasOptions | Prefer places by country, boundary, circle, location |
146 | | allowNonVerifiedHouseNumber | boolean | Allow the addition of house numbers that are not verified by the Geocoding API or missing in the database. Check the *"Working with non-verified values"* section for details. |
147 | | allowNonVerifiedStreet | boolean | Allow the addition of streets that are not verified by the Geocoding API or missing in the database. Check the *"Working with non-verified values"* section for details. |
148 |
149 | #### LanguageCode
150 | 2-character [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code: `ab`, `aa`, `af`, `ak`, `sq`, `am`, `ar`, `an`, `hy`, `as`, `av`, `ae`, `ay`, `az`, `bm`, `ba`, `eu`, `be`, `bn`, `bh`, `bi`, `bs`, `br`, `bg`, `my`, `ca`, `ch`, `ce`, `ny`, `zh`, `cv`, `kw`, `co`, `cr`, `hr`, `cs`, `da`, `dv`, `nl`, `en`, `eo`, `et`, `ee`, `fo`, `fj`, `fi`, `fr`, `ff`, `gl`, `ka`, `de`, `el`, `gn`, `gu`, `ht`, `ha`, `he`, `hz`, `hi`, `ho`, `hu`, `ia`, `id`, `ie`, `ga`, `ig`, `ik`, `io`, `is`, `it`, `iu`, `ja`, `jv`, `kl`, `kn`, `kr`, `ks`, `kk`, `km`, `ki`, `rw`, `ky`, `kv`, `kg`, `ko`, `ku`, `kj`, `la`, `lb`, `lg`, `li`, `ln`, `lo`, `lt`, `lu`, `lv`, `gv`, `mk`, `mg`, `ms`, `ml`, `mt`, `mi`, `mr`, `mh`, `mn`, `na`, `nv`, `nb`, `nd`, `ne`, `ng`, `nn`, `no`, `ii`, `nr`, `oc`, `oj`, `cu`, `om`, `or`, `os`, `pa`, `pi`, `fa`, `pl`, `ps`, `pt`, `qu`, `rm`, `rn`, `ro`, `ru`, `sa`, `sc`, `sd`, `se`, `sm`, `sg`, `sr`, `gd`, `sn`, `si`, `sk`, `sl`, `so`, `st`, `es`, `su`, `sw`, `ss`, `sv`, `ta`, `te`, `tg`, `th`, `ti`, `bo`, `tk`, `tl`, `tn`, `to`, `tr`, `ts`, `tt`, `tw`, `ty`, `ug`, `uk`, `ur`, `uz`, `ve`, `vi`, `vo`, `wa`, `cy`, `wo`, `fy`, `xh`, `yi`, `yo`, `za`.
151 |
152 | #### FilterOptions
153 | The Geocoder Autocomplete allows specify the following types of filters:
154 |
155 | Name | Filter | Filter Value | Description | Examples
156 | --- | --- | --- | --- | ---
157 | By circle | *circle* | `{ lon: number ,lat: number, radiusMeters: number }` | Search places inside the circle | `filter['circle'] = {lon: -87.770231, lat: 41.878968, radiusMeters: 5000}`
158 | By rectangle | *rect* | `{ lon1: number ,lat1: number, lon2: number ,lat2: number}` | Search places inside the rectangle | `filter['rect'] = {lon1: 89.097540, lat1: 39.668983, lon2: -88.399274, lat2: 40.383412}`
159 | By country | *countrycode* | `CountyCode[]` | Search places in the countries | `filter['countrycode'] = ['de', 'fr', 'es']`
160 | By place | *place* | `string` | Search for places within a given city or postal code. For example, search for streets within a city. Use the 'place_id' returned by another search to specify a filter. | `filter['place'] = '51ac66e77e9826274059f9426dc08c114840f00101f901dcf3000000000000c00208'`
161 |
162 | You can provide filters as initial options or add by calling a function:
163 | ```
164 | options.filter = {
165 | 'circle': {lon: -87.770231, lat: 41.878968, radiusMeters: 5000}
166 | };
167 |
168 | // or
169 |
170 | autocomplete.addFilterByCircle({lon: -87.770231, lat: 41.878968, radiusMeters: 5000});
171 |
172 | ```
173 |
174 | You can combine several filters (but only one of each type) in one request. The **AND** logic is applied to the multiple filters.
175 |
176 | #### BiasOptions
177 | You can chage priority of the search by setting bias. The Geocoder Autocomplete allows specify the following types of bias:
178 |
179 | Name | Bias | Bias Value | Description | Examples
180 | --- | --- | --- | --- | ---
181 | By circle | *circle* | `{ lon: number ,lat: number, radiusMeters: number }` | First, search places inside the circle, then worldwide | `bias['circle'] = {lon: -87.770231, lat: 41.878968, radiusMeters: 5000}`
182 | By rectangle | *rect* | `{ lon1: number ,lat1: number, lon2: number ,lat2: number}` | First, search places inside the rectangle, then worldwide | `bias['rect'] = {lon1: 89.097540, lat1: 39.668983, lon2: -88.399274, lat2: 40.383412}`
183 | By country | *countrycode* | `CountyCode[]` | First, search places in the countries, then worldwide | `bias['countrycode'] = ['de', 'fr', 'es']`
184 | By location | *proximity* | `{lon: number ,lat: number}` | Prioritize results by farness from the location | `bias['proximity'] = {lon: -87.770231, lat: 41.878968}`
185 |
186 | You can combine several bias parameters (but only one of each type) in one request. The OR logic is applied to the multiple bias.
187 |
188 | NOTE! The default bias for the geocoding requests is "countrycode:auto", the API detects a country by IP address and provides the search there first. Set `bias['countrycode'] = ['none']` to avoid prioritization by country.
189 |
190 | You can provide filters as initial options or add by calling a function:
191 | ```
192 | options.bias = {
193 | 'circle': {lon: -87.770231, lat: 41.878968, radiusMeters: 5000},
194 | 'countrycode': ['none']
195 | };
196 |
197 | // or
198 |
199 | autocomplete.addBiasByCircle({lon: -87.770231, lat: 41.878968, radiusMeters: 5000});
200 |
201 | ```
202 | #### CountyCode
203 |
204 | * Use 'auto' to detect the country by IP address;
205 | * Use 'none' to skip;
206 | * 2-digits [ISO 3166-1 Alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements) country code: `ad`, `ae`, `af`, `ag`, `ai`, `al`, `am`, `an`, `ao`, `ap`, `aq`, `ar`, `as`, `at`, `au`, `aw`, `az`, `ba`, `bb`, `bd`, `be`, `bf`, `bg`, `bh`, `bi`, `bj`, `bm`, `bn`, `bo`, `br`, `bs`, `bt`, `bv`, `bw`, `by`, `bz`, `ca`, `cc`, `cd`, `cf`, `cg`, `ch`, `ci`, `ck`, `cl`, `cm`, `cn`, `co`, `cr`, `cu`, `cv`, `cx`, `cy`, `cz`, `de`, `dj`, `dk`, `dm`, `do`, `dz`, `ec`, `ee`, `eg`, `eh`, `er`, `es`, `et`, `eu`, `fi`, `fj`, `fk`, `fm`, `fo`, `fr`, `ga`, `gb`, `gd`, `ge`, `gf`, `gh`, `gi`, `gl`, `gm`, `gn`, `gp`, `gq`, `gr`, `gs`, `gt`, `gu`, `gw`, `gy`, `hk`, `hm`, `hn`, `hr`, `ht`, `hu`, `id`, `ie`, `il`, `in`, `io`, `iq`, `ir`, `is`, `it`, `jm`, `jo`, `jp`, `ke`, `kg`, `kh`, `ki`, `km`, `kn`, `kp`, `kr`, `kw`, `ky`, `kz`, `la`, `lb`, `lc`, `li`, `lk`, `lr`, `ls`, `lt`, `lu`, `lv`, `ly`, `ma`, `mc`, `md`, `me`, `mg`, `mh`, `mk`, `ml`, `mm`, `mn`, `mo`, `mp`, `mq`, `mr`, `ms`, `mt`, `mu`, `mv`, `mw`, `mx`, `my`, `mz`, `na`, `nc`, `ne`, `nf`, `ng`, `ni`, `nl`, `no`, `np`, `nr`, `nu`, `nz`, `om`, `pa`, `pe`, `pf`, `pg`, `ph`, `pk`, `pl`, `pm`, `pr`, `ps`, `pt`, `pw`, `py`, `qa`, `re`, `ro`, `rs`, `ru`, `rw`, `sa`, `sb`, `sc`, `sd`, `se`, `sg`, `sh`, `si`, `sj`, `sk`, `sl`, `sm`, `sn`, `so`, `sr`, `st`, `sv`, `sy`, `sz`, `tc`, `td`, `tf`, `tg`, `th`, `tj`, `tk`, `tm`, `tn`, `to`, `tr`, `tt`, `tv`, `tw`, `tz`, `ua`, `ug`, `um`, `us`, `uy`, `uz`, `va`, `vc`, `ve`, `vg`, `vi`, `vn`, `vu`, `wf`, `ws`, `ye`, `yt`, `za`, `zm`, `zw`.
207 |
208 | Learn more about Geoapify Geocoder options on [Geoapify Documentation page](https://apidocs.geoapify.com/docs/geocoding).
209 |
210 | ### Methods
211 |
212 | Here's a description of the API methods:
213 |
214 | | Method | Description |
215 | |-----------------------------------------|---------------------------------------------------------------------|
216 | | *setType(type: 'country' or 'state' or 'city' or 'postcode' or 'street' or 'amenity'): void* | Sets the type of location for address suggestions. |
217 | | *setLang(lang: SupportedLanguage): void* | Sets the language for address suggestions. |
218 | | *setCountryCodes(codes: CountyCode[]): void* | Sets specific country codes to filter address suggestions. |
219 | | *setPosition(position: GeoPosition = {lat: number; lon: number}): void* | Sets the geographic position to influence suggestions based on proximity.|
220 | | *setLimit(limit: number): void* | Sets the maximum number of suggestions to display. |
221 | | *setValue(value: string): void* | Sets the value of the input field programmatically. |
222 | | *getValue(): string* | Retrieves the current value of the input field. |
223 | | *addFilterByCountry(codes: CountyCode[]): void* | Adds a filter to include or exclude suggestions based on specific country codes. |
224 | | *addFilterByCircle(filterByCircle: ByCircleOptions = {lon: number; lat: number; radiusMeters: number }): void* | Adds a circular filter to include or exclude suggestions within a specified geographic area. |
225 | | *addFilterByRect(filterByRect: ByRectOptions = { lon1: number; lat1: number; lon2: number; lat2: number}): void* | Adds a rectangular filter to include or exclude suggestions within a specified geographic area. |
226 | | *addFilterByPlace(filterByPlace: string): void* | Adds a filter to include or exclude suggestions based on a specific place or location. |
227 | | *clearFilters(): void* | Clears all previously added filters. |
228 | | *addBiasByCountry(codes: CountyCode[]): void* | Adds a bias to prioritize suggestions from specific countries. |
229 | | *addBiasByCircle(biasByCircle: ByCircleOptions = {lon: number; lat: number; radiusMeters: number }): void* | Adds a circular bias to prioritize suggestions within a specified geographic area. |
230 | | *addBiasByRect(biasByRect: ByRectOptions = { lon1: number; lat1: number; lon2: number; lat2: number}): void* | Adds a rectangular bias to prioritize suggestions within a specified geographic area. |
231 | | *addBiasByProximity(biasByProximity: ByProximityOptions = { lon: number; lat: number }): void* | Adds a bias based on proximity to a specific location. |
232 | | *clearBias(): void* | Clears all previously added biases. |
233 | | *setSuggestionsFilter(suggestionsFilterFunc?: (suggestions: GeoJSON.Feature[]) => GeoJSON.Feature[]): void* | Sets a custom filter function for suggestions. |
234 | | *setPreprocessHook(preprocessHookFunc?: (value: string) => string): void* | Sets a preprocessing hook to modify the input value before sending a request. |
235 | | *setPostprocessHook(postprocessHookFunc?: (value: string) => string): void* | Sets a post-processing hook to modify the suggestion values after retrieval. |
236 | | *isOpen(): boolean* | Checks if the suggestions dropdown is currently open. |
237 | | *close(): void* | Manually closes the suggestions dropdown. |
238 | | *open(): void* | Manually opens the suggestions dropdown. |
239 | | *sendGeocoderRequest(value: string): Promise* | Sends a geocoder request based on the provided value and returns a Promise with the response in [GeoJSON FeatureCollection](https://en.wikipedia.org/wiki/GeoJSON) format containing suggestions. |
240 | | *sendPlaceDetailsRequest(feature: GeoJSON.Feature): Promise* | Sends a place details request based on the provided [GeoJSON feature](https://en.wikipedia.org/wiki/GeoJSON) and returns a Promise with the response in GeoJSON Feature format containing place details. |
241 | | *setSendGeocoderRequestFunc(sendGeocoderRequestFunc: (value: string, geocoderAutocomplete: GeocoderAutocomplete) => Promise): void* | Sets a custom function to send geocoder requests. |
242 | | *setSendPlaceDetailsRequestFunc(sendPlaceDetailsRequestFunc: (feature: GeoJSON.Feature, geocoderAutocomplete: GeocoderAutocomplete) => Promise): void* | Sets a custom function to send place details requests. |
243 | | *on(operation: 'select' or 'suggestions' or 'input' or 'close' or 'open', callback: (param: any) => void): void* | Attaches event listeners to various operations such as selection, suggestions, input changes, and dropdown open/close. |
244 | | *off(operation: 'select' or 'suggestions' or 'input' or 'close' or 'open', callback?: (param: any) => void): void* | Detaches previously attached event listeners. |
245 | | *once(operation: 'select' or 'suggestions' or 'input' or 'close' or 'open', callback: (param: any) => void): void* | Attaches a one-time event listener that triggers only once for the specified operation. |
246 |
247 | #### Example. Setting Geocoder options
248 | The library offers a flexible API that enables the dynamic configuration of Geoapify Geocoder options at runtime:
249 |
250 | ```javascript
251 | const autocomplete = new GeocoderAutocomplete(...);
252 |
253 | // set location type
254 | autocomplete.setType(options.type);
255 | // set results language
256 | autocomplete.setLang(options.lang);
257 | // set dropdown elements limit
258 | autocomplete.setLimit(options.limit);
259 |
260 | // set filter
261 | autocomplete.addFilterByCountry(codes);
262 | autocomplete.addFilterByCircle(filterByCircle);
263 | autocomplete.addFilterByRect(filterByRect);
264 | autocomplete.addFilterByPlace(placeId);
265 | autocomplete.clearFilters()
266 |
267 | // set bias
268 | autocomplete.addBiasByCountry(codes);
269 | autocomplete.addBiasByCircle(biasByCircle);
270 | autocomplete.addBiasByRect(biasByRect);
271 | autocomplete.addBiasByProximity(biasByProximity);
272 | autocomplete.clearBias();
273 | ```
274 |
275 | #### Example. Close and open the suggestions dropdown automatically
276 |
277 | You can also interact with the suggestions dropdown through the API, allowing you to check its current state and toggle the open/close state as needed:
278 |
279 | ```javascript
280 | // get and change dropdown list state
281 | autocomplete.isOpen();
282 | autocomplete.open();
283 | autocomplete.close();
284 | ```
285 |
286 | #### Example. Setting and getting the display value
287 | You have the ability to retrieve the current value or modify the displayed value:
288 |
289 | ```javascript
290 | autocomplete.setValue(value);
291 |
292 | const displayValue = autocomplete.getValue();
293 | ```
294 |
295 | #### Example. Hooks and suggestions filter
296 |
297 | Through the inclusion of preprocessing and post-processing hooks, as well as a suggestions filter, you modify both the entered values before sending the request and the received suggestions list:
298 |
299 | * **Preprocess Hook** - Empower your address search by modifying the input text dynamically. For instance, when expecting a street name, you can enhance the search by adding a city or postcode to find streets within that specific location.
300 | * **Postprocess Hook** - Tailor the displayed text in both the input field and suggestions list to match your desired format. For example, you can choose to display only the street name, offering a cleaner and more user-friendly presentation.
301 | * **Suggestions Filter** - Efficiently manage and filter suggestions to prevent duplication or remove unnecessary items. This feature is particularly useful when applying a post-process hook, ensuring that suggestions with identical street names are intelligently handled and presented without redundancy.
302 |
303 | ```javascript
304 |
305 | // add preprocess hook
306 | autocomplete.setPreprocessHook((value: string) => {
307 | // return augmented value here
308 | return `${value} ${someAdditionalInfo}`
309 | });
310 |
311 | // remove the hook
312 | autocomplete.setPreprocessHook(null);
313 |
314 | autocomplete.setPostprocessHook((feature) => {
315 | // feature is GeoJSON feature containing structured address
316 | // return a part of the address to be displayed
317 | return feature.properties.street;
318 | });
319 |
320 | // remove the hook
321 | autocomplete.setPostprocessHook(null);
322 |
323 | autocomplete.setSuggestionsFilter((features: any[]) => {
324 | // features is an array of GeoJSON features, that contains suggestions
325 | // return filtered array
326 | const processedStreets = [];
327 | const filtered = features.filter(feature => {
328 | if (!feature.properties.street || processedStreets.indexOf(feature.properties.street) >= 0) {
329 | return false;
330 | } else {
331 | processedStreets.push(feature.properties.street);
332 | return true;
333 | }
334 | });
335 | return filtered;
336 | });
337 |
338 | // remove the filter
339 | autocomplete.setSuggestionsFilter(null);
340 |
341 | ```
342 |
343 | #### Example. Overwrite send request method
344 |
345 | The library provides the flexibility to override default API methods, with Geoapify API being the default choice, for searching addresses. This allows you to seamlessly integrate custom or third-party address search services, offering you the freedom to tailor the geocoding functionality to your specific needs.
346 |
347 | Here is an example of how you can override the default API methods to integrate custom or third-party address search services:
348 |
349 | ```javascript
350 | autocomplete.setSendGeocoderRequestFunc((value: string, geocoderAutocomplete: GeocoderAutocomplete) => {
351 |
352 | if (/* check here if you can geocode the value */) {
353 | ...
354 | return new Promise((resolve, reject) => {
355 | resolve({
356 | "type": "FeatureCollection",
357 | "features": [
358 | {
359 | "type": "Feature",
360 | "properties": {
361 | ...
362 | "formatted": address
363 | },
364 | "geometry": {
365 | "type": "Point",
366 | "coordinates": [lon, lat]
367 | }
368 | }
369 | ]
370 | })
371 | });
372 | }
373 |
374 | // You can call the default search method this way
375 | return geocoderAutocomplete.sendGeocoderRequest(value);
376 | });
377 |
378 | autocomplete.setSendPlaceDetailsRequestFunc((feature: any, geocoderAutocomplete: GeocoderAutocomplete) => {
379 | if (/* you have details for the place */) {
380 | ...
381 | return new Promise((resolve, reject) => {
382 | resolve({
383 | "type": "FeatureCollection",
384 | "features": [
385 | {
386 | "type": "Feature",
387 | "properties": {
388 | ...
389 | "formatted": address
390 | },
391 | "geometry": { ... }
392 | }
393 | ]
394 | })
395 | });
396 | }
397 |
398 | // You can call the default place details method this way
399 | return geocoderAutocomplete.sendPlaceDetailsRequest(feature);
400 | });
401 | ```
402 |
403 | ### Events
404 |
405 | `@geoapify/geocoder-autocomplete` provides a set of event handling functions—on, off, and once. These functions allow you to attach, detach, and manage event listeners for various user interactions and changes within the library.
406 |
407 | | Event Name | Description |
408 | |---------------|------------------------------------------------------------------------------------------------------------------------|
409 | | `select` | Triggered when a suggestion is selected from the dropdown. Useful for capturing and responding to user selections. |
410 | | `suggestions` | Fired when suggestions are provided, allowing access to the list of suggestions for customization or interaction. |
411 | | `input` | Occurs whenever the input field value changes, providing real-time feedback on user input for dynamic adjustments. |
412 | | `close` | Triggered when the suggestions dropdown is closed, enabling actions to be performed when the dropdown closes. |
413 | | `open` | Fired when the suggestions dropdown is opened, offering an opportunity to respond to dropdown opening events. |
414 |
415 | These events offer flexibility and customization options for creating tailored interactions and user experiences in your application.
416 |
417 | #### Example. Geocoder Autocomplete events
418 |
419 | You have the option to attach event listeners to the Geocoder Autocomplete:
420 |
421 | ```javascript
422 | autocomplete.on('select', (location) => {
423 | // check selected location here
424 | });
425 |
426 | autocomplete.on('suggestions', (suggestions) => {
427 | // process suggestions here
428 | });
429 |
430 | autocomplete.on('input', (userInput) => {
431 | // process user input here
432 | });
433 |
434 | autocomplete.on('open', () => {
435 | // dropdown list is opened
436 | });
437 |
438 | autocomplete.on('close', () => {
439 | // dropdown list is closed
440 | });
441 |
442 | autocomplete.once('open', () => {
443 | // dropdown list is opened, one time callback
444 | });
445 |
446 | autocomplete.once('close', () => {
447 | // dropdown list is closed, one time callback
448 | });
449 | ```
450 | The location have [GeoJSON.Feature](https://geojson.org/) type, suggestions have GeoJSON.Feature[] type. The feature properties contain information about address and location.
451 |
452 | Use `off()` function to remove event listerers:
453 | ```javascript
454 | // remove open event
455 | autocomplete.off('open', this.onOpen);
456 |
457 | // remove all open events
458 | autocomplete.off('open');
459 | ```
460 |
461 | Learn more about Geocoder result properties on [Geoapify Documentation page](https://apidocs.geoapify.com/docs/geocoding/).
462 |
463 | ## Styling
464 |
465 | We offer a variety of built-in themes within the library, catering to different webpage styles:
466 |
467 | 1. **Minimal Theme**: Designed for webpages with a light background color.
468 | 2. **Round Borders Theme**: Tailored for webpages with a light background color, featuring rounded borders.
469 | 3. **Minimal Dark Theme**: Ideal for webpages with a dark background color.
470 | 4. **Round Borders Dark Theme**: Specifically crafted for webpages with a dark background color, incorporating rounded borders.
471 |
472 | These themes offer versatile styling options to seamlessly integrate the address autocomplete component into various webpage designs.
473 |
474 | Moreover, if you prefer to have complete control over the styling, you have the opportunity to customize the component yourself. Below are the CSS classes used for styling:
475 |
476 | | Class Name | Description |
477 | | ---------------------------------------------- | -------------------------------------------------------------- |
478 | | `.geoapify-autocomplete-input` | Styles the input element. |
479 | | `.geoapify-autocomplete-items` | Styles the dropdown list. |
480 | | `.geoapify-autocomplete-items .active` | Styles the selected item in the dropdown list. |
481 | | `.geoapify-autocomplete-item` | Styles the individual dropdown list items. |
482 | | `.geoapify-autocomplete-item.icon` | Styles the icon within the dropdown list items. |
483 | | `.geoapify-autocomplete-item.text` | Styles the text within the dropdown list items. |
484 | | `.geoapify-close-button` | Styles the clear button. |
485 | | `.geoapify-autocomplete-items .main-part .non-verified` | Styles a portion of the street address that could not be verified by the Geocoder. |
486 |
487 | ## Working with non-verified address components
488 |
489 | When utilizing the Geocoder Autocomplete library to gather postal addresses, it's often advantageous to adopt a more permissive approach, allowing for the inclusion of non-verified address components such as house numbers and streets.
490 |
491 | In real-world scenarios, it's possible that newly constructed streets or houses may not yet be included in existing databases. Restricting users from entering such addresses may not align with your objectives.
492 |
493 | To accommodate users and enable the inclusion of non-verified address parts, you can leverage the `allowNonVerifiedHouseNumber` and `allowNonVerifiedStreet` parameters. These settings empower users to contribute address details that may not yet be officially validated, fostering flexibility and completeness in your address data collection process.
494 |
495 | ### How it works
496 |
497 | The library operates by utilizing the API to retrieve essential address details, including the parsed address, the located address, and a match type as results. Using this information as a foundation, the library enhances the result by filling in missing values, such as house numbers, to provide a more complete and user-friendly address representation.
498 |
499 | Notably, non-verified address components are denoted with a "non-verified" class, making them visually distinct by default, often highlighted in red to indicate their provisional or unverified status.
500 |
501 | It's essential to note that the GPS coordinates associated with the results correspond to the actual locations. Users have the flexibility to adjust these coordinates as needed to ensure accuracy.
502 |
503 | Furthermore, the result object is expanded to include a list of non-verified properties. For instance:
504 |
505 | ```json
506 | {
507 | "type": "Feature",
508 | "properties": {
509 | ...
510 | "address_line1": "Bürgermeister-Heinrich-Straße 60",
511 | "address_line2": "93077 Bad Abbach, Germany",
512 | "housenumber": "60",
513 | "nonVerifiedParts": [
514 | "housenumber"
515 | ]
516 | },
517 | ...
518 | }
519 | ```
520 |
521 | This extended result object provides transparency by clearly indicating which address components are non-verified, allowing for informed decision-making and customization based on the level of validation required for your specific use case.
522 |
523 | ## License
524 | This library is open-source and released under the MIT License.
525 |
526 | ## Contributions and Support
527 | We welcome contributions from the developer community. If you encounter any issues or have suggestions for improvements, please feel free to open an issue or submit a pull request on [GitHub](https://github.com/geoapify/geocoder-autocomplete).
--------------------------------------------------------------------------------
/demo/index.css:
--------------------------------------------------------------------------------
1 | .address-collection-container {
2 | max-width: 500px;
3 | margin: auto;
4 | }
5 |
6 | .autocomplete-container {
7 | position: relative;
8 | }
9 |
10 | .address-row {
11 | display: flex;
12 | flex-direction: row;
13 |
14 | margin-bottom: 10px;
15 | }
16 |
17 | .address-row .main-part {
18 | flex: 1
19 | }
20 |
21 | .small-input {
22 | width: 90px;
23 | }
24 |
25 | .margin-right {
26 | margin-right: 10px
27 | }
28 |
29 | .message-container {
30 | margin-top: 10px;
31 | }
32 |
33 | .button-container {
34 | margin-top: 20px;
35 | text-align: right
36 | }
37 |
38 | .warning-input {
39 | border-color: #ffb610;
40 | }
41 |
42 | .message-container {
43 | color: grey;
44 | }
45 |
46 | .geoapify-autocomplete-input {
47 | width: calc(100%) !important;
48 | }
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | My NPM Project
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 | House number:
18 |
19 |
20 |
21 |
22 |
23 |
24 | Apartment, suite (optional):
25 |
26 |
27 |
28 |
29 |
30 |
31 |
35 |
36 |
37 | ZIP code:
38 |
39 |
40 |
41 |
42 |
53 |
54 |
55 |
Check address
56 |
57 |
58 |
59 |
60 |
61 |
240 |
241 |
--------------------------------------------------------------------------------
/img/address-autocomplete-example-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoapify/geocoder-autocomplete/ffa8e5b976898e47c7aa4f171530d80183be0e1b/img/address-autocomplete-example-dark.png
--------------------------------------------------------------------------------
/img/address-autocomplete-example-round-borders-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoapify/geocoder-autocomplete/ffa8e5b976898e47c7aa4f171530d80183be0e1b/img/address-autocomplete-example-round-borders-dark.png
--------------------------------------------------------------------------------
/img/address-autocomplete-example-round-borders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoapify/geocoder-autocomplete/ffa8e5b976898e47c7aa4f171530d80183be0e1b/img/address-autocomplete-example-round-borders.png
--------------------------------------------------------------------------------
/img/address-autocomplete-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoapify/geocoder-autocomplete/ffa8e5b976898e47c7aa4f171530d80183be0e1b/img/address-autocomplete-example.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: "jsdom",
3 | transform: {
4 | "^.+.tsx?$": ["ts-jest",{}],
5 | },
6 | testTimeout: 30000
7 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@geoapify/geocoder-autocomplete",
3 | "version": "2.1.0",
4 | "description": "Postal address autocomplete input field",
5 | "main": "dist/index.cjs.js",
6 | "typings": "dist/index.d.ts",
7 | "minimized": "dist/index.min.js",
8 | "module": "dist/index.min.esm.js",
9 | "scripts": {
10 | "test": "jest",
11 | "test-coverage": "jest --coverage",
12 | "build": "tsc",
13 | "build-all": "npm run build && npm run build-minimized",
14 | "build-minimized": "rollup -c rollup-config.mjs",
15 | "build-css-minimized": "postcss -o styles/minimal.min.css styles/minimal.css",
16 | "start:demo": "npm install && npm run build-all && open demo/index.html"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://git@github.com/geoapify/geocoder-autocomplete.git"
21 | },
22 | "keywords": [
23 | "geocoding",
24 | "address",
25 | "autocomplete",
26 | "search",
27 | "street",
28 | "city",
29 | "country",
30 | "state",
31 | "amenity",
32 | "location",
33 | "regions",
34 | "boundaries",
35 | "place details"
36 | ],
37 | "author": {
38 | "name": "Geoapify GmbH",
39 | "email": "info@geoapify.com",
40 | "url": "https://geoapify.com"
41 | },
42 | "license": "MIT",
43 | "bugs": {
44 | "url": "https://github.com/geoapify/geocoder-autocomplete/issues"
45 | },
46 | "homepage": "https://github.com/geoapify/geocoder-autocomplete#readme",
47 | "devDependencies": {
48 | "@rollup/plugin-commonjs": "^26.0.1",
49 | "@rollup/plugin-json": "^6.1.0",
50 | "@rollup/plugin-node-resolve": "^15.2.3",
51 | "@rollup/plugin-terser": "^0.4.4",
52 | "@rollup/plugin-typescript": "^11.1.6",
53 | "@testing-library/jest-dom": "^6.5.0",
54 | "@types/jest": "^29.5.13",
55 | "cssnano": "^7.0.5",
56 | "jest": "^29.7.0",
57 | "jest-environment-jsdom": "^29.7.0",
58 | "jest-fetch-mock": "^3.0.3",
59 | "jsdom": "^25.0.0",
60 | "postcss": "^8.4.44",
61 | "postcss-cli": "^11.0.0",
62 | "postcss-import": "^16.1.0",
63 | "rollup": "^4.21.2",
64 | "ts-jest": "^29.2.5",
65 | "tslib": "^2.7.0",
66 | "typescript": "^5.5.4"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-import'),
4 | require('cssnano')
5 | ]
6 | }
--------------------------------------------------------------------------------
/rollup-config.mjs:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript';
2 | import resolve from '@rollup/plugin-node-resolve';
3 | import terser from '@rollup/plugin-terser';
4 | import commonjs from '@rollup/plugin-commonjs';
5 | import json from '@rollup/plugin-json';
6 | import {readFileSync} from 'node:fs';
7 |
8 | // TODO: Replace this with a regular import when ESLint adds support for import assertions.
9 | // See: https://rollupjs.org/guide/en/#importing-packagejson
10 | const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url)));
11 |
12 | export const nodeResolve = resolve({
13 | browser: true,
14 | preferBuiltins: false
15 | });
16 |
17 | export default [{
18 | input: 'src/index.ts',
19 | output: [
20 | {
21 | file: pkg.minimized,
22 | name: "autocomplete",
23 | format: 'umd',
24 | sourcemap: true,
25 | freeze: false,
26 | esModule: false
27 | },
28 | {
29 | file: pkg.module,
30 | format: 'esm',
31 | sourcemap: true,
32 | freeze: false
33 | },
34 | {
35 | file: pkg.main,
36 | format: 'cjs',
37 | sourcemap: true,
38 | freeze: false
39 | },
40 | ],
41 | plugins: [
42 | json(),
43 | terser({
44 | compress: {
45 | // eslint-disable-next-line camelcase
46 | pure_getters: true,
47 | passes: 3
48 | }
49 | }),
50 | nodeResolve,
51 | typescript(),
52 | commonjs({
53 | ignoreGlobal: true
54 | })
55 | ]
56 | }]
--------------------------------------------------------------------------------
/src/autocomplete.ts:
--------------------------------------------------------------------------------
1 | import { CalculationHelper } from "./helpers/calculation.helper";
2 | import { DomHelper } from "./helpers/dom.helper";
3 | import {
4 | BY_CIRCLE,
5 | BY_COUNTRYCODE,
6 | BY_PLACE,
7 | BY_PROXIMITY,
8 | BY_RECT
9 | } from "./helpers/constants";
10 | import { Callbacks } from "./helpers/callbacks";
11 |
12 | export class GeocoderAutocomplete {
13 |
14 | private inputElement: HTMLInputElement;
15 | private inputClearButton: HTMLElement;
16 | private autocompleteItemsElement: HTMLElement = null;
17 |
18 | /* Focused item in the autocomplete list. This variable is used to navigate with buttons */
19 | private focusedItemIndex: number;
20 |
21 | /* Current autocomplete items data (GeoJSON.Feature) */
22 | private currentItems: any;
23 |
24 | /* Active request promise reject function. To be able to cancel the promise when a new request comes */
25 | private currentPromiseReject: any;
26 |
27 | /* Active place details request promise reject function */
28 | private currentPlaceDetailsPromiseReject: any;
29 |
30 | /* We set timeout before sending a request to avoid unnecessary calls */
31 | private currentTimeout: number;
32 |
33 | private callbacks = new Callbacks();
34 |
35 | private preprocessHook?: (value: string) => string;
36 | private postprocessHook?: (feature: any) => string;
37 | private suggestionsFilter?: (suggetions: any[]) => any[];
38 |
39 | private sendGeocoderRequestAlt?: (value: string, geocoderAutocomplete: GeocoderAutocomplete) => Promise;
40 | private sendPlaceDetailsRequestAlt?: (feature: any, geocoderAutocomplete: GeocoderAutocomplete) => Promise;
41 |
42 | private geocoderUrl = "https://api.geoapify.com/v1/geocode/autocomplete";
43 | private placeDetailsUrl = "https://api.geoapify.com/v2/place-details";
44 |
45 | private options: GeocoderAutocompleteOptions = {
46 | limit: 5,
47 | debounceDelay: 100
48 | };
49 |
50 | constructor(private container: HTMLElement, private apiKey: string, options?: GeocoderAutocompleteOptions) {
51 | this.constructOptions(options);
52 |
53 | this.inputElement = document.createElement("input");
54 | DomHelper.createInputElement(this.inputElement, this.options, this.container);
55 |
56 | this.addClearButton();
57 |
58 | this.addEventListeners();
59 | }
60 |
61 | public setGeocoderUrl(geocoderUrl: string) {
62 | this.geocoderUrl = geocoderUrl;
63 | }
64 |
65 | public setPlaceDetailsUrl(placeDetailsUrl: string) {
66 | this.placeDetailsUrl = placeDetailsUrl;
67 | }
68 |
69 | public setType(type: 'country' | 'state' | 'city' | 'postcode' | 'street' | 'amenity') {
70 | this.options.type = type;
71 | }
72 |
73 | public setLang(lang: SupportedLanguage) {
74 | this.options.lang = lang;
75 | }
76 |
77 | public setAddDetails(addDetails: boolean) {
78 | this.options.addDetails = addDetails;
79 | }
80 |
81 | public setSkipIcons(skipIcons: boolean) {
82 | this.options.skipIcons = skipIcons;
83 | }
84 |
85 | public setAllowNonVerifiedHouseNumber(allowNonVerifiedHouseNumber: boolean) {
86 | this.options.allowNonVerifiedHouseNumber = allowNonVerifiedHouseNumber;
87 | }
88 |
89 | public setAllowNonVerifiedStreet(allowNonVerifiedStreet: boolean) {
90 | this.options.allowNonVerifiedStreet = allowNonVerifiedStreet;
91 | }
92 |
93 | public setCountryCodes(codes: CountyCode[]) {
94 | console.warn("WARNING! Obsolete function called. Function setCountryCodes() has been deprecated, please use the new addFilterByCountry() function instead!");
95 | this.options.countryCodes = codes;
96 | }
97 |
98 | public setPosition(position: GeoPosition) {
99 | console.warn("WARNING! Obsolete function called. Function setPosition() has been deprecated, please use the new addBiasByProximity() function instead!");
100 | this.options.position = position;
101 | }
102 |
103 | public setLimit(limit: number) {
104 | this.options.limit = limit;
105 | }
106 |
107 | public setValue(value: string) {
108 | if (!value) {
109 | this.inputClearButton.classList.remove("visible");
110 | } else {
111 | this.inputClearButton.classList.add("visible");
112 | }
113 |
114 | this.inputElement.value = value;
115 | }
116 |
117 | public getValue() {
118 | return this.inputElement.value;
119 | }
120 |
121 | public addFilterByCountry(codes: ByCountryCodeOptions) {
122 | this.options.filter[BY_COUNTRYCODE] = codes;
123 | }
124 |
125 | public addFilterByCircle(filterByCircle: ByCircleOptions) {
126 | this.options.filter[BY_CIRCLE] = filterByCircle;
127 | }
128 |
129 | public addFilterByRect(filterByRect: ByRectOptions) {
130 | this.options.filter[BY_RECT] = filterByRect;
131 | }
132 |
133 | public addFilterByPlace(filterByPlace: string) {
134 | this.options.filter[BY_PLACE] = filterByPlace;
135 | }
136 |
137 | public clearFilters() {
138 | this.options.filter = {};
139 | }
140 |
141 | public addBiasByCountry(codes: ByCountryCodeOptions) {
142 | this.options.bias[BY_COUNTRYCODE] = codes;
143 | }
144 |
145 | public addBiasByCircle(biasByCircle: ByCircleOptions) {
146 | this.options.bias[BY_CIRCLE] = biasByCircle;
147 | }
148 |
149 | public addBiasByRect(biasByRect: ByRectOptions) {
150 | this.options.bias[BY_RECT] = biasByRect;
151 | }
152 |
153 | public addBiasByProximity(biasByProximity: ByProximityOptions) {
154 | this.options.bias[BY_PROXIMITY] = biasByProximity;
155 | }
156 |
157 | public clearBias() {
158 | this.options.bias = {};
159 | }
160 |
161 | public on(operation: 'select' | 'suggestions' | 'input' | 'close' | 'open', callback: (param: any) => void) {
162 | this.callbacks.addCallback(operation, callback);
163 | }
164 |
165 | public off(operation: 'select' | 'suggestions' | 'input' | 'close' | 'open', callback?: (param: any) => any) {
166 | this.callbacks.removeCallback(operation, callback);
167 | }
168 |
169 | public once(operation: 'select' | 'suggestions' | 'input' | 'close' | 'open', callback: (param: any) => any) {
170 | this.on(operation, callback);
171 |
172 | const current = this;
173 | const currentListener = () => {
174 | current.off(operation, callback);
175 | current.off(operation, currentListener);
176 | }
177 |
178 | this.on(operation, currentListener);
179 | }
180 |
181 | public setSuggestionsFilter(suggestionsFilterFunc?: (suggestions: any[]) => any[]) {
182 | this.suggestionsFilter = CalculationHelper.returnIfFunction(suggestionsFilterFunc);
183 | }
184 |
185 | public setPreprocessHook(preprocessHookFunc?: (value: string) => string) {
186 | this.preprocessHook = CalculationHelper.returnIfFunction(preprocessHookFunc);
187 | }
188 |
189 | public setPostprocessHook(postprocessHookFunc?: (value: string) => string) {
190 | this.postprocessHook = CalculationHelper.returnIfFunction(postprocessHookFunc);
191 | }
192 |
193 | public setSendGeocoderRequestFunc(sendGeocoderRequestFunc: (value: string, geocoderAutocomplete: GeocoderAutocomplete) => Promise) {
194 | this.sendGeocoderRequestAlt = CalculationHelper.returnIfFunction(sendGeocoderRequestFunc);
195 | }
196 |
197 | public setSendPlaceDetailsRequestFunc(sendPlaceDetailsRequestFunc: (feature: any, geocoderAutocomplete: GeocoderAutocomplete) => Promise) {
198 | this.sendPlaceDetailsRequestAlt = CalculationHelper.returnIfFunction(sendPlaceDetailsRequestFunc);
199 | }
200 |
201 | public isOpen(): boolean {
202 | return !!this.autocompleteItemsElement;
203 | }
204 |
205 | public close() {
206 | this.closeDropDownList();
207 | }
208 |
209 | public open() {
210 | if (!this.isOpen()) {
211 | this.openDropdownAgain();
212 | }
213 | }
214 |
215 | private sendGeocoderRequestOrAlt(currentValue: string): Promise {
216 | if (this.sendGeocoderRequestAlt) {
217 | return this.sendGeocoderRequestAlt(currentValue, this);
218 | } else {
219 | return this.sendGeocoderRequest(currentValue);
220 | }
221 |
222 | }
223 |
224 | public sendGeocoderRequest(value: string): Promise {
225 | return new Promise((resolve, reject) => {
226 | this.currentPromiseReject = reject;
227 |
228 | let url = CalculationHelper.generateUrl(value, this.geocoderUrl, this.apiKey, this.options);
229 |
230 | fetch(url)
231 | .then((response) => {
232 | if (response.ok) {
233 | response.json().then(data => resolve(data));
234 | } else {
235 | response.json().then(data => reject(data));
236 | }
237 | });
238 | });
239 | }
240 |
241 | public sendPlaceDetailsRequest(feature: any): Promise {
242 | return new Promise((resolve, reject) => {
243 |
244 | if (CalculationHelper.isNotOpenStreetMapData(feature)) {
245 | // only OSM data has detailed information; return the original object if the source is different from OSM
246 | resolve(feature);
247 | return;
248 | }
249 |
250 | this.currentPlaceDetailsPromiseReject = reject;
251 | let url = CalculationHelper.generatePlacesUrl(this.placeDetailsUrl, feature.properties.place_id, this.apiKey, this.options);
252 |
253 | fetch(url)
254 | .then((response) => {
255 | if (response.ok) {
256 | response.json().then(data => {
257 | if (!data.features.length) {
258 | resolve(feature);
259 | }
260 |
261 | resolve(data.features[0]);
262 | });
263 | } else {
264 | response.json().then(data => reject(data));
265 | }
266 | });
267 | });
268 | }
269 |
270 | /* Execute a function when someone writes in the text field: */
271 | onUserInput(event: Event) {
272 | let currentValue = this.inputElement.value;
273 | let userEnteredValue = this.inputElement.value;
274 |
275 | this.callbacks.notifyInputChange(currentValue);
276 |
277 | /* Close any already open dropdown list */
278 | this.closeDropDownList();
279 |
280 | this.focusedItemIndex = -1;
281 |
282 | this.cancelPreviousRequest();
283 |
284 | this.cancelPreviousTimeout();
285 |
286 | if (!currentValue) {
287 | this.removeClearButton();
288 | return false;
289 | }
290 |
291 | this.showClearButton();
292 |
293 | this.currentTimeout = window.setTimeout(() => {
294 | /* Create a new promise and send geocoding request */
295 | if (CalculationHelper.returnIfFunction(this.preprocessHook)) {
296 | currentValue = this.preprocessHook(currentValue);
297 | }
298 |
299 | let promise = this.sendGeocoderRequestOrAlt(currentValue);
300 |
301 | promise.then((data: any) => {
302 | this.onDropdownDataLoad(data, userEnteredValue, event);
303 | }, (err) => {
304 | if (!err.canceled) {
305 | console.log(err);
306 | }
307 | });
308 | }, this.options.debounceDelay);
309 | }
310 |
311 | private onDropdownDataLoad(data: any, userEnteredValue: string, event: Event) {
312 | if (CalculationHelper.needToCalculateExtendByNonVerifiedValues(data, this.options)) {
313 | CalculationHelper.extendByNonVerifiedValues(this.options, data.features, data?.query?.parsed);
314 | }
315 |
316 | this.currentItems = data.features;
317 |
318 | if (CalculationHelper.needToFilterDataBySuggestionsFilter(this.currentItems, this.suggestionsFilter)) {
319 | this.currentItems = this.suggestionsFilter(this.currentItems);
320 | }
321 |
322 | this.callbacks.notifySuggestions(this.currentItems);
323 |
324 | if (!this.currentItems.length) {
325 | return;
326 | }
327 |
328 | this.createDropdown();
329 |
330 | this.currentItems.forEach((feature: any, index: number) => {
331 | this.populateDropdownItem(feature, userEnteredValue, event, index);
332 | });
333 | }
334 |
335 | private populateDropdownItem(feature: any, userEnteredValue: string, event: Event, index: number) {
336 | /* Create a DIV element for each element: */
337 | const itemElement = DomHelper.createDropdownItem();
338 |
339 | if (!this.options.skipIcons) {
340 | DomHelper.addDropdownIcon(feature, itemElement);
341 | }
342 |
343 | const textElement = DomHelper.createDropdownItemText();
344 |
345 | if (CalculationHelper.returnIfFunction(this.postprocessHook)) {
346 | const value = this.postprocessHook(feature);
347 | textElement.innerHTML = DomHelper.getStyledAddressSingleValue(value, userEnteredValue);
348 | } else {
349 | textElement.innerHTML = DomHelper.getStyledAddress(feature.properties, userEnteredValue);
350 | }
351 |
352 | itemElement.appendChild(textElement);
353 | this.addEventListenerOnDropdownClick(itemElement, event, index);
354 | this.autocompleteItemsElement.appendChild(itemElement);
355 | }
356 |
357 | private addEventListenerOnDropdownClick(itemElement: HTMLDivElement, event: Event, index: number) {
358 | itemElement.addEventListener("click", (e) => {
359 | event.stopPropagation();
360 | this.setValueAndNotify(this.currentItems[index])
361 | });
362 | }
363 |
364 | private createDropdown() {
365 | /*create a DIV element that will contain the items (values):*/
366 | this.autocompleteItemsElement = document.createElement("div");
367 | this.autocompleteItemsElement.setAttribute("class", "geoapify-autocomplete-items");
368 |
369 | this.callbacks.notifyOpened();
370 |
371 | /* Append the DIV element as a child of the autocomplete container:*/
372 | this.container.appendChild(this.autocompleteItemsElement);
373 | }
374 |
375 | private cancelPreviousTimeout() {
376 | if (this.currentTimeout) {
377 | window.clearTimeout(this.currentTimeout);
378 | this.currentTimeout = null;
379 | }
380 | }
381 |
382 | private cancelPreviousRequest() {
383 | if (this.currentPromiseReject) {
384 | this.currentPromiseReject({
385 | canceled: true
386 | });
387 | this.currentPromiseReject = null;
388 | }
389 | }
390 |
391 | private addEventListeners() {
392 | this.inputElement.addEventListener('input', this.onUserInput.bind(this), false);
393 | this.inputElement.addEventListener('keydown', this.onUserKeyPress.bind(this), false);
394 |
395 | document.addEventListener("click", (event) => {
396 | if (event.target !== this.inputElement) {
397 | this.closeDropDownList();
398 | } else if (!this.autocompleteItemsElement) {
399 | // open dropdown list again
400 | this.openDropdownAgain();
401 | }
402 | });
403 | }
404 |
405 | private showClearButton() {
406 | this.inputClearButton.classList.add("visible");
407 | }
408 |
409 | private removeClearButton() {
410 | this.inputClearButton.classList.remove("visible");
411 | }
412 |
413 | private onUserKeyPress(event: KeyboardEvent) {
414 | if (this.autocompleteItemsElement) {
415 | const itemElements: HTMLCollectionOf = this.autocompleteItemsElement.getElementsByTagName("div");
416 | if (event.code === 'ArrowDown') {
417 | this.handleArrowDownEvent(event, itemElements);
418 | } else if (event.code === 'ArrowUp') {
419 | this.handleArrowUpEvent(event, itemElements);
420 | } else if (event.code === "Enter") {
421 | this.handleEnterEvent(event);
422 | } else if (event.code === "Escape") {
423 | /* If the ESC key is presses, close the list */
424 | this.closeDropDownList();
425 | }
426 | } else {
427 | if (event.code == 'ArrowDown') {
428 | /* Open dropdown list again */
429 | this.openDropdownAgain();
430 | }
431 | }
432 | }
433 |
434 | private handleEnterEvent(event: KeyboardEvent) {
435 | /* If the ENTER key is pressed and value as selected, close the list*/
436 | event.preventDefault();
437 | if (this.focusedItemIndex > -1) {
438 | if (this.options.skipSelectionOnArrowKey) {
439 | // select the location if it wasn't selected by navigation
440 | this.setValueAndNotify(this.currentItems[this.focusedItemIndex]);
441 | } else {
442 | this.closeDropDownList();
443 | }
444 | }
445 | }
446 |
447 | private handleArrowUpEvent(event: KeyboardEvent, itemElements: HTMLCollectionOf) {
448 | event.preventDefault();
449 |
450 | /*If the arrow UP key is pressed, decrease the focusedItemIndex variable:*/
451 | this.focusedItemIndex--;
452 | if (this.focusedItemIndex < 0) this.focusedItemIndex = (itemElements.length - 1);
453 | /*and and make the current item more visible:*/
454 | this.setActive(itemElements, this.focusedItemIndex);
455 | }
456 |
457 | private handleArrowDownEvent(event: KeyboardEvent, itemElements: HTMLCollectionOf) {
458 | event.preventDefault();
459 |
460 | /*If the arrow DOWN key is pressed, increase the focusedItemIndex variable:*/
461 | this.focusedItemIndex++;
462 | if (this.focusedItemIndex >= itemElements.length) this.focusedItemIndex = 0;
463 | /*and and make the current item more visible:*/
464 | this.setActive(itemElements, this.focusedItemIndex);
465 | }
466 |
467 | private setActive(items: HTMLCollectionOf, index: number) {
468 | if (!items || !items.length) return false;
469 | DomHelper.addActiveClassToDropdownItem(items, index);
470 |
471 | if (!this.options.skipSelectionOnArrowKey) {
472 | // Change input value and notify
473 | if (CalculationHelper.returnIfFunction(this.postprocessHook)) {
474 | this.inputElement.value = this.postprocessHook(this.currentItems[index]);
475 | } else {
476 | this.inputElement.value = this.currentItems[index].properties.formatted;
477 | }
478 | this.notifyValueSelected(this.currentItems[index]);
479 | }
480 | }
481 |
482 | private setValueAndNotify(feature: any) {
483 | if (CalculationHelper.returnIfFunction(this.postprocessHook)) {
484 | this.inputElement.value = this.postprocessHook(feature);
485 | } else {
486 | this.inputElement.value = feature.properties.formatted;
487 | }
488 |
489 | this.notifyValueSelected(feature);
490 |
491 | /* Close the list of autocompleted values: */
492 | this.closeDropDownList();
493 | }
494 |
495 | private clearFieldAndNotify(event: MouseEvent) {
496 | event.stopPropagation();
497 | this.inputElement.value = '';
498 | this.removeClearButton();
499 |
500 | this.cancelPreviousRequest();
501 |
502 | this.cancelPreviousTimeout();
503 |
504 | this.closeDropDownList();
505 |
506 | // notify here
507 | this.notifyValueSelected(null);
508 | }
509 |
510 | private closeDropDownList() {
511 | if (this.autocompleteItemsElement) {
512 | this.container.removeChild(this.autocompleteItemsElement);
513 | this.autocompleteItemsElement = null;
514 | this.callbacks.notifyClosed();
515 | }
516 | }
517 |
518 | private notifyValueSelected(feature: any) {
519 | this.cancelPreviousPlaceDetailsRequest();
520 |
521 | if (this.noNeedToRequestPlaceDetails(feature)) {
522 | this.callbacks.notifyChange(feature);
523 | } else {
524 | let promise = this.sendPlaceDetailsRequestOrAlt(feature);
525 |
526 | promise.then((detailesFeature: any) => {
527 | this.callbacks.notifyChange(detailesFeature);
528 | this.currentPlaceDetailsPromiseReject = null;
529 | }, (err) => {
530 | if (!err.canceled) {
531 | console.log(err);
532 | this.callbacks.notifyChange(feature);
533 | this.currentPlaceDetailsPromiseReject = null;
534 | }
535 | });
536 | }
537 | }
538 |
539 | private sendPlaceDetailsRequestOrAlt(feature: any) {
540 | if (this.sendPlaceDetailsRequestAlt) {
541 | return this.sendPlaceDetailsRequestAlt(feature, this)
542 | } else {
543 | return this.sendPlaceDetailsRequest(feature);
544 | }
545 | }
546 |
547 | private noNeedToRequestPlaceDetails(feature: any) {
548 | return !this.options.addDetails || !feature || feature.properties.nonVerifiedParts?.length;
549 | }
550 |
551 | private cancelPreviousPlaceDetailsRequest() {
552 | if (this.currentPlaceDetailsPromiseReject) {
553 | this.currentPlaceDetailsPromiseReject({
554 | canceled: true
555 | });
556 | this.currentPlaceDetailsPromiseReject = null;
557 | }
558 | }
559 |
560 | private openDropdownAgain() {
561 | const event = document.createEvent('Event');
562 | event.initEvent('input', true, true);
563 | this.inputElement.dispatchEvent(event);
564 | }
565 |
566 | private constructOptions(options: GeocoderAutocompleteOptions) {
567 | this.options = options ? {...this.options, ...options} : this.options;
568 | this.options.filter = this.options.filter || {};
569 | this.options.bias = this.options.bias || {};
570 |
571 | if (this.options.countryCodes) {
572 | this.addFilterByCountry(this.options.countryCodes);
573 | }
574 |
575 | if (this.options.position) {
576 | this.addBiasByProximity(this.options.position);
577 | }
578 | }
579 |
580 | private addClearButton() {
581 | this.inputClearButton = document.createElement("div");
582 | this.inputClearButton.classList.add("geoapify-close-button");
583 | DomHelper.addIcon(this.inputClearButton, 'close');
584 | this.inputClearButton.addEventListener("click", this.clearFieldAndNotify.bind(this), false);
585 | this.container.appendChild(this.inputClearButton);
586 | }
587 | }
588 |
589 | export interface GeocoderAutocompleteOptions {
590 | type?: LocationType;
591 | lang?: SupportedLanguage;
592 | limit?: number;
593 | placeholder?: string;
594 | debounceDelay?: number;
595 |
596 | filter?: { [key: string]: ByCircleOptions | ByCountryCodeOptions | ByRectOptions | string },
597 | bias?: { [key: string]: ByCircleOptions | ByCountryCodeOptions | ByRectOptions | ByProximityOptions },
598 |
599 | skipIcons?: boolean;
600 |
601 | /**
602 | * @deprecated The property should not be used; it is true by default. Use the addDetails property to add details.
603 | */
604 | skipDetails?: boolean;
605 |
606 | addDetails?: boolean;
607 |
608 | skipSelectionOnArrowKey?: boolean;
609 |
610 | // to remove in the next version
611 | position?: GeoPosition;
612 | countryCodes?: CountyCode[];
613 |
614 | // extend results with non verified values if needed
615 | allowNonVerifiedHouseNumber?: boolean;
616 | allowNonVerifiedStreet?: boolean;
617 | }
618 |
619 | export interface GeoPosition {
620 | lat: number;
621 | lon: number;
622 | }
623 |
624 | export type ByCountryCodeOptions = CountyCode[];
625 |
626 | export interface ByProximityOptions {
627 | lon: number;
628 | lat: number;
629 | }
630 |
631 | export interface ByCircleOptions {
632 | lon: number;
633 | lat: number;
634 | radiusMeters: number;
635 | }
636 |
637 | export interface ByRectOptions {
638 | lon1: number;
639 | lat1: number;
640 | lon2: number;
641 | lat2: number;
642 | }
643 |
644 | export type LocationType = 'country' | 'state' | 'city' | 'postcode' | 'street' | 'amenity';
645 | export type SupportedLanguage = "ab" | "aa" | "af" | "ak" | "sq" | "am" | "ar" | "an" | "hy" | "as" | "av" | "ae" | "ay" | "az" | "bm" | "ba" | "eu" | "be" | "bn" | "bh" | "bi" | "bs" | "br" | "bg" | "my" | "ca" | "ch" | "ce" | "ny" | "zh" | "cv" | "kw" | "co" | "cr" | "hr" | "cs" | "da" | "dv" | "nl" | "en" | "eo" | "et" | "ee" | "fo" | "fj" | "fi" | "fr" | "ff" | "gl" | "ka" | "de" | "el" | "gn" | "gu" | "ht" | "ha" | "he" | "hz" | "hi" | "ho" | "hu" | "ia" | "id" | "ie" | "ga" | "ig" | "ik" | "io" | "is" | "it" | "iu" | "ja" | "jv" | "kl" | "kn" | "kr" | "ks" | "kk" | "km" | "ki" | "rw" | "ky" | "kv" | "kg" | "ko" | "ku" | "kj" | "la" | "lb" | "lg" | "li" | "ln" | "lo" | "lt" | "lu" | "lv" | "gv" | "mk" | "mg" | "ms" | "ml" | "mt" | "mi" | "mr" | "mh" | "mn" | "na" | "nv" | "nb" | "nd" | "ne" | "ng" | "nn" | "no" | "ii" | "nr" | "oc" | "oj" | "cu" | "om" | "or" | "os" | "pa" | "pi" | "fa" | "pl" | "ps" | "pt" | "qu" | "rm" | "rn" | "ro" | "ru" | "sa" | "sc" | "sd" | "se" | "sm" | "sg" | "sr" | "gd" | "sn" | "si" | "sk" | "sl" | "so" | "st" | "es" | "su" | "sw" | "ss" | "sv" | "ta" | "te" | "tg" | "th" | "ti" | "bo" | "tk" | "tl" | "tn" | "to" | "tr" | "ts" | "tt" | "tw" | "ty" | "ug" | "uk" | "ur" | "uz" | "ve" | "vi" | "vo" | "wa" | "cy" | "wo" | "fy" | "xh" | "yi" | "yo" | "za";
646 | export type CountyCode = "none" | "auto" | "ad" | "ae" | "af" | "ag" | "ai" | "al" | "am" | "an" | "ao" | "ap" | "aq" | "ar" | "as" | "at" | "au" | "aw" | "az" | "ba" | "bb" | "bd" | "be" | "bf" | "bg" | "bh" | "bi" | "bj" | "bm" | "bn" | "bo" | "br" | "bs" | "bt" | "bv" | "bw" | "by" | "bz" | "ca" | "cc" | "cd" | "cf" | "cg" | "ch" | "ci" | "ck" | "cl" | "cm" | "cn" | "co" | "cr" | "cu" | "cv" | "cx" | "cy" | "cz" | "de" | "dj" | "dk" | "dm" | "do" | "dz" | "ec" | "ee" | "eg" | "eh" | "er" | "es" | "et" | "eu" | "fi" | "fj" | "fk" | "fm" | "fo" | "fr" | "ga" | "gb" | "gd" | "ge" | "gf" | "gh" | "gi" | "gl" | "gm" | "gn" | "gp" | "gq" | "gr" | "gs" | "gt" | "gu" | "gw" | "gy" | "hk" | "hm" | "hn" | "hr" | "ht" | "hu" | "id" | "ie" | "il" | "in" | "io" | "iq" | "ir" | "is" | "it" | "jm" | "jo" | "jp" | "ke" | "kg" | "kh" | "ki" | "km" | "kn" | "kp" | "kr" | "kw" | "ky" | "kz" | "la" | "lb" | "lc" | "li" | "lk" | "lr" | "ls" | "lt" | "lu" | "lv" | "ly" | "ma" | "mc" | "md" | "me" | "mg" | "mh" | "mk" | "ml" | "mm" | "mn" | "mo" | "mp" | "mq" | "mr" | "ms" | "mt" | "mu" | "mv" | "mw" | "mx" | "my" | "mz" | "na" | "nc" | "ne" | "nf" | "ng" | "ni" | "nl" | "no" | "np" | "nr" | "nu" | "nz" | "om" | "pa" | "pe" | "pf" | "pg" | "ph" | "pk" | "pl" | "pm" | "pr" | "ps" | "pt" | "pw" | "py" | "qa" | "re" | "ro" | "rs" | "ru" | "rw" | "sa" | "sb" | "sc" | "sd" | "se" | "sg" | "sh" | "si" | "sj" | "sk" | "sl" | "sm" | "sn" | "so" | "sr" | "st" | "sv" | "sy" | "sz" | "tc" | "td" | "tf" | "tg" | "th" | "tj" | "tk" | "tm" | "tn" | "to" | "tr" | "tt" | "tv" | "tw" | "tz" | "ua" | "ug" | "um" | "us" | "uy" | "uz" | "va" | "vc" | "ve" | "vg" | "vi" | "vn" | "vu" | "wf" | "ws" | "ye" | "yt" | "za" | "zm" | "zw";
647 |
--------------------------------------------------------------------------------
/src/countries.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "code": "AD",
4 | "emoji": "🇦🇩",
5 | "unicode": "U+1F1E6 U+1F1E9",
6 | "name": "Andorra",
7 | "title": "flag for Andorra",
8 | "dialCode": "+376"
9 | },
10 | {
11 | "code": "AE",
12 | "emoji": "🇦🇪",
13 | "unicode": "U+1F1E6 U+1F1EA",
14 | "name": "United Arab Emirates",
15 | "title": "flag for United Arab Emirates",
16 | "dialCode": "+971"
17 | },
18 | {
19 | "code": "AF",
20 | "emoji": "🇦🇫",
21 | "unicode": "U+1F1E6 U+1F1EB",
22 | "name": "Afghanistan",
23 | "title": "flag for Afghanistan",
24 | "dialCode": "+93"
25 | },
26 | {
27 | "code": "AG",
28 | "emoji": "🇦🇬",
29 | "unicode": "U+1F1E6 U+1F1EC",
30 | "name": "Antigua and Barbuda",
31 | "title": "flag for Antigua and Barbuda",
32 | "dialCode": "+1268"
33 | },
34 | {
35 | "code": "AI",
36 | "emoji": "🇦🇮",
37 | "unicode": "U+1F1E6 U+1F1EE",
38 | "name": "Anguilla",
39 | "title": "flag for Anguilla",
40 | "dialCode": "+1 264"
41 | },
42 | {
43 | "code": "AL",
44 | "emoji": "🇦🇱",
45 | "unicode": "U+1F1E6 U+1F1F1",
46 | "name": "Albania",
47 | "title": "flag for Albania",
48 | "dialCode": "+355"
49 | },
50 | {
51 | "code": "AM",
52 | "emoji": "🇦🇲",
53 | "unicode": "U+1F1E6 U+1F1F2",
54 | "name": "Armenia",
55 | "title": "flag for Armenia",
56 | "dialCode": "+374"
57 | },
58 | {
59 | "code": "AO",
60 | "emoji": "🇦🇴",
61 | "unicode": "U+1F1E6 U+1F1F4",
62 | "name": "Angola",
63 | "title": "flag for Angola",
64 | "dialCode": "+244"
65 | },
66 | {
67 | "code": "AQ",
68 | "emoji": "🇦🇶",
69 | "unicode": "U+1F1E6 U+1F1F6",
70 | "name": "Antarctica",
71 | "title": "flag for Antarctica",
72 | "dialCode": null
73 | },
74 | {
75 | "code": "AR",
76 | "emoji": "🇦🇷",
77 | "unicode": "U+1F1E6 U+1F1F7",
78 | "name": "Argentina",
79 | "title": "flag for Argentina",
80 | "dialCode": "+54"
81 | },
82 | {
83 | "code": "AS",
84 | "emoji": "🇦🇸",
85 | "unicode": "U+1F1E6 U+1F1F8",
86 | "name": "American Samoa",
87 | "title": "flag for American Samoa",
88 | "dialCode": "+1 684"
89 | },
90 | {
91 | "code": "AT",
92 | "emoji": "🇦🇹",
93 | "unicode": "U+1F1E6 U+1F1F9",
94 | "name": "Austria",
95 | "title": "flag for Austria",
96 | "dialCode": "+43"
97 | },
98 | {
99 | "code": "AU",
100 | "emoji": "🇦🇺",
101 | "unicode": "U+1F1E6 U+1F1FA",
102 | "name": "Australia",
103 | "title": "flag for Australia",
104 | "dialCode": "+61"
105 | },
106 | {
107 | "code": "AW",
108 | "emoji": "🇦🇼",
109 | "unicode": "U+1F1E6 U+1F1FC",
110 | "name": "Aruba",
111 | "title": "flag for Aruba",
112 | "dialCode": "+297"
113 | },
114 | {
115 | "code": "AX",
116 | "emoji": "🇦🇽",
117 | "unicode": "U+1F1E6 U+1F1FD",
118 | "name": "Åland Islands",
119 | "title": "flag for Åland Islands",
120 | "dialCode": ""
121 | },
122 | {
123 | "code": "AZ",
124 | "emoji": "🇦🇿",
125 | "unicode": "U+1F1E6 U+1F1FF",
126 | "name": "Azerbaijan",
127 | "title": "flag for Azerbaijan",
128 | "dialCode": "+994"
129 | },
130 | {
131 | "code": "BA",
132 | "emoji": "🇧🇦",
133 | "unicode": "U+1F1E7 U+1F1E6",
134 | "name": "Bosnia and Herzegovina",
135 | "title": "flag for Bosnia and Herzegovina",
136 | "dialCode": "+387"
137 | },
138 | {
139 | "code": "BB",
140 | "emoji": "🇧🇧",
141 | "unicode": "U+1F1E7 U+1F1E7",
142 | "name": "Barbados",
143 | "title": "flag for Barbados",
144 | "dialCode": "+1 246"
145 | },
146 | {
147 | "code": "BD",
148 | "emoji": "🇧🇩",
149 | "unicode": "U+1F1E7 U+1F1E9",
150 | "name": "Bangladesh",
151 | "title": "flag for Bangladesh",
152 | "dialCode": "+880"
153 | },
154 | {
155 | "code": "BE",
156 | "emoji": "🇧🇪",
157 | "unicode": "U+1F1E7 U+1F1EA",
158 | "name": "Belgium",
159 | "title": "flag for Belgium",
160 | "dialCode": "+32"
161 | },
162 | {
163 | "code": "BF",
164 | "emoji": "🇧🇫",
165 | "unicode": "U+1F1E7 U+1F1EB",
166 | "name": "Burkina Faso",
167 | "title": "flag for Burkina Faso",
168 | "dialCode": "+226"
169 | },
170 | {
171 | "code": "BG",
172 | "emoji": "🇧🇬",
173 | "unicode": "U+1F1E7 U+1F1EC",
174 | "name": "Bulgaria",
175 | "title": "flag for Bulgaria",
176 | "dialCode": "+359"
177 | },
178 | {
179 | "code": "BH",
180 | "emoji": "🇧🇭",
181 | "unicode": "U+1F1E7 U+1F1ED",
182 | "name": "Bahrain",
183 | "title": "flag for Bahrain",
184 | "dialCode": "+973"
185 | },
186 | {
187 | "code": "BI",
188 | "emoji": "🇧🇮",
189 | "unicode": "U+1F1E7 U+1F1EE",
190 | "name": "Burundi",
191 | "title": "flag for Burundi",
192 | "dialCode": "+257"
193 | },
194 | {
195 | "code": "BJ",
196 | "emoji": "🇧🇯",
197 | "unicode": "U+1F1E7 U+1F1EF",
198 | "name": "Benin",
199 | "title": "flag for Benin",
200 | "dialCode": "+229"
201 | },
202 | {
203 | "code": "BL",
204 | "emoji": "🇧🇱",
205 | "unicode": "U+1F1E7 U+1F1F1",
206 | "name": "Saint Barthélemy",
207 | "title": "flag for Saint Barthélemy",
208 | "dialCode": "+590"
209 | },
210 | {
211 | "code": "BM",
212 | "emoji": "🇧🇲",
213 | "unicode": "U+1F1E7 U+1F1F2",
214 | "name": "Bermuda",
215 | "title": "flag for Bermuda",
216 | "dialCode": "+1 441"
217 | },
218 | {
219 | "code": "BN",
220 | "emoji": "🇧🇳",
221 | "unicode": "U+1F1E7 U+1F1F3",
222 | "name": "Brunei Darussalam",
223 | "title": "flag for Brunei Darussalam",
224 | "dialCode": "+673"
225 | },
226 | {
227 | "code": "BO",
228 | "emoji": "🇧🇴",
229 | "unicode": "U+1F1E7 U+1F1F4",
230 | "name": "Bolivia",
231 | "title": "flag for Bolivia",
232 | "dialCode": "+591"
233 | },
234 | {
235 | "code": "BQ",
236 | "emoji": "🇧🇶",
237 | "unicode": "U+1F1E7 U+1F1F6",
238 | "name": "Bonaire, Sint Eustatius and Saba",
239 | "title": "flag for Bonaire, Sint Eustatius and Saba"
240 | },
241 | {
242 | "code": "BR",
243 | "emoji": "🇧🇷",
244 | "unicode": "U+1F1E7 U+1F1F7",
245 | "name": "Brazil",
246 | "title": "flag for Brazil",
247 | "dialCode": "+55"
248 | },
249 | {
250 | "code": "BS",
251 | "emoji": "🇧🇸",
252 | "unicode": "U+1F1E7 U+1F1F8",
253 | "name": "Bahamas",
254 | "title": "flag for Bahamas",
255 | "dialCode": "+1 242"
256 | },
257 | {
258 | "code": "BT",
259 | "emoji": "🇧🇹",
260 | "unicode": "U+1F1E7 U+1F1F9",
261 | "name": "Bhutan",
262 | "title": "flag for Bhutan",
263 | "dialCode": "+975"
264 | },
265 | {
266 | "code": "BV",
267 | "emoji": "🇧🇻",
268 | "unicode": "U+1F1E7 U+1F1FB",
269 | "name": "Bouvet Island",
270 | "title": "flag for Bouvet Island"
271 | },
272 | {
273 | "code": "BW",
274 | "emoji": "🇧🇼",
275 | "unicode": "U+1F1E7 U+1F1FC",
276 | "name": "Botswana",
277 | "title": "flag for Botswana",
278 | "dialCode": "+267"
279 | },
280 | {
281 | "code": "BY",
282 | "emoji": "🇧🇾",
283 | "unicode": "U+1F1E7 U+1F1FE",
284 | "name": "Belarus",
285 | "title": "flag for Belarus",
286 | "dialCode": "+375"
287 | },
288 | {
289 | "code": "BZ",
290 | "emoji": "🇧🇿",
291 | "unicode": "U+1F1E7 U+1F1FF",
292 | "name": "Belize",
293 | "title": "flag for Belize",
294 | "dialCode": "+501"
295 | },
296 | {
297 | "code": "CA",
298 | "emoji": "🇨🇦",
299 | "unicode": "U+1F1E8 U+1F1E6",
300 | "name": "Canada",
301 | "title": "flag for Canada",
302 | "dialCode": "+1"
303 | },
304 | {
305 | "code": "CC",
306 | "emoji": "🇨🇨",
307 | "unicode": "U+1F1E8 U+1F1E8",
308 | "name": "Cocos (Keeling) Islands",
309 | "title": "flag for Cocos (Keeling) Islands",
310 | "dialCode": "+61"
311 | },
312 | {
313 | "code": "CD",
314 | "emoji": "🇨🇩",
315 | "unicode": "U+1F1E8 U+1F1E9",
316 | "name": "Congo",
317 | "title": "flag for Congo",
318 | "dialCode": "+243"
319 | },
320 | {
321 | "code": "CF",
322 | "emoji": "🇨🇫",
323 | "unicode": "U+1F1E8 U+1F1EB",
324 | "name": "Central African Republic",
325 | "title": "flag for Central African Republic",
326 | "dialCode": "+236"
327 | },
328 | {
329 | "code": "CG",
330 | "emoji": "🇨🇬",
331 | "unicode": "U+1F1E8 U+1F1EC",
332 | "name": "Congo",
333 | "title": "flag for Congo",
334 | "dialCode": "+242"
335 | },
336 | {
337 | "code": "CH",
338 | "emoji": "🇨🇭",
339 | "unicode": "U+1F1E8 U+1F1ED",
340 | "name": "Switzerland",
341 | "title": "flag for Switzerland",
342 | "dialCode": "+41"
343 | },
344 | {
345 | "code": "CI",
346 | "emoji": "🇨🇮",
347 | "unicode": "U+1F1E8 U+1F1EE",
348 | "name": "Côte D'Ivoire",
349 | "title": "flag for Côte D'Ivoire",
350 | "dialCode": "+225"
351 | },
352 | {
353 | "code": "CK",
354 | "emoji": "🇨🇰",
355 | "unicode": "U+1F1E8 U+1F1F0",
356 | "name": "Cook Islands",
357 | "title": "flag for Cook Islands",
358 | "dialCode": "+682"
359 | },
360 | {
361 | "code": "CL",
362 | "emoji": "🇨🇱",
363 | "unicode": "U+1F1E8 U+1F1F1",
364 | "name": "Chile",
365 | "title": "flag for Chile",
366 | "dialCode": "+56"
367 | },
368 | {
369 | "code": "CM",
370 | "emoji": "🇨🇲",
371 | "unicode": "U+1F1E8 U+1F1F2",
372 | "name": "Cameroon",
373 | "title": "flag for Cameroon",
374 | "dialCode": "+237"
375 | },
376 | {
377 | "code": "CN",
378 | "emoji": "🇨🇳",
379 | "unicode": "U+1F1E8 U+1F1F3",
380 | "name": "China",
381 | "title": "flag for China",
382 | "dialCode": "+86"
383 | },
384 | {
385 | "code": "CO",
386 | "emoji": "🇨🇴",
387 | "unicode": "U+1F1E8 U+1F1F4",
388 | "name": "Colombia",
389 | "title": "flag for Colombia",
390 | "dialCode": "+57"
391 | },
392 | {
393 | "code": "CR",
394 | "emoji": "🇨🇷",
395 | "unicode": "U+1F1E8 U+1F1F7",
396 | "name": "Costa Rica",
397 | "title": "flag for Costa Rica",
398 | "dialCode": "+506"
399 | },
400 | {
401 | "code": "CU",
402 | "emoji": "🇨🇺",
403 | "unicode": "U+1F1E8 U+1F1FA",
404 | "name": "Cuba",
405 | "title": "flag for Cuba",
406 | "dialCode": "+53"
407 | },
408 | {
409 | "code": "CV",
410 | "emoji": "🇨🇻",
411 | "unicode": "U+1F1E8 U+1F1FB",
412 | "name": "Cape Verde",
413 | "title": "flag for Cape Verde",
414 | "dialCode": "+238"
415 | },
416 | {
417 | "code": "CW",
418 | "emoji": "🇨🇼",
419 | "unicode": "U+1F1E8 U+1F1FC",
420 | "name": "Curaçao",
421 | "title": "flag for Curaçao"
422 | },
423 | {
424 | "code": "CX",
425 | "emoji": "🇨🇽",
426 | "unicode": "U+1F1E8 U+1F1FD",
427 | "name": "Christmas Island",
428 | "title": "flag for Christmas Island",
429 | "dialCode": "+61"
430 | },
431 | {
432 | "code": "CY",
433 | "emoji": "🇨🇾",
434 | "unicode": "U+1F1E8 U+1F1FE",
435 | "name": "Cyprus",
436 | "title": "flag for Cyprus",
437 | "dialCode": "+537"
438 | },
439 | {
440 | "code": "CZ",
441 | "emoji": "🇨🇿",
442 | "unicode": "U+1F1E8 U+1F1FF",
443 | "name": "Czech Republic",
444 | "title": "flag for Czech Republic",
445 | "dialCode": "+420"
446 | },
447 | {
448 | "code": "DE",
449 | "emoji": "🇩🇪",
450 | "unicode": "U+1F1E9 U+1F1EA",
451 | "name": "Germany",
452 | "title": "flag for Germany",
453 | "dialCode": "+49"
454 | },
455 | {
456 | "code": "DJ",
457 | "emoji": "🇩🇯",
458 | "unicode": "U+1F1E9 U+1F1EF",
459 | "name": "Djibouti",
460 | "title": "flag for Djibouti",
461 | "dialCode": "+253"
462 | },
463 | {
464 | "code": "DK",
465 | "emoji": "🇩🇰",
466 | "unicode": "U+1F1E9 U+1F1F0",
467 | "name": "Denmark",
468 | "title": "flag for Denmark",
469 | "dialCode": "+45"
470 | },
471 | {
472 | "code": "DM",
473 | "emoji": "🇩🇲",
474 | "unicode": "U+1F1E9 U+1F1F2",
475 | "name": "Dominica",
476 | "title": "flag for Dominica",
477 | "dialCode": "+1 767"
478 | },
479 | {
480 | "code": "DO",
481 | "emoji": "🇩🇴",
482 | "unicode": "U+1F1E9 U+1F1F4",
483 | "name": "Dominican Republic",
484 | "title": "flag for Dominican Republic",
485 | "dialCode": "+1 849"
486 | },
487 | {
488 | "code": "DZ",
489 | "emoji": "🇩🇿",
490 | "unicode": "U+1F1E9 U+1F1FF",
491 | "name": "Algeria",
492 | "title": "flag for Algeria",
493 | "dialCode": "+213"
494 | },
495 | {
496 | "code": "EC",
497 | "emoji": "🇪🇨",
498 | "unicode": "U+1F1EA U+1F1E8",
499 | "name": "Ecuador",
500 | "title": "flag for Ecuador",
501 | "dialCode": "+593"
502 | },
503 | {
504 | "code": "EE",
505 | "emoji": "🇪🇪",
506 | "unicode": "U+1F1EA U+1F1EA",
507 | "name": "Estonia",
508 | "title": "flag for Estonia",
509 | "dialCode": "+372"
510 | },
511 | {
512 | "code": "EG",
513 | "emoji": "🇪🇬",
514 | "unicode": "U+1F1EA U+1F1EC",
515 | "name": "Egypt",
516 | "title": "flag for Egypt",
517 | "dialCode": "+20"
518 | },
519 | {
520 | "code": "EH",
521 | "emoji": "🇪🇭",
522 | "unicode": "U+1F1EA U+1F1ED",
523 | "name": "Western Sahara",
524 | "title": "flag for Western Sahara"
525 | },
526 | {
527 | "code": "ER",
528 | "emoji": "🇪🇷",
529 | "unicode": "U+1F1EA U+1F1F7",
530 | "name": "Eritrea",
531 | "title": "flag for Eritrea",
532 | "dialCode": "+291"
533 | },
534 | {
535 | "code": "ES",
536 | "emoji": "🇪🇸",
537 | "unicode": "U+1F1EA U+1F1F8",
538 | "name": "Spain",
539 | "title": "flag for Spain",
540 | "dialCode": "+34"
541 | },
542 | {
543 | "code": "ET",
544 | "emoji": "🇪🇹",
545 | "unicode": "U+1F1EA U+1F1F9",
546 | "name": "Ethiopia",
547 | "title": "flag for Ethiopia",
548 | "dialCode": "+251"
549 | },
550 | {
551 | "code": "EU",
552 | "emoji": "🇪🇺",
553 | "unicode": "U+1F1EA U+1F1FA",
554 | "name": "European Union",
555 | "title": "flag for European Union"
556 | },
557 | {
558 | "code": "FI",
559 | "emoji": "🇫🇮",
560 | "unicode": "U+1F1EB U+1F1EE",
561 | "name": "Finland",
562 | "title": "flag for Finland",
563 | "dialCode": "+358"
564 | },
565 | {
566 | "code": "FJ",
567 | "emoji": "🇫🇯",
568 | "unicode": "U+1F1EB U+1F1EF",
569 | "name": "Fiji",
570 | "title": "flag for Fiji",
571 | "dialCode": "+679"
572 | },
573 | {
574 | "code": "FK",
575 | "emoji": "🇫🇰",
576 | "unicode": "U+1F1EB U+1F1F0",
577 | "name": "Falkland Islands (Malvinas)",
578 | "title": "flag for Falkland Islands (Malvinas)",
579 | "dialCode": "+500"
580 | },
581 | {
582 | "code": "FM",
583 | "emoji": "🇫🇲",
584 | "unicode": "U+1F1EB U+1F1F2",
585 | "name": "Micronesia",
586 | "title": "flag for Micronesia",
587 | "dialCode": "+691"
588 | },
589 | {
590 | "code": "FO",
591 | "emoji": "🇫🇴",
592 | "unicode": "U+1F1EB U+1F1F4",
593 | "name": "Faroe Islands",
594 | "title": "flag for Faroe Islands",
595 | "dialCode": "+298"
596 | },
597 | {
598 | "code": "FR",
599 | "emoji": "🇫🇷",
600 | "unicode": "U+1F1EB U+1F1F7",
601 | "name": "France",
602 | "title": "flag for France",
603 | "dialCode": "+33"
604 | },
605 | {
606 | "code": "GA",
607 | "emoji": "🇬🇦",
608 | "unicode": "U+1F1EC U+1F1E6",
609 | "name": "Gabon",
610 | "title": "flag for Gabon",
611 | "dialCode": "+241"
612 | },
613 | {
614 | "code": "GB",
615 | "emoji": "🇬🇧",
616 | "unicode": "U+1F1EC U+1F1E7",
617 | "name": "United Kingdom",
618 | "title": "flag for United Kingdom",
619 | "dialCode": "+44"
620 | },
621 | {
622 | "code": "GD",
623 | "emoji": "🇬🇩",
624 | "unicode": "U+1F1EC U+1F1E9",
625 | "name": "Grenada",
626 | "title": "flag for Grenada",
627 | "dialCode": "+1 473"
628 | },
629 | {
630 | "code": "GE",
631 | "emoji": "🇬🇪",
632 | "unicode": "U+1F1EC U+1F1EA",
633 | "name": "Georgia",
634 | "title": "flag for Georgia",
635 | "dialCode": "+995"
636 | },
637 | {
638 | "code": "GF",
639 | "emoji": "🇬🇫",
640 | "unicode": "U+1F1EC U+1F1EB",
641 | "name": "French Guiana",
642 | "title": "flag for French Guiana",
643 | "dialCode": "+594"
644 | },
645 | {
646 | "code": "GG",
647 | "emoji": "🇬🇬",
648 | "unicode": "U+1F1EC U+1F1EC",
649 | "name": "Guernsey",
650 | "title": "flag for Guernsey",
651 | "dialCode": "+44"
652 | },
653 | {
654 | "code": "GH",
655 | "emoji": "🇬🇭",
656 | "unicode": "U+1F1EC U+1F1ED",
657 | "name": "Ghana",
658 | "title": "flag for Ghana",
659 | "dialCode": "+233"
660 | },
661 | {
662 | "code": "GI",
663 | "emoji": "🇬🇮",
664 | "unicode": "U+1F1EC U+1F1EE",
665 | "name": "Gibraltar",
666 | "title": "flag for Gibraltar",
667 | "dialCode": "+350"
668 | },
669 | {
670 | "code": "GL",
671 | "emoji": "🇬🇱",
672 | "unicode": "U+1F1EC U+1F1F1",
673 | "name": "Greenland",
674 | "title": "flag for Greenland",
675 | "dialCode": "+299"
676 | },
677 | {
678 | "code": "GM",
679 | "emoji": "🇬🇲",
680 | "unicode": "U+1F1EC U+1F1F2",
681 | "name": "Gambia",
682 | "title": "flag for Gambia",
683 | "dialCode": "+220"
684 | },
685 | {
686 | "code": "GN",
687 | "emoji": "🇬🇳",
688 | "unicode": "U+1F1EC U+1F1F3",
689 | "name": "Guinea",
690 | "title": "flag for Guinea",
691 | "dialCode": "+224"
692 | },
693 | {
694 | "code": "GP",
695 | "emoji": "🇬🇵",
696 | "unicode": "U+1F1EC U+1F1F5",
697 | "name": "Guadeloupe",
698 | "title": "flag for Guadeloupe",
699 | "dialCode": "+590"
700 | },
701 | {
702 | "code": "GQ",
703 | "emoji": "🇬🇶",
704 | "unicode": "U+1F1EC U+1F1F6",
705 | "name": "Equatorial Guinea",
706 | "title": "flag for Equatorial Guinea",
707 | "dialCode": "+240"
708 | },
709 | {
710 | "code": "GR",
711 | "emoji": "🇬🇷",
712 | "unicode": "U+1F1EC U+1F1F7",
713 | "name": "Greece",
714 | "title": "flag for Greece",
715 | "dialCode": "+30"
716 | },
717 | {
718 | "code": "GS",
719 | "emoji": "🇬🇸",
720 | "unicode": "U+1F1EC U+1F1F8",
721 | "name": "South Georgia",
722 | "title": "flag for South Georgia",
723 | "dialCode": "+500"
724 | },
725 | {
726 | "code": "GT",
727 | "emoji": "🇬🇹",
728 | "unicode": "U+1F1EC U+1F1F9",
729 | "name": "Guatemala",
730 | "title": "flag for Guatemala",
731 | "dialCode": "+502"
732 | },
733 | {
734 | "code": "GU",
735 | "emoji": "🇬🇺",
736 | "unicode": "U+1F1EC U+1F1FA",
737 | "name": "Guam",
738 | "title": "flag for Guam",
739 | "dialCode": "+1 671"
740 | },
741 | {
742 | "code": "GW",
743 | "emoji": "🇬🇼",
744 | "unicode": "U+1F1EC U+1F1FC",
745 | "name": "Guinea-Bissau",
746 | "title": "flag for Guinea-Bissau",
747 | "dialCode": "+245"
748 | },
749 | {
750 | "code": "GY",
751 | "emoji": "🇬🇾",
752 | "unicode": "U+1F1EC U+1F1FE",
753 | "name": "Guyana",
754 | "title": "flag for Guyana",
755 | "dialCode": "+595"
756 | },
757 | {
758 | "code": "HK",
759 | "emoji": "🇭🇰",
760 | "unicode": "U+1F1ED U+1F1F0",
761 | "name": "Hong Kong",
762 | "title": "flag for Hong Kong",
763 | "dialCode": "+852"
764 | },
765 | {
766 | "code": "HM",
767 | "emoji": "🇭🇲",
768 | "unicode": "U+1F1ED U+1F1F2",
769 | "name": "Heard Island and Mcdonald Islands",
770 | "title": "flag for Heard Island and Mcdonald Islands"
771 | },
772 | {
773 | "code": "HN",
774 | "emoji": "🇭🇳",
775 | "unicode": "U+1F1ED U+1F1F3",
776 | "name": "Honduras",
777 | "title": "flag for Honduras",
778 | "dialCode": "+504"
779 | },
780 | {
781 | "code": "HR",
782 | "emoji": "🇭🇷",
783 | "unicode": "U+1F1ED U+1F1F7",
784 | "name": "Croatia",
785 | "title": "flag for Croatia",
786 | "dialCode": "+385"
787 | },
788 | {
789 | "code": "HT",
790 | "emoji": "🇭🇹",
791 | "unicode": "U+1F1ED U+1F1F9",
792 | "name": "Haiti",
793 | "title": "flag for Haiti",
794 | "dialCode": "+509"
795 | },
796 | {
797 | "code": "HU",
798 | "emoji": "🇭🇺",
799 | "unicode": "U+1F1ED U+1F1FA",
800 | "name": "Hungary",
801 | "title": "flag for Hungary",
802 | "dialCode": "+36"
803 | },
804 | {
805 | "code": "ID",
806 | "emoji": "🇮🇩",
807 | "unicode": "U+1F1EE U+1F1E9",
808 | "name": "Indonesia",
809 | "title": "flag for Indonesia",
810 | "dialCode": "+62"
811 | },
812 | {
813 | "code": "IE",
814 | "emoji": "🇮🇪",
815 | "unicode": "U+1F1EE U+1F1EA",
816 | "name": "Ireland",
817 | "title": "flag for Ireland",
818 | "dialCode": "+353"
819 | },
820 | {
821 | "code": "IL",
822 | "emoji": "🇮🇱",
823 | "unicode": "U+1F1EE U+1F1F1",
824 | "name": "Israel",
825 | "title": "flag for Israel",
826 | "dialCode": "+972"
827 | },
828 | {
829 | "code": "IM",
830 | "emoji": "🇮🇲",
831 | "unicode": "U+1F1EE U+1F1F2",
832 | "name": "Isle of Man",
833 | "title": "flag for Isle of Man",
834 | "dialCode": "+44"
835 | },
836 | {
837 | "code": "IN",
838 | "emoji": "🇮🇳",
839 | "unicode": "U+1F1EE U+1F1F3",
840 | "name": "India",
841 | "title": "flag for India",
842 | "dialCode": "+91"
843 | },
844 | {
845 | "code": "IO",
846 | "emoji": "🇮🇴",
847 | "unicode": "U+1F1EE U+1F1F4",
848 | "name": "British Indian Ocean Territory",
849 | "title": "flag for British Indian Ocean Territory",
850 | "dialCode": "+246"
851 | },
852 | {
853 | "code": "IQ",
854 | "emoji": "🇮🇶",
855 | "unicode": "U+1F1EE U+1F1F6",
856 | "name": "Iraq",
857 | "title": "flag for Iraq",
858 | "dialCode": "+964"
859 | },
860 | {
861 | "code": "IR",
862 | "emoji": "🇮🇷",
863 | "unicode": "U+1F1EE U+1F1F7",
864 | "name": "Iran",
865 | "title": "flag for Iran",
866 | "dialCode": "+98"
867 | },
868 | {
869 | "code": "IS",
870 | "emoji": "🇮🇸",
871 | "unicode": "U+1F1EE U+1F1F8",
872 | "name": "Iceland",
873 | "title": "flag for Iceland",
874 | "dialCode": "+354"
875 | },
876 | {
877 | "code": "IT",
878 | "emoji": "🇮🇹",
879 | "unicode": "U+1F1EE U+1F1F9",
880 | "name": "Italy",
881 | "title": "flag for Italy",
882 | "dialCode": "+39"
883 | },
884 | {
885 | "code": "JE",
886 | "emoji": "🇯🇪",
887 | "unicode": "U+1F1EF U+1F1EA",
888 | "name": "Jersey",
889 | "title": "flag for Jersey",
890 | "dialCode": "+44"
891 | },
892 | {
893 | "code": "JM",
894 | "emoji": "🇯🇲",
895 | "unicode": "U+1F1EF U+1F1F2",
896 | "name": "Jamaica",
897 | "title": "flag for Jamaica",
898 | "dialCode": "+1 876"
899 | },
900 | {
901 | "code": "JO",
902 | "emoji": "🇯🇴",
903 | "unicode": "U+1F1EF U+1F1F4",
904 | "name": "Jordan",
905 | "title": "flag for Jordan",
906 | "dialCode": "+962"
907 | },
908 | {
909 | "code": "JP",
910 | "emoji": "🇯🇵",
911 | "unicode": "U+1F1EF U+1F1F5",
912 | "name": "Japan",
913 | "title": "flag for Japan",
914 | "dialCode": "+81"
915 | },
916 | {
917 | "code": "KE",
918 | "emoji": "🇰🇪",
919 | "unicode": "U+1F1F0 U+1F1EA",
920 | "name": "Kenya",
921 | "title": "flag for Kenya",
922 | "dialCode": "+254"
923 | },
924 | {
925 | "code": "KG",
926 | "emoji": "🇰🇬",
927 | "unicode": "U+1F1F0 U+1F1EC",
928 | "name": "Kyrgyzstan",
929 | "title": "flag for Kyrgyzstan",
930 | "dialCode": "+996"
931 | },
932 | {
933 | "code": "KH",
934 | "emoji": "🇰🇭",
935 | "unicode": "U+1F1F0 U+1F1ED",
936 | "name": "Cambodia",
937 | "title": "flag for Cambodia",
938 | "dialCode": "+855"
939 | },
940 | {
941 | "code": "KI",
942 | "emoji": "🇰🇮",
943 | "unicode": "U+1F1F0 U+1F1EE",
944 | "name": "Kiribati",
945 | "title": "flag for Kiribati",
946 | "dialCode": "+686"
947 | },
948 | {
949 | "code": "KM",
950 | "emoji": "🇰🇲",
951 | "unicode": "U+1F1F0 U+1F1F2",
952 | "name": "Comoros",
953 | "title": "flag for Comoros",
954 | "dialCode": "+269"
955 | },
956 | {
957 | "code": "KN",
958 | "emoji": "🇰🇳",
959 | "unicode": "U+1F1F0 U+1F1F3",
960 | "name": "Saint Kitts and Nevis",
961 | "title": "flag for Saint Kitts and Nevis",
962 | "dialCode": "+1 869"
963 | },
964 | {
965 | "code": "KP",
966 | "emoji": "🇰🇵",
967 | "unicode": "U+1F1F0 U+1F1F5",
968 | "name": "North Korea",
969 | "title": "flag for North Korea",
970 | "dialCode": "+850"
971 | },
972 | {
973 | "code": "KR",
974 | "emoji": "🇰🇷",
975 | "unicode": "U+1F1F0 U+1F1F7",
976 | "name": "South Korea",
977 | "title": "flag for South Korea",
978 | "dialCode": "+82"
979 | },
980 | {
981 | "code": "KW",
982 | "emoji": "🇰🇼",
983 | "unicode": "U+1F1F0 U+1F1FC",
984 | "name": "Kuwait",
985 | "title": "flag for Kuwait",
986 | "dialCode": "+965"
987 | },
988 | {
989 | "code": "KY",
990 | "emoji": "🇰🇾",
991 | "unicode": "U+1F1F0 U+1F1FE",
992 | "name": "Cayman Islands",
993 | "title": "flag for Cayman Islands",
994 | "dialCode": "+ 345"
995 | },
996 | {
997 | "code": "KZ",
998 | "emoji": "🇰🇿",
999 | "unicode": "U+1F1F0 U+1F1FF",
1000 | "name": "Kazakhstan",
1001 | "title": "flag for Kazakhstan",
1002 | "dialCode": "+7 7"
1003 | },
1004 | {
1005 | "code": "LA",
1006 | "emoji": "🇱🇦",
1007 | "unicode": "U+1F1F1 U+1F1E6",
1008 | "name": "Lao People's Democratic Republic",
1009 | "title": "flag for Lao People's Democratic Republic",
1010 | "dialCode": "+856"
1011 | },
1012 | {
1013 | "code": "LB",
1014 | "emoji": "🇱🇧",
1015 | "unicode": "U+1F1F1 U+1F1E7",
1016 | "name": "Lebanon",
1017 | "title": "flag for Lebanon",
1018 | "dialCode": "+961"
1019 | },
1020 | {
1021 | "code": "LC",
1022 | "emoji": "🇱🇨",
1023 | "unicode": "U+1F1F1 U+1F1E8",
1024 | "name": "Saint Lucia",
1025 | "title": "flag for Saint Lucia",
1026 | "dialCode": "+1 758"
1027 | },
1028 | {
1029 | "code": "LI",
1030 | "emoji": "🇱🇮",
1031 | "unicode": "U+1F1F1 U+1F1EE",
1032 | "name": "Liechtenstein",
1033 | "title": "flag for Liechtenstein",
1034 | "dialCode": "+423"
1035 | },
1036 | {
1037 | "code": "LK",
1038 | "emoji": "🇱🇰",
1039 | "unicode": "U+1F1F1 U+1F1F0",
1040 | "name": "Sri Lanka",
1041 | "title": "flag for Sri Lanka",
1042 | "dialCode": "+94"
1043 | },
1044 | {
1045 | "code": "LR",
1046 | "emoji": "🇱🇷",
1047 | "unicode": "U+1F1F1 U+1F1F7",
1048 | "name": "Liberia",
1049 | "title": "flag for Liberia",
1050 | "dialCode": "+231"
1051 | },
1052 | {
1053 | "code": "LS",
1054 | "emoji": "🇱🇸",
1055 | "unicode": "U+1F1F1 U+1F1F8",
1056 | "name": "Lesotho",
1057 | "title": "flag for Lesotho",
1058 | "dialCode": "+266"
1059 | },
1060 | {
1061 | "code": "LT",
1062 | "emoji": "🇱🇹",
1063 | "unicode": "U+1F1F1 U+1F1F9",
1064 | "name": "Lithuania",
1065 | "title": "flag for Lithuania",
1066 | "dialCode": "+370"
1067 | },
1068 | {
1069 | "code": "LU",
1070 | "emoji": "🇱🇺",
1071 | "unicode": "U+1F1F1 U+1F1FA",
1072 | "name": "Luxembourg",
1073 | "title": "flag for Luxembourg",
1074 | "dialCode": "+352"
1075 | },
1076 | {
1077 | "code": "LV",
1078 | "emoji": "🇱🇻",
1079 | "unicode": "U+1F1F1 U+1F1FB",
1080 | "name": "Latvia",
1081 | "title": "flag for Latvia",
1082 | "dialCode": "+371"
1083 | },
1084 | {
1085 | "code": "LY",
1086 | "emoji": "🇱🇾",
1087 | "unicode": "U+1F1F1 U+1F1FE",
1088 | "name": "Libya",
1089 | "title": "flag for Libya",
1090 | "dialCode": "+218"
1091 | },
1092 | {
1093 | "code": "MA",
1094 | "emoji": "🇲🇦",
1095 | "unicode": "U+1F1F2 U+1F1E6",
1096 | "name": "Morocco",
1097 | "title": "flag for Morocco",
1098 | "dialCode": "+212"
1099 | },
1100 | {
1101 | "code": "MC",
1102 | "emoji": "🇲🇨",
1103 | "unicode": "U+1F1F2 U+1F1E8",
1104 | "name": "Monaco",
1105 | "title": "flag for Monaco",
1106 | "dialCode": "+377"
1107 | },
1108 | {
1109 | "code": "MD",
1110 | "emoji": "🇲🇩",
1111 | "unicode": "U+1F1F2 U+1F1E9",
1112 | "name": "Moldova",
1113 | "title": "flag for Moldova",
1114 | "dialCode": "+373"
1115 | },
1116 | {
1117 | "code": "ME",
1118 | "emoji": "🇲🇪",
1119 | "unicode": "U+1F1F2 U+1F1EA",
1120 | "name": "Montenegro",
1121 | "title": "flag for Montenegro",
1122 | "dialCode": "+382"
1123 | },
1124 | {
1125 | "code": "MF",
1126 | "emoji": "🇲🇫",
1127 | "unicode": "U+1F1F2 U+1F1EB",
1128 | "name": "Saint Martin (French Part)",
1129 | "title": "flag for Saint Martin (French Part)",
1130 | "dialCode": "+590"
1131 | },
1132 | {
1133 | "code": "MG",
1134 | "emoji": "🇲🇬",
1135 | "unicode": "U+1F1F2 U+1F1EC",
1136 | "name": "Madagascar",
1137 | "title": "flag for Madagascar",
1138 | "dialCode": "+261"
1139 | },
1140 | {
1141 | "code": "MH",
1142 | "emoji": "🇲🇭",
1143 | "unicode": "U+1F1F2 U+1F1ED",
1144 | "name": "Marshall Islands",
1145 | "title": "flag for Marshall Islands",
1146 | "dialCode": "+692"
1147 | },
1148 | {
1149 | "code": "MK",
1150 | "emoji": "🇲🇰",
1151 | "unicode": "U+1F1F2 U+1F1F0",
1152 | "name": "Macedonia",
1153 | "title": "flag for Macedonia",
1154 | "dialCode": "+389"
1155 | },
1156 | {
1157 | "code": "ML",
1158 | "emoji": "🇲🇱",
1159 | "unicode": "U+1F1F2 U+1F1F1",
1160 | "name": "Mali",
1161 | "title": "flag for Mali",
1162 | "dialCode": "+223"
1163 | },
1164 | {
1165 | "code": "MM",
1166 | "emoji": "🇲🇲",
1167 | "unicode": "U+1F1F2 U+1F1F2",
1168 | "name": "Myanmar",
1169 | "title": "flag for Myanmar",
1170 | "dialCode": "+95"
1171 | },
1172 | {
1173 | "code": "MN",
1174 | "emoji": "🇲🇳",
1175 | "unicode": "U+1F1F2 U+1F1F3",
1176 | "name": "Mongolia",
1177 | "title": "flag for Mongolia",
1178 | "dialCode": "+976"
1179 | },
1180 | {
1181 | "code": "MO",
1182 | "emoji": "🇲🇴",
1183 | "unicode": "U+1F1F2 U+1F1F4",
1184 | "name": "Macao",
1185 | "title": "flag for Macao",
1186 | "dialCode": "+853"
1187 | },
1188 | {
1189 | "code": "MP",
1190 | "emoji": "🇲🇵",
1191 | "unicode": "U+1F1F2 U+1F1F5",
1192 | "name": "Northern Mariana Islands",
1193 | "title": "flag for Northern Mariana Islands",
1194 | "dialCode": "+1 670"
1195 | },
1196 | {
1197 | "code": "MQ",
1198 | "emoji": "🇲🇶",
1199 | "unicode": "U+1F1F2 U+1F1F6",
1200 | "name": "Martinique",
1201 | "title": "flag for Martinique",
1202 | "dialCode": "+596"
1203 | },
1204 | {
1205 | "code": "MR",
1206 | "emoji": "🇲🇷",
1207 | "unicode": "U+1F1F2 U+1F1F7",
1208 | "name": "Mauritania",
1209 | "title": "flag for Mauritania",
1210 | "dialCode": "+222"
1211 | },
1212 | {
1213 | "code": "MS",
1214 | "emoji": "🇲🇸",
1215 | "unicode": "U+1F1F2 U+1F1F8",
1216 | "name": "Montserrat",
1217 | "title": "flag for Montserrat",
1218 | "dialCode": "+1664"
1219 | },
1220 | {
1221 | "code": "MT",
1222 | "emoji": "🇲🇹",
1223 | "unicode": "U+1F1F2 U+1F1F9",
1224 | "name": "Malta",
1225 | "title": "flag for Malta",
1226 | "dialCode": "+356"
1227 | },
1228 | {
1229 | "code": "MU",
1230 | "emoji": "🇲🇺",
1231 | "unicode": "U+1F1F2 U+1F1FA",
1232 | "name": "Mauritius",
1233 | "title": "flag for Mauritius",
1234 | "dialCode": "+230"
1235 | },
1236 | {
1237 | "code": "MV",
1238 | "emoji": "🇲🇻",
1239 | "unicode": "U+1F1F2 U+1F1FB",
1240 | "name": "Maldives",
1241 | "title": "flag for Maldives",
1242 | "dialCode": "+960"
1243 | },
1244 | {
1245 | "code": "MW",
1246 | "emoji": "🇲🇼",
1247 | "unicode": "U+1F1F2 U+1F1FC",
1248 | "name": "Malawi",
1249 | "title": "flag for Malawi",
1250 | "dialCode": "+265"
1251 | },
1252 | {
1253 | "code": "MX",
1254 | "emoji": "🇲🇽",
1255 | "unicode": "U+1F1F2 U+1F1FD",
1256 | "name": "Mexico",
1257 | "title": "flag for Mexico",
1258 | "dialCode": "+52"
1259 | },
1260 | {
1261 | "code": "MY",
1262 | "emoji": "🇲🇾",
1263 | "unicode": "U+1F1F2 U+1F1FE",
1264 | "name": "Malaysia",
1265 | "title": "flag for Malaysia",
1266 | "dialCode": "+60"
1267 | },
1268 | {
1269 | "code": "MZ",
1270 | "emoji": "🇲🇿",
1271 | "unicode": "U+1F1F2 U+1F1FF",
1272 | "name": "Mozambique",
1273 | "title": "flag for Mozambique",
1274 | "dialCode": "+258"
1275 | },
1276 | {
1277 | "code": "NA",
1278 | "emoji": "🇳🇦",
1279 | "unicode": "U+1F1F3 U+1F1E6",
1280 | "name": "Namibia",
1281 | "title": "flag for Namibia",
1282 | "dialCode": "+264"
1283 | },
1284 | {
1285 | "code": "NC",
1286 | "emoji": "🇳🇨",
1287 | "unicode": "U+1F1F3 U+1F1E8",
1288 | "name": "New Caledonia",
1289 | "title": "flag for New Caledonia",
1290 | "dialCode": "+687"
1291 | },
1292 | {
1293 | "code": "NE",
1294 | "emoji": "🇳🇪",
1295 | "unicode": "U+1F1F3 U+1F1EA",
1296 | "name": "Niger",
1297 | "title": "flag for Niger",
1298 | "dialCode": "+227"
1299 | },
1300 | {
1301 | "code": "NF",
1302 | "emoji": "🇳🇫",
1303 | "unicode": "U+1F1F3 U+1F1EB",
1304 | "name": "Norfolk Island",
1305 | "title": "flag for Norfolk Island",
1306 | "dialCode": "+672"
1307 | },
1308 | {
1309 | "code": "NG",
1310 | "emoji": "🇳🇬",
1311 | "unicode": "U+1F1F3 U+1F1EC",
1312 | "name": "Nigeria",
1313 | "title": "flag for Nigeria",
1314 | "dialCode": "+234"
1315 | },
1316 | {
1317 | "code": "NI",
1318 | "emoji": "🇳🇮",
1319 | "unicode": "U+1F1F3 U+1F1EE",
1320 | "name": "Nicaragua",
1321 | "title": "flag for Nicaragua",
1322 | "dialCode": "+505"
1323 | },
1324 | {
1325 | "code": "NL",
1326 | "emoji": "🇳🇱",
1327 | "unicode": "U+1F1F3 U+1F1F1",
1328 | "name": "Netherlands",
1329 | "title": "flag for Netherlands",
1330 | "dialCode": "+31"
1331 | },
1332 | {
1333 | "code": "NO",
1334 | "emoji": "🇳🇴",
1335 | "unicode": "U+1F1F3 U+1F1F4",
1336 | "name": "Norway",
1337 | "title": "flag for Norway",
1338 | "dialCode": "+47"
1339 | },
1340 | {
1341 | "code": "NP",
1342 | "emoji": "🇳🇵",
1343 | "unicode": "U+1F1F3 U+1F1F5",
1344 | "name": "Nepal",
1345 | "title": "flag for Nepal",
1346 | "dialCode": "+977"
1347 | },
1348 | {
1349 | "code": "NR",
1350 | "emoji": "🇳🇷",
1351 | "unicode": "U+1F1F3 U+1F1F7",
1352 | "name": "Nauru",
1353 | "title": "flag for Nauru",
1354 | "dialCode": "+674"
1355 | },
1356 | {
1357 | "code": "NU",
1358 | "emoji": "🇳🇺",
1359 | "unicode": "U+1F1F3 U+1F1FA",
1360 | "name": "Niue",
1361 | "title": "flag for Niue",
1362 | "dialCode": "+683"
1363 | },
1364 | {
1365 | "code": "NZ",
1366 | "emoji": "🇳🇿",
1367 | "unicode": "U+1F1F3 U+1F1FF",
1368 | "name": "New Zealand",
1369 | "title": "flag for New Zealand",
1370 | "dialCode": "+64"
1371 | },
1372 | {
1373 | "code": "OM",
1374 | "emoji": "🇴🇲",
1375 | "unicode": "U+1F1F4 U+1F1F2",
1376 | "name": "Oman",
1377 | "title": "flag for Oman",
1378 | "dialCode": "+968"
1379 | },
1380 | {
1381 | "code": "PA",
1382 | "emoji": "🇵🇦",
1383 | "unicode": "U+1F1F5 U+1F1E6",
1384 | "name": "Panama",
1385 | "title": "flag for Panama",
1386 | "dialCode": "+507"
1387 | },
1388 | {
1389 | "code": "PE",
1390 | "emoji": "🇵🇪",
1391 | "unicode": "U+1F1F5 U+1F1EA",
1392 | "name": "Peru",
1393 | "title": "flag for Peru",
1394 | "dialCode": "+51"
1395 | },
1396 | {
1397 | "code": "PF",
1398 | "emoji": "🇵🇫",
1399 | "unicode": "U+1F1F5 U+1F1EB",
1400 | "name": "French Polynesia",
1401 | "title": "flag for French Polynesia",
1402 | "dialCode": "+689"
1403 | },
1404 | {
1405 | "code": "PG",
1406 | "emoji": "🇵🇬",
1407 | "unicode": "U+1F1F5 U+1F1EC",
1408 | "name": "Papua New Guinea",
1409 | "title": "flag for Papua New Guinea",
1410 | "dialCode": "+675"
1411 | },
1412 | {
1413 | "code": "PH",
1414 | "emoji": "🇵🇭",
1415 | "unicode": "U+1F1F5 U+1F1ED",
1416 | "name": "Philippines",
1417 | "title": "flag for Philippines",
1418 | "dialCode": "+63"
1419 | },
1420 | {
1421 | "code": "PK",
1422 | "emoji": "🇵🇰",
1423 | "unicode": "U+1F1F5 U+1F1F0",
1424 | "name": "Pakistan",
1425 | "title": "flag for Pakistan",
1426 | "dialCode": "+92"
1427 | },
1428 | {
1429 | "code": "PL",
1430 | "emoji": "🇵🇱",
1431 | "unicode": "U+1F1F5 U+1F1F1",
1432 | "name": "Poland",
1433 | "title": "flag for Poland",
1434 | "dialCode": "+48"
1435 | },
1436 | {
1437 | "code": "PM",
1438 | "emoji": "🇵🇲",
1439 | "unicode": "U+1F1F5 U+1F1F2",
1440 | "name": "Saint Pierre and Miquelon",
1441 | "title": "flag for Saint Pierre and Miquelon",
1442 | "dialCode": "+508"
1443 | },
1444 | {
1445 | "code": "PN",
1446 | "emoji": "🇵🇳",
1447 | "unicode": "U+1F1F5 U+1F1F3",
1448 | "name": "Pitcairn",
1449 | "title": "flag for Pitcairn",
1450 | "dialCode": "+872"
1451 | },
1452 | {
1453 | "code": "PR",
1454 | "emoji": "🇵🇷",
1455 | "unicode": "U+1F1F5 U+1F1F7",
1456 | "name": "Puerto Rico",
1457 | "title": "flag for Puerto Rico",
1458 | "dialCode": "+1 939"
1459 | },
1460 | {
1461 | "code": "PS",
1462 | "emoji": "🇵🇸",
1463 | "unicode": "U+1F1F5 U+1F1F8",
1464 | "name": "Palestinian Territory",
1465 | "title": "flag for Palestinian Territory",
1466 | "dialCode": "+970"
1467 | },
1468 | {
1469 | "code": "PT",
1470 | "emoji": "🇵🇹",
1471 | "unicode": "U+1F1F5 U+1F1F9",
1472 | "name": "Portugal",
1473 | "title": "flag for Portugal",
1474 | "dialCode": "+351"
1475 | },
1476 | {
1477 | "code": "PW",
1478 | "emoji": "🇵🇼",
1479 | "unicode": "U+1F1F5 U+1F1FC",
1480 | "name": "Palau",
1481 | "title": "flag for Palau",
1482 | "dialCode": "+680"
1483 | },
1484 | {
1485 | "code": "PY",
1486 | "emoji": "🇵🇾",
1487 | "unicode": "U+1F1F5 U+1F1FE",
1488 | "name": "Paraguay",
1489 | "title": "flag for Paraguay",
1490 | "dialCode": "+595"
1491 | },
1492 | {
1493 | "code": "QA",
1494 | "emoji": "🇶🇦",
1495 | "unicode": "U+1F1F6 U+1F1E6",
1496 | "name": "Qatar",
1497 | "title": "flag for Qatar",
1498 | "dialCode": "+974"
1499 | },
1500 | {
1501 | "code": "RE",
1502 | "emoji": "🇷🇪",
1503 | "unicode": "U+1F1F7 U+1F1EA",
1504 | "name": "Réunion",
1505 | "title": "flag for Réunion",
1506 | "dialCode": "+262"
1507 | },
1508 | {
1509 | "code": "RO",
1510 | "emoji": "🇷🇴",
1511 | "unicode": "U+1F1F7 U+1F1F4",
1512 | "name": "Romania",
1513 | "title": "flag for Romania",
1514 | "dialCode": "+40"
1515 | },
1516 | {
1517 | "code": "RS",
1518 | "emoji": "🇷🇸",
1519 | "unicode": "U+1F1F7 U+1F1F8",
1520 | "name": "Serbia",
1521 | "title": "flag for Serbia",
1522 | "dialCode": "+381"
1523 | },
1524 | {
1525 | "code": "RU",
1526 | "emoji": "🇷🇺",
1527 | "unicode": "U+1F1F7 U+1F1FA",
1528 | "name": "Russia",
1529 | "title": "flag for Russia",
1530 | "dialCode": "+7"
1531 | },
1532 | {
1533 | "code": "RW",
1534 | "emoji": "🇷🇼",
1535 | "unicode": "U+1F1F7 U+1F1FC",
1536 | "name": "Rwanda",
1537 | "title": "flag for Rwanda",
1538 | "dialCode": "+250"
1539 | },
1540 | {
1541 | "code": "SA",
1542 | "emoji": "🇸🇦",
1543 | "unicode": "U+1F1F8 U+1F1E6",
1544 | "name": "Saudi Arabia",
1545 | "title": "flag for Saudi Arabia",
1546 | "dialCode": "+966"
1547 | },
1548 | {
1549 | "code": "SB",
1550 | "emoji": "🇸🇧",
1551 | "unicode": "U+1F1F8 U+1F1E7",
1552 | "name": "Solomon Islands",
1553 | "title": "flag for Solomon Islands",
1554 | "dialCode": "+677"
1555 | },
1556 | {
1557 | "code": "SC",
1558 | "emoji": "🇸🇨",
1559 | "unicode": "U+1F1F8 U+1F1E8",
1560 | "name": "Seychelles",
1561 | "title": "flag for Seychelles",
1562 | "dialCode": "+248"
1563 | },
1564 | {
1565 | "code": "SD",
1566 | "emoji": "🇸🇩",
1567 | "unicode": "U+1F1F8 U+1F1E9",
1568 | "name": "Sudan",
1569 | "title": "flag for Sudan",
1570 | "dialCode": "+249"
1571 | },
1572 | {
1573 | "code": "SE",
1574 | "emoji": "🇸🇪",
1575 | "unicode": "U+1F1F8 U+1F1EA",
1576 | "name": "Sweden",
1577 | "title": "flag for Sweden",
1578 | "dialCode": "+46"
1579 | },
1580 | {
1581 | "code": "SG",
1582 | "emoji": "🇸🇬",
1583 | "unicode": "U+1F1F8 U+1F1EC",
1584 | "name": "Singapore",
1585 | "title": "flag for Singapore",
1586 | "dialCode": "+65"
1587 | },
1588 | {
1589 | "code": "SH",
1590 | "emoji": "🇸🇭",
1591 | "unicode": "U+1F1F8 U+1F1ED",
1592 | "name": "Saint Helena, Ascension and Tristan Da Cunha",
1593 | "title": "flag for Saint Helena, Ascension and Tristan Da Cunha",
1594 | "dialCode": "+290"
1595 | },
1596 | {
1597 | "code": "SI",
1598 | "emoji": "🇸🇮",
1599 | "unicode": "U+1F1F8 U+1F1EE",
1600 | "name": "Slovenia",
1601 | "title": "flag for Slovenia",
1602 | "dialCode": "+386"
1603 | },
1604 | {
1605 | "code": "SJ",
1606 | "emoji": "🇸🇯",
1607 | "unicode": "U+1F1F8 U+1F1EF",
1608 | "name": "Svalbard and Jan Mayen",
1609 | "title": "flag for Svalbard and Jan Mayen",
1610 | "dialCode": "+47"
1611 | },
1612 | {
1613 | "code": "SK",
1614 | "emoji": "🇸🇰",
1615 | "unicode": "U+1F1F8 U+1F1F0",
1616 | "name": "Slovakia",
1617 | "title": "flag for Slovakia",
1618 | "dialCode": "+421"
1619 | },
1620 | {
1621 | "code": "SL",
1622 | "emoji": "🇸🇱",
1623 | "unicode": "U+1F1F8 U+1F1F1",
1624 | "name": "Sierra Leone",
1625 | "title": "flag for Sierra Leone",
1626 | "dialCode": "+232"
1627 | },
1628 | {
1629 | "code": "SM",
1630 | "emoji": "🇸🇲",
1631 | "unicode": "U+1F1F8 U+1F1F2",
1632 | "name": "San Marino",
1633 | "title": "flag for San Marino",
1634 | "dialCode": "+378"
1635 | },
1636 | {
1637 | "code": "SN",
1638 | "emoji": "🇸🇳",
1639 | "unicode": "U+1F1F8 U+1F1F3",
1640 | "name": "Senegal",
1641 | "title": "flag for Senegal",
1642 | "dialCode": "+221"
1643 | },
1644 | {
1645 | "code": "SO",
1646 | "emoji": "🇸🇴",
1647 | "unicode": "U+1F1F8 U+1F1F4",
1648 | "name": "Somalia",
1649 | "title": "flag for Somalia",
1650 | "dialCode": "+252"
1651 | },
1652 | {
1653 | "code": "SR",
1654 | "emoji": "🇸🇷",
1655 | "unicode": "U+1F1F8 U+1F1F7",
1656 | "name": "Suriname",
1657 | "title": "flag for Suriname",
1658 | "dialCode": "+597"
1659 | },
1660 | {
1661 | "code": "SS",
1662 | "emoji": "🇸🇸",
1663 | "unicode": "U+1F1F8 U+1F1F8",
1664 | "name": "South Sudan",
1665 | "title": "flag for South Sudan"
1666 | },
1667 | {
1668 | "code": "ST",
1669 | "emoji": "🇸🇹",
1670 | "unicode": "U+1F1F8 U+1F1F9",
1671 | "name": "Sao Tome and Principe",
1672 | "title": "flag for Sao Tome and Principe",
1673 | "dialCode": "+239"
1674 | },
1675 | {
1676 | "code": "SV",
1677 | "emoji": "🇸🇻",
1678 | "unicode": "U+1F1F8 U+1F1FB",
1679 | "name": "El Salvador",
1680 | "title": "flag for El Salvador",
1681 | "dialCode": "+503"
1682 | },
1683 | {
1684 | "code": "SX",
1685 | "emoji": "🇸🇽",
1686 | "unicode": "U+1F1F8 U+1F1FD",
1687 | "name": "Sint Maarten (Dutch Part)",
1688 | "title": "flag for Sint Maarten (Dutch Part)"
1689 | },
1690 | {
1691 | "code": "SY",
1692 | "emoji": "🇸🇾",
1693 | "unicode": "U+1F1F8 U+1F1FE",
1694 | "name": "Syrian Arab Republic",
1695 | "title": "flag for Syrian Arab Republic",
1696 | "dialCode": "+963"
1697 | },
1698 | {
1699 | "code": "SZ",
1700 | "emoji": "🇸🇿",
1701 | "unicode": "U+1F1F8 U+1F1FF",
1702 | "name": "Swaziland",
1703 | "title": "flag for Swaziland",
1704 | "dialCode": "+268"
1705 | },
1706 | {
1707 | "code": "TC",
1708 | "emoji": "🇹🇨",
1709 | "unicode": "U+1F1F9 U+1F1E8",
1710 | "name": "Turks and Caicos Islands",
1711 | "title": "flag for Turks and Caicos Islands",
1712 | "dialCode": "+1 649"
1713 | },
1714 | {
1715 | "code": "TD",
1716 | "emoji": "🇹🇩",
1717 | "unicode": "U+1F1F9 U+1F1E9",
1718 | "name": "Chad",
1719 | "title": "flag for Chad",
1720 | "dialCode": "+235"
1721 | },
1722 | {
1723 | "code": "TF",
1724 | "emoji": "🇹🇫",
1725 | "unicode": "U+1F1F9 U+1F1EB",
1726 | "name": "French Southern Territories",
1727 | "title": "flag for French Southern Territories"
1728 | },
1729 | {
1730 | "code": "TG",
1731 | "emoji": "🇹🇬",
1732 | "unicode": "U+1F1F9 U+1F1EC",
1733 | "name": "Togo",
1734 | "title": "flag for Togo",
1735 | "dialCode": "+228"
1736 | },
1737 | {
1738 | "code": "TH",
1739 | "emoji": "🇹🇭",
1740 | "unicode": "U+1F1F9 U+1F1ED",
1741 | "name": "Thailand",
1742 | "title": "flag for Thailand",
1743 | "dialCode": "+66"
1744 | },
1745 | {
1746 | "code": "TJ",
1747 | "emoji": "🇹🇯",
1748 | "unicode": "U+1F1F9 U+1F1EF",
1749 | "name": "Tajikistan",
1750 | "title": "flag for Tajikistan",
1751 | "dialCode": "+992"
1752 | },
1753 | {
1754 | "code": "TK",
1755 | "emoji": "🇹🇰",
1756 | "unicode": "U+1F1F9 U+1F1F0",
1757 | "name": "Tokelau",
1758 | "title": "flag for Tokelau",
1759 | "dialCode": "+690"
1760 | },
1761 | {
1762 | "code": "TL",
1763 | "emoji": "🇹🇱",
1764 | "unicode": "U+1F1F9 U+1F1F1",
1765 | "name": "Timor-Leste",
1766 | "title": "flag for Timor-Leste",
1767 | "dialCode": "+670"
1768 | },
1769 | {
1770 | "code": "TM",
1771 | "emoji": "🇹🇲",
1772 | "unicode": "U+1F1F9 U+1F1F2",
1773 | "name": "Turkmenistan",
1774 | "title": "flag for Turkmenistan",
1775 | "dialCode": "+993"
1776 | },
1777 | {
1778 | "code": "TN",
1779 | "emoji": "🇹🇳",
1780 | "unicode": "U+1F1F9 U+1F1F3",
1781 | "name": "Tunisia",
1782 | "title": "flag for Tunisia",
1783 | "dialCode": "+216"
1784 | },
1785 | {
1786 | "code": "TO",
1787 | "emoji": "🇹🇴",
1788 | "unicode": "U+1F1F9 U+1F1F4",
1789 | "name": "Tonga",
1790 | "title": "flag for Tonga",
1791 | "dialCode": "+676"
1792 | },
1793 | {
1794 | "code": "TR",
1795 | "emoji": "🇹🇷",
1796 | "unicode": "U+1F1F9 U+1F1F7",
1797 | "name": "Turkey",
1798 | "title": "flag for Turkey",
1799 | "dialCode": "+90"
1800 | },
1801 | {
1802 | "code": "TT",
1803 | "emoji": "🇹🇹",
1804 | "unicode": "U+1F1F9 U+1F1F9",
1805 | "name": "Trinidad and Tobago",
1806 | "title": "flag for Trinidad and Tobago",
1807 | "dialCode": "+1 868"
1808 | },
1809 | {
1810 | "code": "TV",
1811 | "emoji": "🇹🇻",
1812 | "unicode": "U+1F1F9 U+1F1FB",
1813 | "name": "Tuvalu",
1814 | "title": "flag for Tuvalu",
1815 | "dialCode": "+688"
1816 | },
1817 | {
1818 | "code": "TW",
1819 | "emoji": "🇹🇼",
1820 | "unicode": "U+1F1F9 U+1F1FC",
1821 | "name": "Taiwan",
1822 | "title": "flag for Taiwan",
1823 | "dialCode": "+886"
1824 | },
1825 | {
1826 | "code": "TZ",
1827 | "emoji": "🇹🇿",
1828 | "unicode": "U+1F1F9 U+1F1FF",
1829 | "name": "Tanzania",
1830 | "title": "flag for Tanzania",
1831 | "dialCode": "+255"
1832 | },
1833 | {
1834 | "code": "UA",
1835 | "emoji": "🇺🇦",
1836 | "unicode": "U+1F1FA U+1F1E6",
1837 | "name": "Ukraine",
1838 | "title": "flag for Ukraine",
1839 | "dialCode": "+380"
1840 | },
1841 | {
1842 | "code": "UG",
1843 | "emoji": "🇺🇬",
1844 | "unicode": "U+1F1FA U+1F1EC",
1845 | "name": "Uganda",
1846 | "title": "flag for Uganda",
1847 | "dialCode": "+256"
1848 | },
1849 | {
1850 | "code": "UM",
1851 | "emoji": "🇺🇲",
1852 | "unicode": "U+1F1FA U+1F1F2",
1853 | "name": "United States Minor Outlying Islands",
1854 | "title": "flag for United States Minor Outlying Islands"
1855 | },
1856 | {
1857 | "code": "US",
1858 | "emoji": "🇺🇸",
1859 | "unicode": "U+1F1FA U+1F1F8",
1860 | "name": "United States",
1861 | "title": "flag for United States",
1862 | "dialCode": "+1"
1863 | },
1864 | {
1865 | "code": "UY",
1866 | "emoji": "🇺🇾",
1867 | "unicode": "U+1F1FA U+1F1FE",
1868 | "name": "Uruguay",
1869 | "title": "flag for Uruguay",
1870 | "dialCode": "+598"
1871 | },
1872 | {
1873 | "code": "UZ",
1874 | "emoji": "🇺🇿",
1875 | "unicode": "U+1F1FA U+1F1FF",
1876 | "name": "Uzbekistan",
1877 | "title": "flag for Uzbekistan",
1878 | "dialCode": "+998"
1879 | },
1880 | {
1881 | "code": "VA",
1882 | "emoji": "🇻🇦",
1883 | "unicode": "U+1F1FB U+1F1E6",
1884 | "name": "Vatican City",
1885 | "title": "flag for Vatican City",
1886 | "dialCode": "+379"
1887 | },
1888 | {
1889 | "code": "VC",
1890 | "emoji": "🇻🇨",
1891 | "unicode": "U+1F1FB U+1F1E8",
1892 | "name": "Saint Vincent and The Grenadines",
1893 | "title": "flag for Saint Vincent and The Grenadines",
1894 | "dialCode": "+1 784"
1895 | },
1896 | {
1897 | "code": "VE",
1898 | "emoji": "🇻🇪",
1899 | "unicode": "U+1F1FB U+1F1EA",
1900 | "name": "Venezuela",
1901 | "title": "flag for Venezuela",
1902 | "dialCode": "+58"
1903 | },
1904 | {
1905 | "code": "VG",
1906 | "emoji": "🇻🇬",
1907 | "unicode": "U+1F1FB U+1F1EC",
1908 | "name": "Virgin Islands, British",
1909 | "title": "flag for Virgin Islands, British",
1910 | "dialCode": "+1 284"
1911 | },
1912 | {
1913 | "code": "VI",
1914 | "emoji": "🇻🇮",
1915 | "unicode": "U+1F1FB U+1F1EE",
1916 | "name": "Virgin Islands, U.S.",
1917 | "title": "flag for Virgin Islands, U.S.",
1918 | "dialCode": "+1 340"
1919 | },
1920 | {
1921 | "code": "VN",
1922 | "emoji": "🇻🇳",
1923 | "unicode": "U+1F1FB U+1F1F3",
1924 | "name": "Viet Nam",
1925 | "title": "flag for Viet Nam",
1926 | "dialCode": "+84"
1927 | },
1928 | {
1929 | "code": "VU",
1930 | "emoji": "🇻🇺",
1931 | "unicode": "U+1F1FB U+1F1FA",
1932 | "name": "Vanuatu",
1933 | "title": "flag for Vanuatu",
1934 | "dialCode": "+678"
1935 | },
1936 | {
1937 | "code": "WF",
1938 | "emoji": "🇼🇫",
1939 | "unicode": "U+1F1FC U+1F1EB",
1940 | "name": "Wallis and Futuna",
1941 | "title": "flag for Wallis and Futuna",
1942 | "dialCode": "+681"
1943 | },
1944 | {
1945 | "code": "WS",
1946 | "emoji": "🇼🇸",
1947 | "unicode": "U+1F1FC U+1F1F8",
1948 | "name": "Samoa",
1949 | "title": "flag for Samoa",
1950 | "dialCode": "+685"
1951 | },
1952 | {
1953 | "code": "XK",
1954 | "emoji": "🇽🇰",
1955 | "unicode": "U+1F1FD U+1F1F0",
1956 | "name": "Kosovo",
1957 | "title": "flag for Kosovo",
1958 | "dialCode": "+383"
1959 | },
1960 | {
1961 | "code": "YE",
1962 | "emoji": "🇾🇪",
1963 | "unicode": "U+1F1FE U+1F1EA",
1964 | "name": "Yemen",
1965 | "title": "flag for Yemen",
1966 | "dialCode": "+967"
1967 | },
1968 | {
1969 | "code": "YT",
1970 | "emoji": "🇾🇹",
1971 | "unicode": "U+1F1FE U+1F1F9",
1972 | "name": "Mayotte",
1973 | "title": "flag for Mayotte",
1974 | "dialCode": "+262"
1975 | },
1976 | {
1977 | "code": "ZA",
1978 | "emoji": "🇿🇦",
1979 | "unicode": "U+1F1FF U+1F1E6",
1980 | "name": "South Africa",
1981 | "title": "flag for South Africa",
1982 | "dialCode": "+27"
1983 | },
1984 | {
1985 | "code": "ZM",
1986 | "emoji": "🇿🇲",
1987 | "unicode": "U+1F1FF U+1F1F2",
1988 | "name": "Zambia",
1989 | "title": "flag for Zambia",
1990 | "dialCode": "+260"
1991 | },
1992 | {
1993 | "code": "ZW",
1994 | "emoji": "🇿🇼",
1995 | "unicode": "U+1F1FF U+1F1FC",
1996 | "name": "Zimbabwe",
1997 | "title": "flag for Zimbabwe",
1998 | "dialCode": "+263"
1999 | }
2000 | ]
--------------------------------------------------------------------------------
/src/helpers/calculation.helper.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ByCircleOptions,
3 | ByCountryCodeOptions,
4 | ByProximityOptions,
5 | ByRectOptions,
6 | GeocoderAutocompleteOptions
7 | } from "../autocomplete";
8 | import { BY_CIRCLE, BY_COUNTRYCODE, BY_PLACE, BY_PROXIMITY, BY_RECT } from "./constants";
9 |
10 | export class CalculationHelper {
11 | public static isLatitude(num: any) {
12 | return num !== '' && num !== null && isFinite(num) && Math.abs(num) <= 90;
13 | }
14 |
15 | public static isLongitude(num: any) {
16 | return num !== '' && num !== null && isFinite(num) && Math.abs(num) <= 180;
17 | }
18 |
19 | public static isNotOpenStreetMapData(feature: any) {
20 | return feature.properties.datasource?.sourcename !== 'openstreetmap' || !feature.properties.place_id;
21 | }
22 |
23 | public static extendByNonVerifiedValues(options: GeocoderAutocompleteOptions, features: any, parsedAddress: any) {
24 | features.forEach((feature: any) => {
25 | if (parsedAddress.housenumber &&
26 | options.allowNonVerifiedHouseNumber && feature.properties.rank.match_type === "match_by_street") {
27 | // add housenumber
28 | this.addHouseNumberToFormatted(feature.properties, null, parsedAddress.housenumber)
29 | feature.properties.nonVerifiedParts = ["housenumber"];
30 | } else if (parsedAddress.street && parsedAddress.housenumber &&
31 | options.allowNonVerifiedStreet &&
32 | (feature.properties.rank.match_type === "match_by_city_or_disrict" || feature.properties.rank.match_type === "match_by_postcode")) {
33 | // add housenumber and street
34 | this.addHouseNumberToFormatted(feature.properties, parsedAddress.street, parsedAddress.housenumber)
35 | feature.properties.nonVerifiedParts = ["housenumber", "street"];
36 | } else if (parsedAddress.street &&
37 | options.allowNonVerifiedStreet &&
38 | (feature.properties.rank.match_type === "match_by_city_or_disrict" || feature.properties.rank.match_type === "match_by_postcode")) {
39 | // add street
40 | feature.properties.street = parsedAddress.street.replace(/(^\w|\s\w|[-]\w)/g, (m: string) => m.toUpperCase());
41 |
42 | feature.properties.address_line1 = feature.properties.street;
43 | feature.properties.address_line2 = feature.properties.formatted;
44 |
45 | feature.properties.formatted = feature.properties.street + ", " + feature.properties.formatted;
46 | feature.properties.nonVerifiedParts = ["street"];
47 | }
48 | });
49 | }
50 |
51 | private static addHouseNumberToFormatted(featureProperties: any, street: string, housenumber: string) {
52 | const houseNumberAndStreetFormatsPerCountry: { [key: string]: string[] } = {
53 | "{{{road}}} {{{house_number}}}": ["af", "ai", "al", "ao", "ar", "at", "aw", "ax", "ba", "be", "bg", "bi", "bo", "bq", "br", "bs", "bt", "bv", "bw", "cf", "ch", "cl", "cm", "co", "cr", "cu", "cv", "cw", "cy", "cz", "de", "dk", "do", "ec", "ee", "eh", "er", "et", "fi", "fo", "gd", "ge", "gl", "gq", "gr", "gt", "gw", "hn", "hr", "ht", "hu", "id", "il", "ir", "is", "jo", "ki", "km", "kp", "kw", "lc", "li", "lr", "lt", "lv", "ly", "me", "mk", "ml", "mn", "mo", "mx", "ni", "nl", "no", "np", "pa", "pe", "pl", "ps", "pt", "pw", "py", "qa", "ro", "rs", "ru", "sb", "sd", "se", "si", "sj", "sk", "so", "sr", "ss", "st", "sv", "sx", "sz", "td", "tj", "tl", "tr", "um", "uz", "uy", "vc", "ve", "vu", "ws"],
54 | "{{{house_number}}} {{{road}}}": ["ad", "ae", "ag", "am", "as", "au", "az", "bb", "bd", "bf", "bh", "bl", "bm", "bz", "ca", "cc", "ci", "ck", "cn", "cx", "dj", "dm", "dz", "eg", "fj", "fk", "fm", "fr", "ga", "gb", "gf", "gg", "gh", "gi", "gm", "gn", "gp", "gs", "gu", "gy", "hk", "hm", "ie", "im", "io", "iq", "je", "jm", "jp", "ke", "kh", "kn", "kr", "ky", "lb", "lk", "ls", "lu", "ma", "mc", "mf", "mh", "mg", "mm", "mp", "ms", "mt", "mq", "mv", "mw", "my", "na", "nc", "ne", "nf", "ng", "nr", "nu", "nz", "om", "pf", "pg", "ph", "pk", "pm", "pr", "re", "rw", "sa", "sc", "sg", "sh", "sl", "sn", "tc", "tf", "th", "tk", "tn", "to", "tt", "tv", "tw", "tz", "ug", "us", "vg", "vi", "wf", "yt", "za", "zm", "zw"],
55 | "{{{road}}}, {{{house_number}}}": ["by", "es", "it", "kg", "kz", "md", "mz", "sm", "sy", "ua", "va"],
56 | "{{{house_number}}}, {{{road}}}": ["bj", "bn", "cd", "cg", "in", "la", "mr", "mu", "tg", "tm", "vn", "ye"]
57 | }
58 |
59 | const format = Object.keys(houseNumberAndStreetFormatsPerCountry).find(key => houseNumberAndStreetFormatsPerCountry[key].indexOf(featureProperties.country_code) >= 0) || "{{{road}}} {{{house_number}}}";
60 |
61 | if (street) {
62 | // add street and housenumber
63 | featureProperties.street = street.replace(/(^\w|\s\w|[-]\w)/g, m => m.toUpperCase());
64 |
65 | featureProperties.housenumber = housenumber;
66 | const addressPart = format.replace("{{{road}}}", featureProperties.street).replace("{{{house_number}}}", housenumber);
67 | featureProperties.address_line1 = addressPart;
68 | featureProperties.address_line2 = featureProperties.formatted;
69 |
70 | featureProperties.formatted = addressPart + ", " + featureProperties.formatted;
71 | } else {
72 | // add house number only
73 | featureProperties.housenumber = housenumber;
74 | const addressPart = format.replace("{{{road}}}", featureProperties.street).replace("{{{house_number}}}", housenumber);
75 |
76 | featureProperties.address_line1 = featureProperties.address_line1.replace(featureProperties.street, addressPart);;
77 | featureProperties.formatted = featureProperties.formatted.replace(featureProperties.street, addressPart);
78 | }
79 | }
80 |
81 | public static generatePlacesUrl(placeDetailsUrl: string,
82 | placeId: string,
83 | apiKey: string,
84 | options: GeocoderAutocompleteOptions): string {
85 | let url = `${placeDetailsUrl}?id=${placeId}&apiKey=${apiKey}`;
86 | if (options.lang) {
87 | url += `&lang=${options.lang}`;
88 | }
89 | return url;
90 | }
91 |
92 | public static needToFilterDataBySuggestionsFilter(currentItems: any, suggestionsFilter?: (suggetions: any[]) => any[]) {
93 | return currentItems && currentItems.length && suggestionsFilter && typeof suggestionsFilter === 'function';
94 | }
95 |
96 | public static needToCalculateExtendByNonVerifiedValues(data: any,
97 | options: GeocoderAutocompleteOptions) {
98 | return data.features && data.features.length &&
99 | data?.query?.parsed &&
100 | (options.allowNonVerifiedHouseNumber || options.allowNonVerifiedStreet);
101 | }
102 |
103 | public static generateUrl(value: string,
104 | geocoderUrl: string,
105 | apiKey: string,
106 | options: GeocoderAutocompleteOptions): string {
107 | let url = `${geocoderUrl}?text=${encodeURIComponent(value)}&apiKey=${apiKey}`;
108 | // Add type of the location if set. Learn more about possible parameters on https://apidocs.geoapify.com/docs/geocoding/api/api
109 | if (options.type) {
110 | url += `&type=${options.type}`;
111 | }
112 |
113 | if (options.limit) {
114 | url += `&limit=${options.limit}`;
115 | }
116 |
117 | if (options.lang) {
118 | url += `&lang=${options.lang}`;
119 | }
120 |
121 | const filters = [];
122 | const filterByCountryCodes: ByCountryCodeOptions = options.filter[BY_COUNTRYCODE] as ByCountryCodeOptions;
123 | const filterByCircle: ByCircleOptions = options.filter[BY_CIRCLE] as ByCircleOptions;
124 | const filterByRect: ByRectOptions = options.filter[BY_RECT] as ByRectOptions;
125 | const filterByPlace: string = options.filter[BY_PLACE] as string;
126 |
127 | if (filterByCountryCodes && filterByCountryCodes.length) {
128 | filters.push(`countrycode:${filterByCountryCodes.join(',').toLowerCase()}`);
129 | }
130 |
131 | if (filterByCircle && CalculationHelper.isLatitude(filterByCircle.lat) && CalculationHelper.isLongitude(filterByCircle.lon) && filterByCircle.radiusMeters > 0) {
132 | filters.push(`circle:${filterByCircle.lon},${filterByCircle.lat},${filterByCircle.radiusMeters}`);
133 | }
134 |
135 | if (filterByRect && CalculationHelper.isLatitude(filterByRect.lat1) && CalculationHelper.isLongitude(filterByRect.lon1) && CalculationHelper.isLatitude(filterByRect.lat2) && CalculationHelper.isLongitude(filterByRect.lon2)) {
136 | filters.push(`rect:${filterByRect.lon1},${filterByRect.lat1},${filterByRect.lon2},${filterByRect.lat2}`);
137 | }
138 |
139 | if (filterByPlace) {
140 | filters.push(`place:${filterByPlace}`);
141 | }
142 |
143 |
144 | url += filters.length ? `&filter=${filters.join('|')}` : '';
145 |
146 | const bias = [];
147 | const biasByCountryCodes: ByCountryCodeOptions = options.bias[BY_COUNTRYCODE] as ByCountryCodeOptions;
148 | const biasByCircle: ByCircleOptions = options.bias[BY_CIRCLE] as ByCircleOptions;
149 | const biasByRect: ByRectOptions = options.bias[BY_RECT] as ByRectOptions;
150 | const biasByProximity: ByProximityOptions = options.bias[BY_PROXIMITY] as ByProximityOptions;
151 |
152 | if (biasByCountryCodes && biasByCountryCodes.length) {
153 | bias.push(`countrycode:${biasByCountryCodes.join(',').toLowerCase()}`);
154 | }
155 |
156 | if (biasByCircle && CalculationHelper.isLatitude(biasByCircle.lat) && CalculationHelper.isLongitude(biasByCircle.lon) && biasByCircle.radiusMeters > 0) {
157 | bias.push(`circle:${biasByCircle.lon},${biasByCircle.lat},${biasByCircle.radiusMeters}`);
158 | }
159 |
160 | if (biasByRect && CalculationHelper.isLatitude(biasByRect.lat1) && CalculationHelper.isLongitude(biasByRect.lon1) && CalculationHelper.isLatitude(biasByRect.lat2) && CalculationHelper.isLongitude(biasByRect.lon2)) {
161 | bias.push(`rect:${biasByRect.lon1},${biasByRect.lat1},${biasByRect.lon2},${biasByRect.lat2}`);
162 | }
163 |
164 | if (biasByProximity && CalculationHelper.isLatitude(biasByProximity.lat) && CalculationHelper.isLongitude(biasByProximity.lon)) {
165 | bias.push(`proximity:${biasByProximity.lon},${biasByProximity.lat}`);
166 | }
167 |
168 | url += bias.length ? `&bias=${bias.join('|')}` : '';
169 |
170 | return url;
171 | }
172 |
173 | public static returnIfFunction(func: any) {
174 | if(func && typeof func === 'function') {
175 | return func;
176 | } else {
177 | return null;
178 | }
179 | }
180 | }
--------------------------------------------------------------------------------
/src/helpers/callbacks.ts:
--------------------------------------------------------------------------------
1 | export class Callbacks {
2 | public changeCallbacks: ((selectedOption: any) => void)[] = [];
3 | public suggestionsChangeCallbacks: ((options: any[]) => void)[] = [];
4 | public inputCallbacks: ((input: string) => void)[] = [];
5 | public openCallbacks: ((opened: boolean) => void)[] = [];
6 | public closeCallbacks: ((opened: boolean) => void)[] = [];
7 |
8 | addCallback(operation: 'select' | 'suggestions' | 'input' | 'close' | 'open', callback: (param: any) => void) {
9 | let currentCallbacks = this.getCallbacksByOperation(operation);
10 | if(currentCallbacks) {
11 | if (currentCallbacks.indexOf(callback) < 0) {
12 | currentCallbacks.push(callback);
13 | }
14 | }
15 | }
16 |
17 | removeCallback(operation: 'select' | 'suggestions' | 'input' | 'close' | 'open', callback?: (param: any) => any) {
18 | let currentCallbacks = this.getCallbacksByOperation(operation);
19 | if(currentCallbacks) {
20 | if (currentCallbacks.indexOf(callback) >= 0) {
21 | currentCallbacks.splice(currentCallbacks.indexOf(callback), 1);
22 | this.setCallbacksByOperation(operation, currentCallbacks);
23 | } else if (!callback) {
24 | this.setCallbacksByOperation(operation, []);
25 | }
26 | }
27 | }
28 |
29 | notifyInputChange(currentValue: string) {
30 | this.inputCallbacks.forEach(callback => callback(currentValue));
31 | }
32 |
33 | notifyChange(feature: any){
34 | this.changeCallbacks.forEach(callback => callback(feature));
35 | }
36 |
37 | notifySuggestions(features: any) {
38 | this.suggestionsChangeCallbacks.forEach(callback => callback(features));
39 | }
40 |
41 | notifyOpened() {
42 | this.openCallbacks.forEach(callback => callback(true));
43 | }
44 |
45 | notifyClosed() {
46 | this.closeCallbacks.forEach(callback => callback(false));
47 | }
48 |
49 | private getCallbacksByOperation(operation: "select" | "suggestions" | "input" | "close" | "open") {
50 | let currentCallbacks = null;
51 | switch (operation) {
52 | case 'select': {
53 | currentCallbacks = this.changeCallbacks;
54 | break;
55 | }
56 | case 'suggestions': {
57 | currentCallbacks = this.suggestionsChangeCallbacks;
58 | break;
59 | }
60 | case 'input': {
61 | currentCallbacks = this.inputCallbacks;
62 | break;
63 | }
64 | case 'close': {
65 | currentCallbacks = this.closeCallbacks;
66 | break;
67 | }
68 | case 'open': {
69 | currentCallbacks = this.openCallbacks;
70 | break;
71 | }
72 | }
73 | return currentCallbacks;
74 | }
75 |
76 | private setCallbacksByOperation(operation: "select" | "suggestions" | "input" | "close" | "open",
77 | callbacks: ((data: any) => void)[]) {
78 | switch (operation) {
79 | case 'select': {
80 | this.changeCallbacks = callbacks;
81 | break;
82 | }
83 | case 'suggestions': {
84 | this.suggestionsChangeCallbacks = callbacks;
85 | break;
86 | }
87 | case 'input': {
88 | this.inputCallbacks = callbacks;
89 | break;
90 | }
91 | case 'close': {
92 | this.closeCallbacks = callbacks;
93 | break;
94 | }
95 | case 'open': {
96 | this.openCallbacks = callbacks;
97 | break;
98 | }
99 | }
100 | }
101 | }
--------------------------------------------------------------------------------
/src/helpers/constants.ts:
--------------------------------------------------------------------------------
1 | export const BY_COUNTRYCODE = 'countrycode';
2 | export const BY_RECT = 'rect';
3 | export const BY_CIRCLE = 'circle';
4 | export const BY_PROXIMITY = 'proximity';
5 | export const BY_PLACE = 'place';
6 |
--------------------------------------------------------------------------------
/src/helpers/dom.helper.ts:
--------------------------------------------------------------------------------
1 | import { GeocoderAutocompleteOptions } from "../autocomplete";
2 | import countiesData from "../countries.json";
3 |
4 | export class DomHelper {
5 | public static createInputElement(inputElement: HTMLInputElement,
6 | options: GeocoderAutocompleteOptions,
7 | container: HTMLElement) {
8 | inputElement.classList.add("geoapify-autocomplete-input");
9 | inputElement.setAttribute("type", "text");
10 | inputElement.setAttribute("placeholder", options.placeholder || "Enter an address here");
11 | container.appendChild(inputElement);
12 | }
13 |
14 | public static addFeatureIcon(element: HTMLElement, type: string, countryCode: string) {
15 | const iconMap: any = {
16 | 'unknown': 'map-marker',
17 | 'amenity': 'map-marker',
18 | 'building': 'map-marker',
19 | 'street': 'road',
20 | 'suburb': 'city',
21 | 'district': 'city',
22 | 'postcode': 'city',
23 | 'city': 'city',
24 | 'county': 'city',
25 | 'state': 'city'
26 | };
27 |
28 | const countryData = countiesData.find(county => countryCode && county.code.toLowerCase() === countryCode.toLowerCase());
29 |
30 | if ((type === 'country') && countryData) {
31 | element.classList.add("emoji");
32 | const emojiElement = document.createElement('span');
33 | emojiElement.innerText = countryData.emoji;
34 | element.appendChild(emojiElement);
35 | } else if (iconMap[type]) {
36 | this.addIcon(element, iconMap[type])
37 | } else {
38 | this.addIcon(element, 'map-marker');
39 | }
40 | }
41 |
42 | public static addIcon(element: HTMLElement, icon: string) {
43 |
44 | //FortAwesome icons 5. Licence - https://fontawesome.com/license/free
45 |
46 | const icons: { [key: string]: any } = {
47 | "close": {
48 | path: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
49 | viewbox: "0 0 24 24"
50 | },
51 | "map-marker": {
52 | path: "M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0zM192 272c44.183 0 80-35.817 80-80s-35.817-80-80-80-80 35.817-80 80 35.817 80 80 80z",
53 | viewbox: "0 0 384 512"
54 | },
55 | "road": {
56 | path: "M573.19 402.67l-139.79-320C428.43 71.29 417.6 64 405.68 64h-97.59l2.45 23.16c.5 4.72-3.21 8.84-7.96 8.84h-29.16c-4.75 0-8.46-4.12-7.96-8.84L267.91 64h-97.59c-11.93 0-22.76 7.29-27.73 18.67L2.8 402.67C-6.45 423.86 8.31 448 30.54 448h196.84l10.31-97.68c.86-8.14 7.72-14.32 15.91-14.32h68.8c8.19 0 15.05 6.18 15.91 14.32L348.62 448h196.84c22.23 0 36.99-24.14 27.73-45.33zM260.4 135.16a8 8 0 0 1 7.96-7.16h39.29c4.09 0 7.53 3.09 7.96 7.16l4.6 43.58c.75 7.09-4.81 13.26-11.93 13.26h-40.54c-7.13 0-12.68-6.17-11.93-13.26l4.59-43.58zM315.64 304h-55.29c-9.5 0-16.91-8.23-15.91-17.68l5.07-48c.86-8.14 7.72-14.32 15.91-14.32h45.15c8.19 0 15.05 6.18 15.91 14.32l5.07 48c1 9.45-6.41 17.68-15.91 17.68z",
57 | viewbox: "0 0 576 512"
58 | },
59 | "city": {
60 | path: "M616 192H480V24c0-13.26-10.74-24-24-24H312c-13.26 0-24 10.74-24 24v72h-64V16c0-8.84-7.16-16-16-16h-16c-8.84 0-16 7.16-16 16v80h-64V16c0-8.84-7.16-16-16-16H80c-8.84 0-16 7.16-16 16v80H24c-13.26 0-24 10.74-24 24v360c0 17.67 14.33 32 32 32h576c17.67 0 32-14.33 32-32V216c0-13.26-10.75-24-24-24zM128 404c0 6.63-5.37 12-12 12H76c-6.63 0-12-5.37-12-12v-40c0-6.63 5.37-12 12-12h40c6.63 0 12 5.37 12 12v40zm0-96c0 6.63-5.37 12-12 12H76c-6.63 0-12-5.37-12-12v-40c0-6.63 5.37-12 12-12h40c6.63 0 12 5.37 12 12v40zm0-96c0 6.63-5.37 12-12 12H76c-6.63 0-12-5.37-12-12v-40c0-6.63 5.37-12 12-12h40c6.63 0 12 5.37 12 12v40zm128 192c0 6.63-5.37 12-12 12h-40c-6.63 0-12-5.37-12-12v-40c0-6.63 5.37-12 12-12h40c6.63 0 12 5.37 12 12v40zm0-96c0 6.63-5.37 12-12 12h-40c-6.63 0-12-5.37-12-12v-40c0-6.63 5.37-12 12-12h40c6.63 0 12 5.37 12 12v40zm0-96c0 6.63-5.37 12-12 12h-40c-6.63 0-12-5.37-12-12v-40c0-6.63 5.37-12 12-12h40c6.63 0 12 5.37 12 12v40zm160 96c0 6.63-5.37 12-12 12h-40c-6.63 0-12-5.37-12-12v-40c0-6.63 5.37-12 12-12h40c6.63 0 12 5.37 12 12v40zm0-96c0 6.63-5.37 12-12 12h-40c-6.63 0-12-5.37-12-12v-40c0-6.63 5.37-12 12-12h40c6.63 0 12 5.37 12 12v40zm0-96c0 6.63-5.37 12-12 12h-40c-6.63 0-12-5.37-12-12V76c0-6.63 5.37-12 12-12h40c6.63 0 12 5.37 12 12v40zm160 288c0 6.63-5.37 12-12 12h-40c-6.63 0-12-5.37-12-12v-40c0-6.63 5.37-12 12-12h40c6.63 0 12 5.37 12 12v40zm0-96c0 6.63-5.37 12-12 12h-40c-6.63 0-12-5.37-12-12v-40c0-6.63 5.37-12 12-12h40c6.63 0 12 5.37 12 12v40z",
61 | viewbox: "0 0 640 512"
62 | }
63 | }
64 |
65 | var svgElement = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
66 | svgElement.setAttribute('viewBox', icons[icon].viewbox);
67 | svgElement.setAttribute('height', "24");
68 |
69 | var iconElement = document.createElementNS("http://www.w3.org/2000/svg", 'path');
70 | iconElement.setAttribute("d", icons[icon].path);
71 | iconElement.setAttribute('fill', 'currentColor');
72 | svgElement.appendChild(iconElement);
73 | element.appendChild(svgElement);
74 | }
75 |
76 | public static getStyledAddressSingleValue(value: string, currentValue: string): string {
77 | let displayValue = value;
78 |
79 | const valueIndex = (displayValue || '').toLowerCase().indexOf(currentValue.toLowerCase());
80 | if (valueIndex >= 0) {
81 | displayValue = displayValue.substring(0, valueIndex) +
82 | `${displayValue.substring(valueIndex, valueIndex + currentValue.length)} ` +
83 | displayValue.substring(valueIndex + currentValue.length);
84 | }
85 |
86 | return `${displayValue} `
87 | }
88 |
89 | public static getStyledAddress(featureProperties: any, currentValue: string): string {
90 | let mainPart: string;
91 | let secondaryPart: string;
92 | const parts = featureProperties.formatted.split(',').map((part: string) => part.trim());
93 |
94 | if (featureProperties.name) {
95 | mainPart = parts[0];
96 | secondaryPart = parts.slice(1).join(', ');
97 | } else {
98 | const mainElements = Math.min(2, Math.max(parts.length - 2, 1));
99 | mainPart = parts.slice(0, mainElements).join(', ');
100 | secondaryPart = parts.slice(mainElements).join(', ');
101 | }
102 |
103 | if (featureProperties.nonVerifiedParts && featureProperties.nonVerifiedParts.length) {
104 | featureProperties.nonVerifiedParts.forEach((part: string) => {
105 | mainPart = mainPart.replace(featureProperties[part], `${featureProperties[part]} `);
106 | });
107 | } else {
108 | const valueIndex = mainPart.toLowerCase().indexOf(currentValue.toLowerCase());
109 | if (valueIndex >= 0) {
110 | mainPart = mainPart.substring(0, valueIndex) +
111 | `${mainPart.substring(valueIndex, valueIndex + currentValue.length)} ` +
112 | mainPart.substring(valueIndex + currentValue.length);
113 |
114 | }
115 | }
116 |
117 | return `${mainPart} ${secondaryPart} `
118 | }
119 |
120 | public static addDropdownIcon(feature: any, itemElement: HTMLDivElement) {
121 | const iconElement = document.createElement("span");
122 | iconElement.classList.add('icon');
123 |
124 | DomHelper.addFeatureIcon(iconElement, feature.properties.result_type, feature.properties.country_code);
125 |
126 | itemElement.appendChild(iconElement);
127 | }
128 |
129 | public static addActiveClassToDropdownItem(items: HTMLCollectionOf, index: number) {
130 | for (var i = 0; i < items.length; i++) {
131 | items[i].classList.remove("active");
132 | }
133 |
134 | /* Add class "autocomplete-active" to the active element*/
135 | items[index].classList.add("active");
136 | }
137 |
138 | public static createDropdownItemText() {
139 | const textElement = document.createElement("span");
140 | textElement.classList.add('address');
141 | return textElement;
142 | }
143 |
144 | public static createDropdownItem() {
145 | const itemElement = document.createElement("div");
146 | itemElement.classList.add('geoapify-autocomplete-item');
147 | return itemElement;
148 | }
149 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export {GeocoderAutocomplete} from './autocomplete';
2 | export type {GeocoderAutocompleteOptions} from './autocomplete';
3 | export type {LocationType} from './autocomplete';
4 | export type {SupportedLanguage} from './autocomplete';
5 | export type {GeoPosition} from './autocomplete';
6 | export type {CountyCode} from './autocomplete';
7 | export type {ByCountryCodeOptions} from './autocomplete';
8 | export type {ByCircleOptions} from './autocomplete';
9 | export type {ByProximityOptions} from './autocomplete';
10 | export type {ByRectOptions} from './autocomplete';
--------------------------------------------------------------------------------
/styles/minimal-dark.css:
--------------------------------------------------------------------------------
1 | .geoapify-autocomplete-input {
2 | padding: 0 31px 0 7px;
3 | width: calc(100% - 40px);
4 |
5 | outline: none;
6 |
7 | line-height: 36px;
8 | height: 36px;
9 | border: 1px solid rgba(255, 255, 255, 0.2);
10 | background-color: rgba(255, 255, 255, 0.1);
11 | color: rgba(255, 255, 255, 0.8);
12 |
13 | font-size: 14px;
14 | }
15 |
16 | .geoapify-autocomplete-input::placeholder {
17 | color: rgba(255, 255, 255, 0.5);
18 | }
19 |
20 | .geoapify-autocomplete-items {
21 | position: absolute;
22 | border: 1px solid rgba(255, 255, 255, 0.2);
23 | border-top: none;
24 | background-color: #313131;
25 |
26 | color: rgba(255, 255, 255, 0.8);
27 |
28 | z-index: 99;
29 | top: 100%;
30 | left: 0;
31 | right: 0;
32 | }
33 |
34 | .geoapify-autocomplete-items div {
35 | padding: 10px;
36 | cursor: pointer;
37 | }
38 |
39 | .geoapify-autocomplete-items div:hover {
40 | background-color: rgba(255, 255, 255, 0.1);
41 | }
42 |
43 | .geoapify-autocomplete-items .active {
44 | background-color: rgba(255, 255, 255, 0.1);
45 | }
46 |
47 | .geoapify-autocomplete-item {
48 | display: flex;
49 | flex-direction: row;
50 | align-items: center;
51 | }
52 |
53 | .geoapify-autocomplete-item .icon {
54 | display: inline-block;
55 | width: 40px;
56 | height: 24px;
57 | color: #aaa;
58 | }
59 |
60 | .geoapify-autocomplete-item .icon.emoji {
61 | color: unset;
62 | font-size: 20px;
63 | opacity: 0.9;
64 | }
65 |
66 | .geoapify-close-button {
67 | position: absolute;
68 | right: 5px;
69 | top: 0;
70 |
71 | height: 100%;
72 | display: none;
73 | align-items: center;
74 | }
75 |
76 | .geoapify-close-button.visible {
77 | display: flex;
78 | }
79 |
80 | .geoapify-close-button {
81 | color: rgba(255, 255, 255, 0.4);
82 | cursor: pointer;
83 | }
84 |
85 | .geoapify-close-button:hover {
86 | color: rgba(255, 255, 255, 0.6);
87 | }
88 |
89 | .geoapify-autocomplete-items .secondary-part {
90 | margin-left: 10px;
91 | font-size: small;
92 | color: rgba(255, 255, 255, 0.6);
93 | }
94 |
95 | .geoapify-autocomplete-items .main-part .non-verified {
96 | color: #ff8080;
97 | }
--------------------------------------------------------------------------------
/styles/minimal.css:
--------------------------------------------------------------------------------
1 | .geoapify-autocomplete-input {
2 | padding: 0 31px 0 7px;
3 | width: calc(100% - 40px);
4 |
5 | outline: none;
6 |
7 | line-height: 36px;
8 | height: 36px;
9 | border: 1px solid rgba(0, 0, 0, 0.3);
10 |
11 | font-size: 14px;
12 | }
13 |
14 | .geoapify-autocomplete-items {
15 | position: absolute;
16 | border: 1px solid rgba(0, 0, 0, 0.3);
17 | border-top: none;
18 | background-color: #fff;
19 |
20 | z-index: 99;
21 | top: 100%;
22 | left: 0;
23 | right: 0;
24 | }
25 |
26 | .geoapify-autocomplete-items div {
27 | padding: 10px;
28 | cursor: pointer;
29 | }
30 |
31 | .geoapify-autocomplete-items div:hover {
32 | background-color: rgba(0, 0, 0, 0.1);
33 | }
34 |
35 | .geoapify-autocomplete-items .active {
36 | background-color: rgba(0, 0, 0, 0.1);
37 | }
38 |
39 | .geoapify-autocomplete-item {
40 | display: flex;
41 | flex-direction: row;
42 | align-items: center;
43 | }
44 |
45 | .geoapify-autocomplete-item .icon {
46 | display: inline-block;
47 | width: 40px;
48 | height: 24px;
49 | color: #aaa;
50 | }
51 |
52 | .geoapify-autocomplete-item .icon.emoji {
53 | color: unset;
54 | font-size: 20px;
55 | opacity: 0.9;
56 | }
57 |
58 | .geoapify-close-button {
59 | position: absolute;
60 | right: 5px;
61 | top: 0;
62 |
63 | height: 100%;
64 | display: none;
65 | align-items: center;
66 | }
67 |
68 | .geoapify-close-button.visible {
69 | display: flex;
70 | }
71 |
72 | .geoapify-close-button {
73 | color: rgba(0, 0, 0, 0.4);
74 | cursor: pointer;
75 | }
76 |
77 | .geoapify-close-button:hover {
78 | color: rgba(0, 0, 0, 0.6);
79 | }
80 |
81 | .geoapify-autocomplete-items .main-part .non-verified {
82 | color: #ff4848;
83 | }
84 |
85 | .geoapify-autocomplete-items .secondary-part {
86 | margin-left: 10px;
87 | font-size: small;
88 | color: rgba(0, 0, 0, 0.6);
89 | }
--------------------------------------------------------------------------------
/styles/round-borders-dark.css:
--------------------------------------------------------------------------------
1 | .geoapify-autocomplete-input {
2 | padding: 0 31px 0 7px;
3 | width: calc(100% - 40px);
4 |
5 | outline: none;
6 |
7 | line-height: 36px;
8 | height: 36px;
9 | border: 1px solid rgba(255, 255, 255, 0.2);
10 | border-radius: 5px;
11 |
12 | background-color: rgba(255, 255, 255, 0.1);
13 | color: rgba(255, 255, 255, 0.8);
14 |
15 | font-size: 14px;
16 | }
17 |
18 | .geoapify-autocomplete-input::placeholder {
19 | color: rgba(255, 255, 255, 0.5);
20 | }
21 |
22 | .geoapify-autocomplete-items {
23 | position: absolute;
24 | top: calc(100% + 2px);
25 | left: 0;
26 | right: 0;
27 |
28 | border: 1px solid rgba(255, 255, 255, 0.1);
29 | border-radius: 5px;
30 | overflow: hidden;
31 |
32 | background-color: #313131;
33 | color: rgba(255, 255, 255, 0.8);
34 | box-shadow: 0px 4px 10px 2px rgba(255, 255, 255, 0.1);
35 |
36 | z-index: 99;
37 | }
38 |
39 | .geoapify-autocomplete-items div {
40 | padding: 10px;
41 | cursor: pointer;
42 | }
43 |
44 | .geoapify-autocomplete-items div:hover {
45 | background-color: rgba(255, 255, 255, 0.1);
46 | }
47 |
48 | .geoapify-autocomplete-items .active {
49 | background-color: rgba(255, 255, 255, 0.1);
50 | }
51 |
52 | .geoapify-autocomplete-item {
53 | display: flex;
54 | flex-direction: row;
55 | align-items: center;
56 | }
57 |
58 | .geoapify-autocomplete-item .icon {
59 | display: inline-block;
60 | width: 40px;
61 | height: 24px;
62 | color: #aaa;
63 | }
64 |
65 | .geoapify-autocomplete-item .icon.emoji {
66 | color: unset;
67 | font-size: 20px;
68 | opacity: 0.9;
69 | }
70 |
71 | .geoapify-close-button {
72 | position: absolute;
73 | right: 5px;
74 | top: 0;
75 |
76 | height: 100%;
77 | display: none;
78 | align-items: center;
79 | }
80 |
81 | .geoapify-close-button.visible {
82 | display: flex;
83 | }
84 |
85 | .geoapify-close-button {
86 | color: rgba(255, 255, 255, 0.4);
87 | cursor: pointer;
88 | }
89 |
90 | .geoapify-close-button:hover {
91 | color: rgba(255, 255, 255, 0.6);
92 | }
93 |
94 | .geoapify-autocomplete-items .secondary-part {
95 | margin-left: 10px;
96 | font-size: small;
97 | color: rgba(255, 255, 255, 0.6);
98 | }
99 |
100 | .geoapify-autocomplete-items .main-part .non-verified {
101 | color: #ff8080;
102 | }
--------------------------------------------------------------------------------
/styles/round-borders.css:
--------------------------------------------------------------------------------
1 | .geoapify-autocomplete-input {
2 | outline: none;
3 | line-height: 36px;
4 | height: 36px;
5 | padding: 0 31px 0 7px;
6 | width: calc(100% - 40px);
7 |
8 | border: 1px solid rgba(0, 0, 0, 0.2);
9 | border-radius: 5px;
10 |
11 | font-size: 14px;
12 | }
13 |
14 | .geoapify-autocomplete-items {
15 | position: absolute;
16 | top: calc(100% + 2px);
17 | left: 0;
18 | right: 0;
19 |
20 | background-color: #fff;
21 | border: 1px solid rgba(0, 0, 0, 0.1);
22 | border-radius: 5px;
23 | overflow: hidden;
24 |
25 | box-shadow: 0px 2px 10px 2px rgba(0, 0, 0, 0.1);
26 | z-index: 99;
27 | }
28 |
29 | .geoapify-autocomplete-items div {
30 | padding: 10px;
31 | cursor: pointer;
32 | }
33 |
34 | .geoapify-autocomplete-items div:hover {
35 | background-color: rgba(0, 0, 0, 0.1);
36 | }
37 |
38 | .geoapify-autocomplete-items .active {
39 | background-color: rgba(0, 0, 0, 0.1);
40 | }
41 |
42 | .geoapify-autocomplete-item {
43 | display: flex;
44 | flex-direction: row;
45 | align-items: center;
46 | }
47 |
48 | .geoapify-autocomplete-item .icon {
49 | display: inline-block;
50 | width: 40px;
51 | height: 24px;
52 | color: #aaa;
53 | }
54 |
55 | .geoapify-autocomplete-item .icon.emoji {
56 | color: unset;
57 | font-size: 20px;
58 | opacity: 0.9;
59 | }
60 |
61 | .geoapify-close-button {
62 | position: absolute;
63 | right: 5px;
64 | top: 0;
65 |
66 | height: 100%;
67 | display: none;
68 | align-items: center;
69 | }
70 |
71 | .geoapify-close-button.visible {
72 | display: flex;
73 | }
74 |
75 | .geoapify-close-button {
76 | color: rgba(0, 0, 0, 0.4);
77 | cursor: pointer;
78 | }
79 |
80 | .geoapify-close-button:hover {
81 | color: rgba(0, 0, 0, 0.6);
82 | }
83 |
84 | .geoapify-autocomplete-items .main-part .non-verified {
85 | color: #ff4848;
86 | }
87 |
88 | .geoapify-autocomplete-items .secondary-part {
89 | margin-left: 10px;
90 | font-size: small;
91 | color: rgba(0, 0, 0, 0.6);
92 | }
--------------------------------------------------------------------------------
/tests/geocoder-autocomplete.test.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { GeocoderAutocomplete, GeocoderAutocompleteOptions } from "../src";
3 | import fetchMock from 'jest-fetch-mock';
4 | import {
5 | addSelectSpy, addSuggestionsSpy,
6 | APP_URL,
7 | checkIfClearButtonInitialized,
8 | checkIfInputInitialized,
9 | clickOutside,
10 | createGeocoderAutocomplete,
11 | expectDropdownIsClosed,
12 | getDropDownItem,
13 | getDropDownItemValue,
14 | getPrivateProperty,
15 | inputText, inputTextWithEvent,
16 | inputValueAndDontExpectTheRequest,
17 | inputValueAndExpectTheRequest,
18 | inputValueAndPopulateDropdown,
19 | inputValueAndReturnResponse,
20 | reset,
21 | selectDropdownItem,
22 | wait, WAIT_TIME
23 | } from "./test-helper";
24 | import {
25 | mockEmptyResponse,
26 | mockResponseWithData,
27 | mockResponseWithData2,
28 | mockResponseWithDataOSM, mockResponseWithDataParsed, mockResponseWithDataParsedWithoutHouseNumber,
29 | options
30 | } from "./test-data";
31 |
32 | fetchMock.enableMocks();
33 |
34 | describe('GeocoderAutocomplete', () => {
35 | let container = document.createElement('div');
36 | let autocomplete = createGeocoderAutocomplete(container);
37 |
38 | beforeEach(() => {
39 | if(autocomplete) {
40 | reset(autocomplete);
41 | }
42 | });
43 |
44 | it('should init component properly', () => {
45 | let autocompleteTest = createGeocoderAutocomplete(container);
46 | expect(autocompleteTest).toBeDefined();
47 | let initiatedOptions: GeocoderAutocompleteOptions = getPrivateProperty(autocompleteTest, "options")
48 | expect(initiatedOptions.filter).toBe(options.filter);
49 | expect(initiatedOptions.bias).toBe(options.bias);
50 |
51 | checkIfInputInitialized(container);
52 | checkIfClearButtonInitialized(container);
53 | });
54 | it('should not open dropdown when user inputs 2 digits', async () => {
55 | fetchMock.mockResponseOnce(JSON.stringify(mockEmptyResponse));
56 |
57 | inputText(container, "12");
58 |
59 | await wait(WAIT_TIME);
60 |
61 | expect(fetchMock).toHaveBeenCalledWith(
62 | `${APP_URL}?text=12&apiKey=XXXXX&limit=5`
63 | );
64 | // expect the dropdown to be null
65 | expectDropdownIsClosed(container);
66 | });
67 | it('should open dropdown when user inputs 3 digits', async () => {
68 | await inputValueAndPopulateDropdown(container);
69 |
70 | // Expect the dropdown element to be created and attached to the input
71 | const dropdown = container.querySelector('.geoapify-autocomplete-items');
72 |
73 | // Check if the dropdown contains items (the geocoder API response should populate this)
74 | const items = dropdown?.querySelectorAll('.geoapify-autocomplete-item');
75 | expect(items?.length).toBe(2);
76 |
77 | // Optional: Check if the first item in the dropdown contains expected text
78 | if (items && items.length > 0) {
79 | const firstItemText = items[0].querySelector('.address')?.textContent;
80 | expect(firstItemText).toContain('123 Main St');
81 | }
82 | });
83 | it('should clear the input/hide dropdown after clicking on clear icon', async () => {
84 | await inputValueAndPopulateDropdown(container);
85 | const clearButton = container.querySelector('.geoapify-close-button') as HTMLElement;
86 | if (clearButton) {
87 | clearButton.click();
88 | } else {
89 | throw new Error('Clear button not found');
90 | }
91 | await wait(WAIT_TIME);
92 | expectDropdownIsClosed(container);
93 | expect(autocomplete.getValue()).toBe('')
94 | });
95 | it('should hide the dropdown if we click outside', async () => {
96 | await inputValueAndPopulateDropdown(container);
97 | clickOutside();
98 | await wait(WAIT_TIME);
99 | expectDropdownIsClosed(container);
100 | expect(autocomplete.getValue()).toBe('123')
101 | });
102 | it('changeCallbacks is triggered properly', async () => {
103 | // testing on('select', x)
104 | const selectSpy = addSelectSpy(autocomplete);
105 | await inputValueAndPopulateDropdown(container);
106 | selectDropdownItem(container, 0);
107 | expect(autocomplete.getValue()).toBe(mockResponseWithData.features[0].text);
108 | expect(selectSpy).toHaveBeenNthCalledWith(1, mockResponseWithData.features[0]);
109 | // testing off('select', x)
110 | autocomplete.off('select', selectSpy);
111 | await inputValueAndPopulateDropdown(container);
112 | selectDropdownItem(container, 1);
113 | expect(selectSpy).toHaveBeenCalledTimes(1);
114 | expect(autocomplete.getValue()).toBe(mockResponseWithData.features[1].text);
115 |
116 | // testing once('select', x)
117 | autocomplete.once('select', selectSpy);
118 | await inputValueAndPopulateDropdown(container);
119 | selectDropdownItem(container, 0);
120 | expect(autocomplete.getValue()).toBe(mockResponseWithData.features[0].text);
121 | expect(selectSpy).toHaveBeenNthCalledWith(2, mockResponseWithData.features[0]);
122 |
123 | await inputValueAndPopulateDropdown(container);
124 | selectDropdownItem(container, 1);
125 | expect(selectSpy).toHaveBeenCalledTimes(2);
126 |
127 | });
128 | it('suggestionsChangeCallbacks is triggered properly', async () => {
129 | // testing on('suggestions', x)
130 | const suggestionChangeSpy = addSuggestionsSpy(autocomplete);
131 | await inputValueAndPopulateDropdown(container);
132 | selectDropdownItem(container, 0);
133 | expect(autocomplete.getValue()).toBe(mockResponseWithData.features[0].text);
134 | expect(suggestionChangeSpy).toHaveBeenNthCalledWith(1, mockResponseWithData.features);
135 | // testing off('suggestions', x)
136 | autocomplete.off('suggestions', suggestionChangeSpy);
137 | await inputValueAndPopulateDropdown(container);
138 | selectDropdownItem(container, 1);
139 | expect(suggestionChangeSpy).toHaveBeenCalledTimes(1);
140 | expect(autocomplete.getValue()).toBe(mockResponseWithData.features[1].text);
141 |
142 | // testing once('suggestions', x)
143 | autocomplete.once('suggestions', suggestionChangeSpy);
144 | await inputValueAndPopulateDropdown(container);
145 | selectDropdownItem(container, 0);
146 | expect(autocomplete.getValue()).toBe(mockResponseWithData.features[0].text);
147 | expect(suggestionChangeSpy).toHaveBeenNthCalledWith(2, mockResponseWithData.features);
148 |
149 | await inputValueAndPopulateDropdown(container);
150 | selectDropdownItem(container, 1);
151 | expect(suggestionChangeSpy).toHaveBeenCalledTimes(2);
152 | });
153 | it('inputCallbacks is triggered properly', async () => {
154 | // testing on('input', x)
155 | const inputChangeSpy = jest.fn();
156 | autocomplete.on('input', inputChangeSpy);
157 | await inputValueAndPopulateDropdown(container);
158 | expect(autocomplete.getValue()).toBe("123");
159 | expect(inputChangeSpy).toHaveBeenNthCalledWith(1, "123");
160 | // testing off('input', x)
161 | autocomplete.off('input', inputChangeSpy);
162 | await inputValueAndPopulateDropdown(container);
163 | expect(inputChangeSpy).toHaveBeenCalledTimes(1);
164 | expect(autocomplete.getValue()).toBe("123");
165 |
166 | // testing once('input', x)
167 | autocomplete.once('input', inputChangeSpy);
168 | await inputValueAndPopulateDropdown(container);
169 | expect(autocomplete.getValue()).toBe("123");
170 | expect(inputChangeSpy).toHaveBeenNthCalledWith(2, "123");
171 |
172 | await inputValueAndPopulateDropdown(container);
173 | expect(inputChangeSpy).toHaveBeenCalledTimes(2);
174 | });
175 | it('closeCallbacks is triggered properly', async () => {
176 | // testing on('close', x)
177 | autocomplete.setValue("");
178 | if(autocomplete.isOpen()) {
179 | autocomplete.close();
180 | }
181 | const closeSpy = jest.fn();
182 | autocomplete.on('close', closeSpy);
183 | await inputValueAndPopulateDropdown(container);
184 | selectDropdownItem(container, 0);
185 | expect(closeSpy).toHaveBeenNthCalledWith(1, false);
186 | expect(closeSpy).toHaveBeenCalledTimes(1);
187 | // testing off('close', x)
188 | autocomplete.off('close', closeSpy);
189 | await inputValueAndPopulateDropdown(container);
190 | selectDropdownItem(container, 0);
191 | expect(closeSpy).toHaveBeenCalledTimes(1);
192 |
193 | // testing once('close', x)
194 | autocomplete.once('close', closeSpy);
195 | await inputValueAndPopulateDropdown(container);
196 | selectDropdownItem(container, 0);
197 | expect(closeSpy).toHaveBeenNthCalledWith(2, false);
198 |
199 | await inputValueAndPopulateDropdown(container);
200 | selectDropdownItem(container, 0);
201 | expect(closeSpy).toHaveBeenCalledTimes(2);
202 | });
203 | it('openCallbacks is triggered properly', async () => {
204 | // testing on('open', x)
205 | const openSpy = jest.fn();
206 | autocomplete.on('open', openSpy);
207 | await inputValueAndPopulateDropdown(container);
208 | selectDropdownItem(container, 0);
209 | expect(openSpy).toHaveBeenNthCalledWith(1, true);
210 | // testing off('open', x)
211 | autocomplete.off('open', openSpy);
212 | await inputValueAndPopulateDropdown(container);
213 | selectDropdownItem(container, 0);
214 | expect(openSpy).toHaveBeenCalledTimes(1);
215 |
216 | // testing once('open', x)
217 | autocomplete.once('open', openSpy);
218 | await inputValueAndPopulateDropdown(container);
219 | selectDropdownItem(container, 0);
220 | expect(openSpy).toHaveBeenNthCalledWith(2, true);
221 |
222 | await inputValueAndPopulateDropdown(container);
223 | selectDropdownItem(container, 0);
224 | expect(openSpy).toHaveBeenCalledTimes(2);
225 | });
226 | it('addFilterByCountry should work properly', async () => {
227 | autocomplete.addFilterByCountry(['ae']);
228 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5&filter=countrycode:ae`);
229 | autocomplete.addFilterByCountry([]);
230 | });
231 | it('addFilterByCircle should work properly', async () => {
232 | autocomplete.addFilterByCircle({
233 | lon: 30,
234 | lat: 40,
235 | radiusMeters: 40
236 | });
237 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5&filter=circle:30,40,40`);
238 | });
239 | it('addFilterByRect should work properly', async () => {
240 | autocomplete.addFilterByRect({
241 | lon1: 40,
242 | lat1: 40,
243 | lon2: 40,
244 | lat2: 40
245 | });
246 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5&filter=rect:40,40,40,40`);
247 | });
248 | it('addFilterByPlace should work properly', async () => {
249 | autocomplete.addFilterByPlace("placeX");
250 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5&filter=place:placeX`);
251 | });
252 | it('addBiasByCountry should work properly', async () => {
253 | autocomplete.addBiasByCountry(['ae']);
254 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5&bias=countrycode:ae`);
255 | });
256 | it('addBiasByCircle should work properly', async () => {
257 | autocomplete.addBiasByCircle({
258 | lon: 30,
259 | lat: 40,
260 | radiusMeters: 40
261 | });
262 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5&bias=circle:30,40,40`);
263 | });
264 | it('addBiasByRect should work properly', async () => {
265 | autocomplete.addBiasByRect({
266 | lon1: 40,
267 | lat1: 40,
268 | lon2: 40,
269 | lat2: 40
270 | });
271 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5&bias=rect:40,40,40,40`);
272 | });
273 | it('addBiasByProximity should work properly', async () => {
274 | autocomplete.addBiasByProximity({
275 | lon: 10,
276 | lat: 20
277 | });
278 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5&bias=proximity:10,20`);
279 | autocomplete.clearBias();
280 | });
281 | it('open/close works properly', async () => {
282 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
283 | autocomplete.close();
284 | await wait(WAIT_TIME);
285 | expectDropdownIsClosed(container);
286 | expect(autocomplete.getValue()).toBe('123')
287 |
288 | autocomplete.open();
289 | expect(container.querySelector('.geoapify-autocomplete-items')).toBeDefined();
290 |
291 | autocomplete.close();
292 | expectDropdownIsClosed(container);
293 | });
294 | it('setSuggestionsFilter works properly', async () => {
295 | const suggestionChangeSpy = addSuggestionsSpy(autocomplete);
296 |
297 | autocomplete.setSuggestionsFilter((items) => items.filter(item => item.properties.formatted.includes("Main")));
298 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
299 |
300 | expect(suggestionChangeSpy).toHaveBeenCalledTimes(1);
301 | expect(suggestionChangeSpy).toHaveBeenNthCalledWith(1, mockResponseWithData.features.filter(item => item.properties.formatted.includes("Main")));
302 |
303 | suggestionChangeSpy.mockReset();
304 | autocomplete.setSuggestionsFilter(null);
305 |
306 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
307 | expect(suggestionChangeSpy).toHaveBeenCalledTimes(1);
308 | expect(suggestionChangeSpy).toHaveBeenNthCalledWith(1, mockResponseWithData.features);
309 | });
310 | it('setPreprocessHook works properly', async () => {
311 | autocomplete.setPreprocessHook(item => item + "_test");
312 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123_test&apiKey=XXXXX&limit=5`);
313 |
314 | autocomplete.setPreprocessHook(null);
315 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
316 | });
317 | it('setPostprocessHook works properly', async () => {
318 | autocomplete.setPostprocessHook((feature: any) => {
319 | return "test_" + feature.properties.formatted;
320 | });
321 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
322 | expect(getDropDownItemValue(container, 0)).toBe("test_123 Main St");
323 | autocomplete.setPostprocessHook(null);
324 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
325 | expect(getDropDownItemValue(container, 0)).toBe("123 Main St");
326 | });
327 | it('setSendGeocoderRequestFunc works properly', async () => {
328 | autocomplete.setSendGeocoderRequestFunc((value: string, geocoderAutocomplete: GeocoderAutocomplete) => {
329 | return new Promise((resolve) => {
330 | resolve(mockResponseWithData2);
331 | });
332 | });
333 | await inputValueAndDontExpectTheRequest(container);
334 | expect(getDropDownItemValue(container, 0)).toBe("555 Main St");
335 |
336 | autocomplete.setSendGeocoderRequestFunc(null);
337 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
338 | expect(getDropDownItemValue(container, 0)).toBe("123 Main St");
339 | });
340 | it('setSendPlaceDetailsRequestFunc works properly', async () => {
341 | autocomplete.setAddDetails(true);
342 | const selectSpy = addSelectSpy(autocomplete);
343 |
344 | autocomplete.setSendPlaceDetailsRequestFunc((value: string, geocoderAutocomplete: GeocoderAutocomplete) => {
345 | return new Promise((resolve) => {
346 | resolve(mockResponseWithData2);
347 | });
348 | });
349 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
350 | selectDropdownItem(container, 0);
351 | await wait(WAIT_TIME);
352 | expect(selectSpy).toHaveBeenNthCalledWith(1, mockResponseWithData2);
353 |
354 | selectSpy.mockReset();
355 | autocomplete.setSendPlaceDetailsRequestFunc(null);
356 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
357 | selectDropdownItem(container, 0);
358 | await wait(WAIT_TIME);
359 | expect(selectSpy).toHaveBeenNthCalledWith(1, mockResponseWithData.features[0]);
360 | autocomplete.setAddDetails(false);
361 | });
362 | it('sendPlaceDetailsRequest works properly', async () => {
363 | autocomplete.setAddDetails(true);
364 | fetchMock.resetMocks();
365 |
366 | const selectSpy = addSelectSpy(autocomplete);
367 | fetchMock.mockResponseOnce(JSON.stringify(mockResponseWithDataOSM));
368 | fetchMock.mockResponseOnce(JSON.stringify(mockResponseWithDataOSM));
369 |
370 | autocomplete.setSendPlaceDetailsRequestFunc(null);
371 | inputText(container, "123");
372 | await wait(WAIT_TIME);
373 |
374 | selectDropdownItem(container, 0);
375 | await wait(WAIT_TIME);
376 | expect(selectSpy).toHaveBeenNthCalledWith(1, mockResponseWithDataOSM.features[0]);
377 | expect(fetchMock).toHaveBeenCalledWith("https://api.geoapify.com/v2/place-details?id=placeId&apiKey=XXXXX");
378 | autocomplete.setAddDetails(false);
379 | });
380 | it('addFeatureIcon works properly', async () => {
381 | autocomplete.setSkipIcons(false);
382 |
383 | fetchMock.resetMocks();
384 |
385 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
386 |
387 | const dropdownItem = getDropDownItem(container, 0);
388 | expect(dropdownItem.querySelector(".icon").innerHTML).toContain('M573.19');
389 | autocomplete.setSkipIcons(true);
390 |
391 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
392 | const dropdownItem2 = getDropDownItem(container, 0);
393 | expect(dropdownItem2.querySelector(".icon")).toBeNull();
394 | });
395 | it('extendByNonVerifiedValues works properly', async () => {
396 | autocomplete.setAllowNonVerifiedHouseNumber(true);
397 | autocomplete.setAllowNonVerifiedStreet(true);
398 |
399 | fetchMock.resetMocks();
400 |
401 | const suggestionChangeSpy = addSuggestionsSpy(autocomplete);
402 | await inputValueAndReturnResponse(container, mockResponseWithDataParsed);
403 | selectDropdownItem(container, 0);
404 |
405 | expect(suggestionChangeSpy).toHaveBeenCalled();
406 | const calls = suggestionChangeSpy.mock.calls;
407 | const properties = calls[0][0];
408 | const item1 = properties[0];
409 | const item2 = properties[1];
410 | expect(item1.properties.housenumber).toBe("test_housenumber");
411 | expect(item1.properties.nonVerifiedParts).toStrictEqual(["housenumber"]);
412 | expect(item2.properties.formatted).toBe("test_housenumber Test_street, 123 Elm St");
413 | expect(item2.properties.address_line1).toBe("test_housenumber Test_street");
414 | expect(item2.properties.street).toBe("Test_street");
415 | expect(item2.properties.nonVerifiedParts).toStrictEqual(["housenumber", "street"]);
416 |
417 | autocomplete.setAllowNonVerifiedHouseNumber(false);
418 | autocomplete.setAllowNonVerifiedStreet(false);
419 | });
420 | it('extendByNonVerifiedValues works properly without housenumber', async () => {
421 | autocomplete.setAllowNonVerifiedHouseNumber(true);
422 | autocomplete.setAllowNonVerifiedStreet(true);
423 |
424 | fetchMock.resetMocks();
425 |
426 | const suggestionChangeSpy = addSuggestionsSpy(autocomplete);
427 | await inputValueAndReturnResponse(container, mockResponseWithDataParsedWithoutHouseNumber);
428 | selectDropdownItem(container, 0);
429 |
430 | expect(suggestionChangeSpy).toHaveBeenCalled();
431 | const calls = suggestionChangeSpy.mock.calls;
432 | const properties = calls[0][0];
433 | const item2 = properties[1];
434 | expect(item2.properties.formatted).toBe("Test_street, 123 Elm St");
435 | expect(item2.properties.address_line1).toBe("Test_street");
436 | expect(item2.properties.street).toBe("Test_street");
437 | expect(item2.properties.nonVerifiedParts).toStrictEqual(["street"]);
438 |
439 | autocomplete.setAllowNonVerifiedHouseNumber(false);
440 | autocomplete.setAllowNonVerifiedStreet(false);
441 | });
442 | it('onUserKeyPress works properly', async () => {
443 | autocomplete.setAllowNonVerifiedHouseNumber(true);
444 | autocomplete.setAllowNonVerifiedStreet(true);
445 |
446 | fetchMock.resetMocks();
447 |
448 | fetchMock.mockResponseOnce(JSON.stringify(mockResponseWithData));
449 |
450 | inputTextWithEvent(container, "123", "ArrowDown");
451 | await wait(WAIT_TIME);
452 | expect(getDropDownItemValue(container, 0)).toBe("123 Main St");
453 |
454 | inputTextWithEvent(container, "123", "Escape");
455 | expectDropdownIsClosed(container);
456 |
457 | fetchMock.mockResponseOnce(JSON.stringify(mockResponseWithData));
458 | inputTextWithEvent(container, "123", "ArrowDown");
459 | await wait(WAIT_TIME);
460 | expect(getDropDownItemValue(container, 0)).toBe("123 Main St");
461 |
462 | const selectSpy = addSelectSpy(autocomplete);
463 | inputTextWithEvent(container, "123", "ArrowDown");
464 | inputTextWithEvent(container, "123", "ArrowDown");
465 | inputTextWithEvent(container, "123", "ArrowUp");
466 | inputTextWithEvent(container, "123", "Enter");
467 | await wait(WAIT_TIME);
468 |
469 | expect(selectSpy).toHaveBeenNthCalledWith(1, mockResponseWithData.features[0]);
470 | expect(selectSpy).toHaveBeenNthCalledWith(2, mockResponseWithData.features[1]);
471 | expect(selectSpy).toHaveBeenNthCalledWith(3, mockResponseWithData.features[0]);
472 |
473 | expectDropdownIsClosed(container);
474 | });
475 | it('setCountryCodes should log warning', async () => {
476 | const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
477 |
478 | autocomplete.setCountryCodes(['ae']);
479 |
480 | expect(warnSpy).toHaveBeenCalledWith('WARNING! Obsolete function called. Function setCountryCodes() has been deprecated, please use the new addFilterByCountry() function instead!');
481 | });
482 | it('setPosition should log warning', async () => {
483 | const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
484 |
485 | autocomplete.setPosition({
486 | lat: 0,
487 | lon: 0
488 | });
489 |
490 | expect(warnSpy).toHaveBeenCalledWith('WARNING! Obsolete function called. Function setPosition() has been deprecated, please use the new addBiasByProximity() function instead!');
491 | });
492 | it('setType should work properly', async () => {
493 | autocomplete.setType('postcode');
494 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&type=postcode&limit=5`);
495 | autocomplete.setType(null);
496 | });
497 | it('setLang should work properly', async () => {
498 | autocomplete.setLang('ab');
499 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5&lang=ab`);
500 | autocomplete.setLang(null);
501 | });
502 | it('setGeocoderUrl works as expected', async () => {
503 | autocomplete.setGeocoderUrl("https://api.geoapify.com/v2/geocode/autocomplete")
504 | await inputValueAndExpectTheRequest(container, `https://api.geoapify.com/v2/geocode/autocomplete?text=123&apiKey=XXXXX&limit=5`);
505 | autocomplete.setGeocoderUrl(APP_URL)
506 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
507 | });
508 | it('setPlaceDetailsUrl works as expected', async () => {
509 | autocomplete.setPlaceDetailsUrl("https://api.geoapify.com/v3/place-details")
510 | autocomplete.setAddDetails(true);
511 | fetchMock.resetMocks();
512 |
513 | const selectSpy = addSelectSpy(autocomplete);
514 | fetchMock.mockResponseOnce(JSON.stringify(mockResponseWithDataOSM));
515 | fetchMock.mockResponseOnce(JSON.stringify(mockResponseWithDataOSM));
516 |
517 | autocomplete.setSendPlaceDetailsRequestFunc(null);
518 | inputText(container, "123");
519 | await wait(WAIT_TIME);
520 |
521 | selectDropdownItem(container, 0);
522 | await wait(WAIT_TIME);
523 | expect(selectSpy).toHaveBeenNthCalledWith(1, mockResponseWithDataOSM.features[0]);
524 | expect(fetchMock).toHaveBeenCalledWith("https://api.geoapify.com/v3/place-details?id=placeId&apiKey=XXXXX");
525 | autocomplete.setAddDetails(false);
526 | });
527 | });
528 |
--------------------------------------------------------------------------------
/tests/test-data.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 |
3 | export const options: any = {
4 | skipDetails: true,
5 | skipIcons: true,
6 | placeholder: "Search location",
7 | filter: {
8 | byCircle: {
9 | lon: 10,
10 | lat: 20,
11 | radiusMeters: 30
12 | },
13 | byCountryCode: ['am'],
14 | byRect: {
15 | lon1: 10,
16 | lat1: 20,
17 | lon2: 30,
18 | lat2: 40
19 | },
20 | customFilter: 'example string filter'
21 | },
22 | bias: {
23 | byCircle: {
24 | lon: 10,
25 | lat: 20,
26 | radiusMeters: 30
27 | },
28 | byCountryCode: ['am'],
29 | byRect: {
30 | lon1: 10,
31 | lat1: 20,
32 | lon2: 30,
33 | lat2: 40
34 | },
35 | byProximity: {
36 | lon: 10,
37 | lat: 20
38 | },
39 | },
40 | countryCodes: ['ad'],
41 | position: {
42 | lon: 10,
43 | lat: 20
44 | }
45 | };
46 |
47 | export const mockResponseWithData = {
48 | features: [
49 | { properties: { result_type: 'street', country_code: 'ad', formatted: '123 Main St' }, text: '123 Main St' },
50 | { properties: { result_type: 'street', country_code: 'ad', formatted: '123 Elm St' }, text: '123 Elm St' }
51 | ]
52 | };
53 |
54 | export const mockResponseWithData2 = {
55 | features: [
56 | { properties: { result_type: 'street', country_code: 'ad', formatted: '555 Main St' }, text: '555 Main St' }
57 | ]
58 | };
59 |
60 | export const mockResponseWithDataOSM = {
61 | features: [
62 | { properties: { result_type: 'street', country_code: 'ad', formatted: '123 Main St', datasource: {sourcename: 'openstreetmap'}, place_id: 'placeId'}, text: '123 Main St'},
63 | ]
64 | };
65 |
66 | export const mockResponseWithDataParsed = {
67 | features: [
68 | { properties: { result_type: 'street', country_code: 'ad', formatted: '123 Main St', rank: {match_type: 'match_by_street'}, address_line1: 'address line 1'}, text: '123 Main St' },
69 | { properties: { result_type: 'street', country_code: 'ad', formatted: '123 Elm St' , rank: {match_type: 'match_by_city_or_disrict'}, address_line1: 'address lin 1'}, text: '123 Elm St' }
70 | ],
71 | query: {
72 | parsed: {
73 | housenumber: 'test_housenumber',
74 | street: 'test_street'
75 | }
76 | }
77 | };
78 |
79 | export const mockResponseWithDataParsedWithoutHouseNumber = {
80 | features: [
81 | { properties: { result_type: 'street', country_code: 'ad', formatted: '123 Main St', rank: {match_type: 'match_by_street'}, address_line1: 'address line 1'}, text: '123 Main St' },
82 | { properties: { result_type: 'street', country_code: 'ad', formatted: '123 Elm St' , rank: {match_type: 'match_by_city_or_disrict'}, address_line1: 'address lin 1'}, text: '123 Elm St' }
83 | ],
84 | query: {
85 | parsed: {
86 | street: 'test_street'
87 | }
88 | }
89 | };
90 |
91 | export const mockEmptyResponse = {
92 | features: [] as string[]
93 | };
--------------------------------------------------------------------------------
/tests/test-helper.ts:
--------------------------------------------------------------------------------
1 | import { GeocoderAutocomplete } from "../src";
2 | import fetchMock from "jest-fetch-mock";
3 | import '@testing-library/jest-dom';
4 | import { mockResponseWithData, options } from "./test-data";
5 |
6 | export const WAIT_TIME = 200;
7 | export const APP_URL = "https://api.geoapify.com/v1/geocode/autocomplete";
8 |
9 | export function reset(autocomplete: GeocoderAutocomplete) {
10 | autocomplete.clearFilters();
11 | autocomplete.clearBias();
12 | }
13 |
14 | export function getPrivateProperty(object: any, field: any) {
15 | return object[field];
16 | }
17 |
18 | export function checkIfClearButtonInitialized(container: HTMLDivElement) {
19 | const clearButton = container.querySelector('.geoapify-close-button');
20 | expect(clearButton).toHaveClass('geoapify-close-button');
21 |
22 | const svgElement = clearButton?.querySelector('svg');
23 | expect(svgElement).toHaveAttribute('viewBox', '0 0 24 24');
24 |
25 | const pathElement = svgElement?.querySelector('path');
26 | expect(pathElement).toHaveAttribute('d', 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'); // Example path for "close" icon
27 | expect(pathElement).toHaveAttribute('fill', 'currentColor');
28 |
29 | }
30 |
31 | export function checkIfInputInitialized(container: HTMLDivElement) {
32 | const inputElement = container.querySelector('input') as HTMLInputElement;
33 | expect(inputElement).toHaveClass('geoapify-autocomplete-input');
34 |
35 | expect(inputElement).toHaveAttribute('type', 'text');
36 |
37 | expect(inputElement).toHaveAttribute('placeholder', 'Search location');
38 | }
39 |
40 |
41 | export function createGeocoderAutocomplete(divElement: HTMLDivElement) {
42 | const myAPIKey = "XXXXX";
43 | return new GeocoderAutocomplete(
44 | divElement,
45 | myAPIKey, options);
46 | }
47 |
48 | export function inputText(container: HTMLDivElement, text: string) {
49 | const inputElement = container.querySelector('input') as HTMLInputElement;
50 |
51 | inputElement.value = text;
52 |
53 | const event = new Event('input', {bubbles: true, cancelable: true});
54 | inputElement.dispatchEvent(event);
55 | }
56 |
57 | export function inputTextWithEvent(container: HTMLDivElement, text: string, eventType: string) {
58 | const inputElement = container.querySelector('input') as HTMLInputElement;
59 |
60 | inputElement.value = text;
61 |
62 | const event = new KeyboardEvent("keydown", {
63 | bubbles: true,
64 | cancelable: true,
65 | code: eventType,
66 | key: eventType
67 | });
68 | inputElement.dispatchEvent(event);
69 | }
70 |
71 | export async function wait(millis: number) {
72 | await new Promise(res => setTimeout(res, millis));
73 | }
74 |
75 | export async function inputValueAndReturnResponse(container: HTMLDivElement, data: any) {
76 | fetchMock.resetMocks();
77 | fetchMock.mockResponseOnce(JSON.stringify(data));
78 |
79 | inputText(container, "123");
80 |
81 | await wait(WAIT_TIME);
82 | }
83 |
84 | export async function inputValueAndExpectTheRequest(container: HTMLDivElement, request: string) {
85 | fetchMock.resetMocks();
86 | fetchMock.mockResponseOnce(JSON.stringify(mockResponseWithData));
87 |
88 | inputText(container, "123");
89 |
90 | await wait(WAIT_TIME);
91 |
92 | expect(fetchMock).toHaveBeenCalledWith(request);
93 | }
94 |
95 | export async function inputValueAndDontExpectTheRequest(container: HTMLDivElement) {
96 | fetchMock.resetMocks();
97 | inputText(container, "123");
98 |
99 | await wait(WAIT_TIME);
100 |
101 | expect(fetchMock).toHaveBeenCalledTimes(0);
102 | }
103 |
104 | export async function inputValueAndPopulateDropdown(container: HTMLDivElement) {
105 | await inputValueAndExpectTheRequest(container, `${APP_URL}?text=123&apiKey=XXXXX&limit=5`);
106 | }
107 |
108 | export function clickOutside() {
109 | const outsideElement = document.createElement('div'); // Create a new element to click outside
110 | document.body.appendChild(outsideElement);
111 | outsideElement.click(); // Simulate click on the outside eleme
112 | document.body.removeChild(outsideElement);
113 | }
114 |
115 | export function selectDropdownItem(container: HTMLDivElement, itemIndex: number) {
116 | let selectedItem = getDropDownItem(container, itemIndex);
117 | selectedItem.click();
118 | }
119 |
120 | export function getDropDownItem(container: HTMLDivElement, itemIndex: number) {
121 | const dropdown = container.querySelector('.geoapify-autocomplete-items');
122 | const items = dropdown?.querySelectorAll('.geoapify-autocomplete-item');
123 | return items[itemIndex] as HTMLDivElement;
124 | }
125 |
126 | export function getDropDownItemValue(container: HTMLDivElement, itemIndex: number) {
127 | let selectedItem = getDropDownItem(container, itemIndex);
128 | let spanItem = selectedItem.getElementsByClassName("main-part")[0];
129 | return spanItem.innerHTML;
130 | }
131 |
132 | export function expectDropdownIsClosed(container: HTMLDivElement) {
133 | expect(container.querySelector('.geoapify-autocomplete-items')).toBeNull();
134 | }
135 |
136 | export function addSelectSpy(autocomplete: GeocoderAutocomplete) {
137 | const selectSpy = jest.fn();
138 | autocomplete.on('select', selectSpy);
139 | return selectSpy;
140 | }
141 |
142 | export function addSuggestionsSpy(autocomplete: GeocoderAutocomplete) {
143 | const suggestionChangeSpy = jest.fn();
144 | autocomplete.on('suggestions', suggestionChangeSpy);
145 | return suggestionChangeSpy;
146 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | // "incremental": true, /* Enable incremental compilation */
5 | "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
6 | "module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
7 | // "lib": [], /* Specify library files to be included in the compilation. */
8 | // "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | "declaration": true, /* Generates corresponding '.d.ts' file. */
12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | "outDir": "./dist", /* Redirect output structure to the directory. */
16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "composite": true, /* Enable project compilation */
18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
19 | // "removeComments": true, /* Do not emit comments to output. */
20 | // "noEmit": true, /* Do not emit outputs. */
21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
24 |
25 | /* Strict Type-Checking Options */
26 | "strict": true, /* Enable all strict type-checking options. */
27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
28 | "strictNullChecks": false, /* Enable strict null checks. */
29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
34 |
35 | /* Additional Checks */
36 | // "noUnusedLocals": true, /* Report errors on unused locals. */
37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
40 |
41 | /* Module Resolution Options */
42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
46 | // "typeRoots": [], /* List of folders to include type definitions from. */
47 | // "types": [], /* Type declaration files to be included in compilation. */
48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
52 |
53 | /* Source Map Options */
54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
58 |
59 | /* Experimental Options */
60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
62 |
63 | /* Advanced Options */
64 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
65 | "resolveJsonModule": true
66 | },
67 | "include": ["src"],
68 | "exclude": ["node_modules", "**/__tests__/*", "dist", "rollup-config.mjs"]
69 | }
70 |
--------------------------------------------------------------------------------