places-autocomplete-svelte 2.2.4 → 2.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # Places (New) Autocomplete Svelte
2
2
 
3
- This Svelte component provides a user-friendly way to search for and retrieve detailed address information within your [SvelteKit](https://kit.svelte.dev) applications, leveraging the power of the [Google Maps Places (New) Autocomplete API](https://developers.google.com/maps/documentation/javascript/place-autocomplete-overview). It comes with default styling using [Tailwind CSS](https://tailwindcss.com/), which you can fully customise.
3
+ [![npm version](https://badge.fury.io/js/places-autocomplete-svelte.svg)](https://badge.fury.io/js/places-autocomplete-svelte)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A flexible and customizable [Svelte](https://kit.svelte.dev) component leveraging the [Google Maps Places (New) Autocomplete API](https://developers.google.com/maps/documentation/javascript/place-autocomplete-overview) to provide a user-friendly way to search for and retrieve detailed address information within your [SvelteKit](https://kit.svelte.dev) applications.
7
+
8
+ This component handles API loading, session tokens, fetching suggestions, and requesting place details, allowing you to focus on integrating the results into your application. Includes features like debounced input, highlighting of matched suggestions, extensive customization via CSS classes, and full TypeScript support.
9
+
4
10
 
5
11
 
6
12
  ## Places (New) Autocomplete – JavaScript Integration
@@ -11,13 +17,16 @@ Simply include a single script tag and handle the response in your JavaScript co
11
17
 
12
18
  ## Features
13
19
 
14
- - **Seamless SvelteKit Integration:** Easily add the component to your SvelteKit projects.
15
- - **Real-time Autocomplete Suggestions:** As the user types, address suggestions appear dynamically.
16
- - **Comprehensive Address Details:** Retrieve detailed information, including street address, city, state/province, postal code, country, and more.
17
- - **Country/Region Filtering:** Narrow down search results by specifying target countries or regions.
18
- - **Customizable Styles:** Tailor the component's appearance to match your application's design by overriding the default Tailwind CSS classes.
19
- - **Flexible Data Control:** Choose the specific data fields you want to retrieve using the `fetchFields` property.
20
- - **Keyboard Navigation & Accessibility:** Use keyboard navigation for selecting suggestions, ensuring accessibility for all users.
20
+ * Integrates with the modern **Google Places (New) Autocomplete API**.
21
+ * Automatically handles **session tokens** for cost management per Google's guidelines.
22
+ * **Debounced Input:** Limits API calls while the user is typing (configurable).
23
+ * **Suggestion Highlighting:** Automatically highlights the portion of text matching the user's input in the suggestions list.
24
+ * **Customizable Styling:** Easily override default styles or apply your own using the `options.classes` prop. Built with [Tailwind CSS](https://tailwindcss.com/) utility classes by default.
25
+ * **TypeScript Support:** Fully written in TypeScript with included type definitions.
26
+ * **Event Handling:** Provides `onResponse` and `onError` callbacks.
27
+ * **Configurable:** Control API parameters (`requestParams`), requested data fields (`fetchFields`), and component behavior/appearance (`options`).
28
+ * **Prop Validation:** Sensible defaults and validation for configuration props.
29
+
21
30
 
22
31
 
23
32
  ## Demo
@@ -45,151 +54,204 @@ See a live demo of the component in action: [Basic Example](https://places-autoc
45
54
  ## Installation Svelte 5
46
55
 
47
56
  ```bash
48
- npm i places-autocomplete-svelte
57
+ npm install places-autocomplete-svelte
58
+ # or
59
+ yarn add places-autocomplete-svelte
49
60
  ```
50
61
 
51
62
 
52
63
 
53
-
54
64
  ## Basic Usage
55
65
 
56
66
  1. Replace `'___YOUR_API_KEY___'` with your actual **Google Maps API Key**.
57
67
  2. Use the `onResponse` callback to **handle the response**.
58
68
 
59
- ```svelte
69
+ ```javascript
60
70
  <script>
61
71
  import { PlaceAutocomplete } from 'places-autocomplete-svelte';
72
+ import type { PlaceResult, ComponentOptions, RequestParams } from 'places-autocomplete-svelte/interfaces'; // Adjust path if needed
62
73
 
63
- //Recommended: Store your key securely as an environment variable
64
- const PUBLIC_GOOGLE_MAPS_API_KEY = '___YOUR_API_KEY___';
74
+ // Get API Key securely (e.g., from environment variables)
75
+ const PUBLIC_GOOGLE_MAPS_API_KEY = import.meta.env.VITE_PUBLIC_GOOGLE_MAPS_API_KEY;
76
+ let fullResponse: PlaceResult | null = $state(null);
77
+ let placesError = $state('');
65
78
 
66
79
 
67
- let fullResponse = $state('')
68
- let onResponse = (response) => {
80
+ // --- Event Handlers ---
81
+ const handleResponse = (response: PlaceResult) => {
82
+ console.log('Place Selected:', response);
69
83
  fullResponse = response;
84
+ placesError = ''; // Clear previous errors
85
+ };
86
+
87
+ const handleError = (error: string) => {
88
+ console.error('Places Autocomplete Error:', error);
89
+ placesError = error;
90
+ fullResponse = null; // Clear previous results
70
91
  };
92
+
93
+ // --- Configuration (Optional) ---
94
+
95
+ // Control API request parameters
96
+ const requestParams: Partial<RequestParams> = $state({
97
+ region: 'GB', // Example: Bias results to Great Britain
98
+ language: 'en-GB',
99
+ // includedRegionCodes: ['GB'], // Example: Only show results in the specified regions,
100
+ // includedPrimaryTypes: ['address'], // Example: Only show addresses
101
+ });
102
+
103
+ // Control which data fields are fetched for Place Details (affects cost!)
104
+ const fetchFields: string[] = $state(['formattedAddress', 'addressComponents', 'name']);
105
+
106
+ // Control component appearance and behavior
107
+ const options: Partial<ComponentOptions> = $state({
108
+ placeholder: 'Start typing your address...',
109
+ debounce: 200, // Debounce input by 200ms (default is 100ms)
110
+ distance: true, // Show distance if origin is provided in requestParams
111
+ classes: {
112
+ // Example: Override input styling and highlight class
113
+ input: 'my-custom-input-class border-blue-500',
114
+ highlight: 'bg-yellow-200 text-black', // Customize suggestion highlighting
115
+ }
116
+ });
117
+
71
118
  </script>
72
119
 
73
- <PlaceAutocomplete {onResponse} {PUBLIC_GOOGLE_MAPS_API_KEY} />
120
+ {#if placesError}
121
+ <div class="error-message" role="alert">
122
+ Error: {placesError}
123
+ </div>
124
+ {/if}
74
125
 
75
- <p>Response Object: {JSON.stringify(fullResponse, null, 2)}</p>
76
- ```
126
+ <PlaceAutocomplete
127
+ {PUBLIC_GOOGLE_MAPS_API_KEY}
128
+ {requestParams}
129
+ {fetchFields}
130
+ {options}
131
+ onResponse={handleResponse}
132
+ onError={handleError}
133
+ />
77
134
 
135
+ {#if fullResponse}
136
+ <h2>Selected Place Details:</h2>
137
+ <pre>{JSON.stringify(fullResponse, null, 2)}</pre>
138
+ {/if}
78
139
 
140
+ <style>
141
+ /* Example of styling an overridden class */
142
+ :global(.my-custom-input-class) {
143
+ padding: 0.75rem;
144
+ border-radius: 0.25rem;
145
+ width: 100%;
146
+ /* Add other styles */
147
+ }
148
+ .error-message {
149
+ color: red;
150
+ margin-bottom: 1rem;
151
+ }
152
+ </style>
153
+ ```
79
154
  ## Component Properties
155
+ | Prop | Type | Required | Default | Description |
156
+ |----------------------------|---------------------------------|----------|-------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
157
+ | PUBLIC_GOOGLE_MAPS_API_KEY | string | Yes | - | Your Google Maps API Key with Places API enabled. |
158
+ | fetchFields | string[] | No | ['formattedAddress', 'addressComponents'] | Array of Place Data Fields to request when a place is selected. Affects API cost. |
159
+ | requestParams | Partial<RequestParams> | No | { inputOffset: 3, ... } | Parameters for the Autocomplete request. See AutocompletionRequest options. |
160
+ | options | Partial<ComponentOptions> | No | { debounce: 100, ... } | Options to control component behavior and appearance. See details below. |
161
+ | onResponse | (response: PlaceResult) => void | Yes | - | Callback function triggered with the selected place details (PlaceResult object) after fetchFields is complete. |
162
+ | onError | (error: string) => void | Yes | - | Callback function triggered when an error occurs (API loading, fetching suggestions, fetching details). |
80
163
 
81
- | Property | Type | Description | Required | Default Value |
82
- |--------------------------|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------------------------------|
83
- | `PUBLIC_GOOGLE_MAPS_API_KEY` | `String` | Your Google Maps Places API Key. | Yes | |
84
- | `onResponse` | `CustomEvent` | Dispatched when a place is selected, containing the place details in `event.detail`. | Yes | |
85
- | `onError` | `CustomEvent` | Dispatched when an error occurs, with the error message in `event.detail`. | No | |
86
- | `requestParams` | `Object` | Object for additional request parameters (e.g., `types`, `bounds`, `origin`, `region`, `language`). See [AutocompleteRequest](https://developers.google.com/maps/documentation/javascript/reference/autocomplete-data#AutocompleteRequest). | No | `{}` |
87
- | `fetchFields` | `Array` | Array of place data fields to return. See [Supported Fields](https://developers.google.com/maps/documentation/javascript/place-class-data-fields) documentation for a comprehensive list of available fields. Note that the Places Autocomplete service does not support the following fields, even if they are available in the Place Details API: `geometry`, `icon`, `name`, `permanentlyClosed`, `photo`, `placeId`, `url`, `utcOffset`, `vicinity`, `openingHours`, `icon`, and `name`. If you need these fields, make a separate call to the Place Details API using the returned `place_id`. | No | `['formattedAddress', 'addressComponents']` |
88
- | `options` | `Object` | Options for customizing the component's behavior and appearance. See "Customization" below. | No | See default values in "Customization" |
89
164
 
90
165
 
91
-
92
- ## Customization
93
166
  ### Options
94
167
 
95
- | Property | Type | Description | Default Value |
96
- |----------------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
97
- | `autofocus` | `boolean` | If `true`, the input field will be focused automatically when the component mounts. | `false` |
98
- | `placeholder` | `String` | Placeholder text for the input field. | `"Search..."` |
99
- | `autocomplete`| `string` | HTML `autocomplete` attribute for the input field. Set to `"off"` to disable browser autocomplete. | `"off"` |
100
- | `distance`| `boolean` | If `true`, and if an `origin` is specified in `requestParams`, displays the distance to each suggestion. The distance is calculated as a geodesic in meters. | `false` |
101
- | `distance_units`| `km` or `miles` | Specified the distance units. | `km` |
102
- | `classes` | `Object` | Object to override default Tailwind CSS classes. | See [styling](https://places-autocomplete-demo.pages.dev/examples/styling) |
103
168
 
104
- ### Styling
105
- Customise the component's appearance by providing an object to the classes property. This object should contain key-value pairs, where the keys correspond to the component's elements and the values are your custom CSS class names. See [styling](https://places-autocomplete-demo.pages.dev/examples/styling) for details.
169
+ | Option | Type | Default | Description |
170
+ |----------------|---------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------|
171
+ | placeholder | string | '' | Placeholder text for the input field. |
172
+ | debounce | number | 100 | (New) Delay in milliseconds before triggering autocomplete API request after user stops typing. Set to 0 to disable debouncing. |
173
+ | distance | boolean | true | Show distance from requestParams.origin in suggestions (if origin is provided). |
174
+ | distance_units | 'km' \| 'miles' | 'km' | Units to display distance in. |
175
+ | label | string | '' | Optional label text displayed above the input field. |
176
+ | autofocus | boolean | false | Automatically focus the input field on mount. |
177
+ | autocomplete | string | 'off' | Standard HTML autocomplete attribute for the input field. |
178
+ | classes | Partial<ComponentClasses> | {} | Object to override default CSS classes. See Styling section. |
106
179
 
107
180
 
108
- ### Request Parameters (requestParams)
109
- Fine-tune the autocomplete search with the requestParams property. This property accepts an object corresponding to the AutocompleteRequest object in the Google Maps API documentation. See this [request parameters](https://places-autocomplete-demo.pages.dev/component/request-parameters) for more details. Here are some common examples:
110
181
 
111
- ```svelte
112
- <script>
113
- // ... other imports
114
-
115
- /**
116
- * @type object optional
117
- * AutocompleteRequest properties
118
- */
119
- const requestParams = {
120
- /**
121
- * @type string optional
122
- */
123
- language : 'en-GB',
124
- /**
125
- * @type string optional
126
- */
127
- region : 'GB',
128
- }
129
-
130
- /**
131
- * @type object optional
132
- * Options
133
- */
134
- const options = {
135
- autofocus: false,
136
- autocompete: 'off',
137
- placeholder: 'Start typing your address',
138
- distance: true,
139
- distance_units: 'km' // or miles
140
- };
141
182
 
142
- /**
143
- * @type array optional
144
- */
145
- const fetchFields = ['formattedAddress', 'addressComponents'];
146
- </script>
183
+ ### Styling (`options.classes`)
184
+ ---------------------------
147
185
 
148
- <PlaceAutocomplete
149
- {onError}
150
- {onResponse}
151
- {PUBLIC_GOOGLE_MAPS_API_KEY}
152
- {requestParams}
153
- {options}
154
- {fetchFields}
155
-
156
- />
186
+ You can customize the appearance of the component by providing your own CSS classes via the `options.classes` prop. The component uses Tailwind CSS utility classes by default. Provide an object where keys are the component parts and values are the class strings you want to apply. See [styling](https://places-autocomplete-demo.pages.dev/examples/styling) for details.
157
187
 
158
- ```
159
188
 
189
+ **Available Class Keys:**
190
+
191
+ - `section`: The main container section.
192
+ - `container`: The div containing the input and suggestions list.
193
+ - `label`: The label element (if `options.label` is provided).
194
+ - `input`: The main text input element.
195
+ - `icon_container`: Container for the optional icon.
196
+ - `icon`: SVG string for the icon.
197
+ - `ul`: The `<ul>` element for the suggestions list.
198
+ - `li`: Each `<li>` suggestion item.
199
+ - `li_current`: Class added to the currently highlighted/selected `<li>` (keyboard/mouse).
200
+ - `li_a`: The inner `<a>` or `<button>` element within each `<li>`.
201
+ - `li_a_current`: Class added to the inner element when its `<li>` is current.
202
+ - `li_div_container`: Container div within the `<a>`/`<button>`.
203
+ - `li_div_one`: First inner div (usually contains the main text).
204
+ - `li_div_one_p`: The `<p>` tag containing the main suggestion text (`@html` is used).
205
+ - `li_div_two`: Second inner div (usually contains the distance).
206
+ - `li_div_two_p`: The `<p>` tag containing the distance text.
207
+ - `kbd_container`: Container for the keyboard hint keys (Esc, Up, Down).
208
+ - `kbd_escape`: The `<kbd>` tag for the 'Esc' hint.
209
+ - `kbd_up`: The `<kbd>` tag for the 'Up Arrow' hint.
210
+ - `kbd_down`: The `<kbd>` tag for the 'Down Arrow' hint.
211
+ - `highlight`: **(New)** The class applied to the `<span>` wrapping the matched text within suggestions. Defaults to `'font-bold'`.
212
+
213
+ ### Example:
214
+
215
+ ```javascript
216
+ const options = {
217
+ classes: {
218
+ input: 'form-input w-full rounded-md shadow-sm', // Replace default input style
219
+ ul: 'absolute bg-white shadow-lg rounded-md mt-1 w-full z-10', // Custom dropdown style
220
+ li_current: 'bg-blue-500 text-white', // Custom highlight style for selected item
221
+ highlight: 'text-blue-700 font-semibold' // Custom style for matched text
222
+ }
223
+ };
224
+ ```
160
225
 
226
+ Events
227
+ ------
161
228
 
229
+ - **`onResponse`**: `(response: PlaceResult) => void`
230
+ - Fired after a user selects a suggestion and the requested `fetchFields` have been successfully retrieved.
231
+ - The `response` argument is an object containing the place details based on the `fetchFields` requested. Its structure mirrors the [PlaceResult](https://developers.google.com/maps/documentation/javascript/reference/places-service#PlaceResult) but includes only the requested fields.
232
+ - **`onError`**: `(error: string) => void`
233
+ - Fired if there's an error loading the Google Maps API, fetching autocomplete suggestions, or fetching place details.
234
+ - The `error` argument is a string describing the error.
162
235
 
163
- ## Error Handling
164
236
 
165
- Use the `onError` event handler to gracefully manage any errors that may occur during the autocomplete process:
237
+ TypeScript
238
+ ----------
166
239
 
240
+ This component is written in TypeScript and includes type definitions for props (`Props`, `ComponentOptions`, `RequestParams`, `ComponentClasses`) and the response (`PlaceResult`, `AddressComponent`). You can import these types from `places-autocomplete-svelte/interfaces` (adjust path if needed based on your setup).
167
241
 
168
- ```svelte
169
- <script>
170
- // ... other imports
171
242
 
172
- // Error handler
173
- let pacError = '';
174
- let onError = (error: string) => {
175
- console.error(error);
176
- pacError = error;
177
- };
178
- </script>
179
243
 
180
- <PlaceAutocomplete
181
- {onResponse}
182
- {onError}
183
- {PUBLIC_GOOGLE_MAPS_API_KEY} />
244
+ Google Places API & Billing
245
+ ---------------------------
184
246
 
185
- {#if pacError}
186
- <p class="error">{pacError}</p>
187
- {/if}
188
- ```
247
+ - This component uses the Google Maps JavaScript API (specifically the Places library). Usage is subject to Google's terms and pricing.
248
+ - An API key enabled for the "Places API" is required.
249
+ - The component uses **Session Tokens** automatically to group Autocomplete requests, which can lead to significant cost savings compared to per-request billing. See [Google's Session Token Pricing](https://developers.google.com/maps/documentation/places/web-service/usage-and-billing#session-pricing).
250
+ - Place Details requests (made via `fetchFields` when a suggestion is selected) are billed separately. Carefully select only the `fetchFields` you need to manage costs. See [Place Data Fields Pricing](https://developers.google.com/maps/documentation/javascript/usage-and-billing#data-pricing).
189
251
 
190
252
  ## Contributing
191
253
 
192
- Contributions are welcome! Please open an issue or submit a pull request on the [GitHub](https://github.com/alexpechkarev/places-autocomplete-svelte/).
254
+ Contributions are welcome! Please feel free to open an issue or submit a pull request.
193
255
 
194
256
  ## License
195
257
 
@@ -1,8 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
3
  import * as GMaps from '@googlemaps/js-api-loader';
4
- import type { Props } from './interfaces.js';
5
- import { validateOptions, validateRequestParams, formatDistance, validateFetchFields } from './helpers.js';
4
+ import type { PlaceResult, Props } from './interfaces.js';
5
+ import {
6
+ validateOptions,
7
+ validateRequestParams,
8
+ formatDistance,
9
+ validateFetchFields,
10
+ componentOptions
11
+ } from './helpers.js';
6
12
  const { Loader } = GMaps;
7
13
 
8
14
  let {
@@ -14,7 +20,7 @@
14
20
  //fetchFields = $bindable(['formattedAddress', 'addressComponents']),
15
21
  fetchFields,
16
22
  options,
17
- onResponse = $bindable((e: Event) => {}),
23
+ onResponse = $bindable((response: PlaceResult) => {}),
18
24
  onError = $bindable((error: string) => {}),
19
25
  requestParams = {}
20
26
  }: Props = $props();
@@ -28,12 +34,11 @@
28
34
  //console.log(fetchFields);
29
35
 
30
36
  // set classes as state
31
- let cl = $state(options.classes);
32
-
37
+ let cl = $state(options.classes ?? {});
33
38
  // reset keyboard classes
34
39
  const resetKbdClasses = () => {
35
- cl.kbd_down = options.classes.kbd_down;
36
- cl.kbd_up = options.classes.kbd_up;
40
+ cl.kbd_down = options.classes?.kbd_down ?? componentOptions.classes?.kbd_down ?? '';
41
+ cl.kbd_up = options.classes?.kbd_up ?? componentOptions.classes?.kbd_up ?? '';
37
42
  };
38
43
 
39
44
  // Local variables
@@ -63,45 +68,99 @@
63
68
  request.input = '';
64
69
  results = [];
65
70
  setSessionToken();
71
+ //console.log('reset completed', results);
66
72
  };
67
73
 
74
+ /**
75
+ * Debounce function to limit the rate at which a function can fire.
76
+ * @param func
77
+ * @param wait
78
+ */
79
+ function debounce<T extends (...args: any[]) => any>(
80
+ func: T,
81
+ wait: number
82
+ ): (...args: Parameters<T>) => void {
83
+ let timeout: ReturnType<typeof setTimeout> | null = null;
84
+ return function executedFunction(...args: Parameters<T>) {
85
+ const later = () => {
86
+ timeout = null;
87
+ func(...args);
88
+ };
89
+ if (timeout !== null) {
90
+ clearTimeout(timeout);
91
+ }
92
+ timeout = setTimeout(later, wait);
93
+ };
94
+ }
95
+
68
96
  /**
69
97
  * Make request and get autocomplete suggestions.
70
98
  * @param event
71
99
  */
72
- const makeAcRequest = async (
73
- event: Event & { currentTarget: HTMLInputElement }
74
- ): Promise<void> => {
75
- const target = event.currentTarget as HTMLInputElement;
76
- if (target?.value == '') {
77
- //title = '';
100
+ const debouncedMakeAcRequest = debounce(async (inputValue: string) => {
101
+ if (inputValue === '') {
78
102
  request.input = '';
79
103
  results = [];
80
104
  return;
81
105
  }
82
- /**
83
- * Prevent making request if the inputOffset is greater than the length of the input
84
- * The input lenght should be greater than the inputOffset before making a request and displaying suggestions
85
- */
86
- if (request.inputOffset && request.inputOffset >= target.value.length) {
106
+ if (request.inputOffset && request.inputOffset >= inputValue.length) {
87
107
  return;
88
108
  }
89
109
 
90
- // set request input
91
- request.input = target.value;
110
+ // User input
111
+ request.input = inputValue;
112
+
113
+ //console.log(request.input);
92
114
 
93
- // attempt to get autocomplete suggestions
94
115
  try {
116
+ // Ensure placesApi is loaded
117
+ if (!placesApi.AutocompleteSuggestion) {
118
+ console.warn('Places API not loaded yet.');
119
+ return;
120
+ }
95
121
  const { suggestions } =
96
122
  await placesApi.AutocompleteSuggestion.fetchAutocompleteSuggestions(request);
123
+
124
+ // Clear previous results
97
125
  results = [];
98
- //const formatter = new Intl.NumberFormat('en');
99
- // iterate suggestions and add results to an array
126
+
127
+ // ieterate over suggestions and add results to an array
100
128
  for (const suggestion of suggestions) {
101
- // add suggestions to results
129
+ // get prediction text
130
+ const predictionText = suggestion.placePrediction.text;
131
+ const originalText = predictionText.text;
132
+ // Array of objects with startOffset, endOffset
133
+ const matches = predictionText.matches;
134
+
135
+ //Highlighting Logic
136
+ let highlightedText = '';
137
+ let lastIndex = 0;
138
+
139
+ // Sort matches just in case they aren't ordered (though they usually are)
140
+ matches.sort(
141
+ (a: { startOffset: number }, b: { startOffset: number }) => a.startOffset - b.startOffset
142
+ );
143
+ for (const match of matches) {
144
+ // Append text before the current match
145
+ highlightedText += originalText.substring(lastIndex, match.startOffset);
146
+
147
+ // Append the highlighted match segment
148
+ // Choose your highlighting class (e.g., 'font-bold' or a custom one)
149
+ highlightedText += `<span class="${options.classes?.highlight ?? 'font-bold'}">`;
150
+ highlightedText += originalText.substring(match.startOffset, match.endOffset);
151
+ highlightedText += `</span>`;
152
+
153
+ // Update the last index processed
154
+ lastIndex = match.endOffset;
155
+ }
156
+
157
+ // Append any remaining text after the last match
158
+ highlightedText += originalText.substring(lastIndex);
159
+ // --- End Highlighting Logic ---
160
+
102
161
  results.push({
103
162
  place: suggestion.placePrediction.toPlace(),
104
- text: suggestion.placePrediction.text.toString(),
163
+ text: highlightedText,
105
164
  distance: formatDistance(
106
165
  suggestion.placePrediction.distanceMeters,
107
166
  options.distance_units ?? 'km'
@@ -111,13 +170,17 @@
111
170
  } catch (e: any) {
112
171
  onError((e.name || 'An error occurred') + ' - ' + (e.message || 'see console for details.'));
113
172
  }
114
- };
173
+ }, options?.debounce ?? 100); // Debounce by 100ms
174
+
115
175
  /**
116
176
  * Event handler for clicking on a suggested place.
117
177
  //https://developers-dot-devsite-v2-prod.appspot.com/maps/documentation/javascript/reference/autocomplete-data#AutocompleteSuggestion
118
178
  * @param place
119
179
  */
120
- const onPlaceSelected = async (place: { fetchFields: (arg0: { fields: string[]; }) => any; toJSON: () => any; }): Promise<void> => {
180
+ const onPlaceSelected = async (place: {
181
+ fetchFields: (arg0: { fields: string[] }) => any;
182
+ toJSON: () => any;
183
+ }): Promise<void> => {
121
184
  try {
122
185
  // console.log(place);
123
186
  // console.log(fetchFields);
@@ -125,15 +188,18 @@
125
188
  fields: fetchFields
126
189
  });
127
190
  let placeData = place.toJSON();
191
+ // reset search input and results
192
+ reset();
128
193
  onResponse(placeData);
129
194
  } catch (e: any) {
195
+ // reset search input and results
196
+ reset();
130
197
  onError(
131
198
  (e.name || 'An error occurred') + ' - ' + (e.message || 'error fetching place details')
132
199
  );
133
200
  }
134
201
 
135
- // reset search input and results
136
- reset();
202
+
137
203
  };
138
204
 
139
205
  /**
@@ -164,7 +230,7 @@
164
230
  libraries: ['places']
165
231
  });
166
232
 
167
- const { AutocompleteSessionToken, AutocompleteSuggestion } =
233
+ const { AutocompleteSessionToken, AutocompleteSuggestion} =
168
234
  await loader.importLibrary('places');
169
235
 
170
236
  placesApi.AutocompleteSessionToken = AutocompleteSessionToken;
@@ -187,11 +253,11 @@
187
253
  if (e.key === 'ArrowDown') {
188
254
  currentSuggestion = Math.min(currentSuggestion + 1, results.length - 1);
189
255
  resetKbdClasses();
190
- cl.kbd_down += ' bg-indigo-500 text-white';
256
+ cl.kbd_down += ' ' + (cl?.kbd_active ?? 'bg-indigo-500 text-white');
191
257
  } else if (e.key === 'ArrowUp') {
192
258
  currentSuggestion = Math.max(currentSuggestion - 1, 0);
193
259
  resetKbdClasses();
194
- cl.kbd_up += ' bg-indigo-500 text-white';
260
+ cl.kbd_up += ' ' + (cl?.kbd_active ?? 'bg-indigo-500 text-white');
195
261
  } else if (e.key === 'Enter') {
196
262
  e.preventDefault();
197
263
  if (currentSuggestion >= 0) {
@@ -201,7 +267,9 @@
201
267
  // reset srarch input and results
202
268
  reset();
203
269
  }
204
-
270
+ // Optional: Scroll suggestion into view
271
+ const selectedElement = document.getElementById(`option-${currentSuggestion + 1}`);
272
+ selectedElement?.scrollIntoView({ block: 'nearest' });
205
273
  setTimeout(() => {
206
274
  resetKbdClasses();
207
275
  }, 300);
@@ -211,8 +279,13 @@
211
279
  <svelte:window onkeydown={onKeyDown} />
212
280
 
213
281
  <section class={options.classes?.section}>
214
- <div class={options.classes.container}>
215
- {#if options.classes.icon}
282
+ {#if options?.label ?? ''}
283
+ <label for="search" class={options.classes?.label ?? ''}>
284
+ {options.label}
285
+ </label>
286
+ {/if}
287
+ <div class={options.classes?.container ?? ''}>
288
+ {#if options.classes?.icon}
216
289
  <div class={options.classes.icon_container}>
217
290
  {@html options.classes.icon}
218
291
  </div>
@@ -222,7 +295,7 @@
222
295
  type="text"
223
296
  name="search"
224
297
  bind:this={inputRef}
225
- class={options.classes.input}
298
+ class={options.classes?.input ?? ''}
226
299
  placeholder={options.placeholder}
227
300
  autocomplete={options.autocomplete}
228
301
  aria-controls="options"
@@ -232,62 +305,69 @@
232
305
  aria-label="Search"
233
306
  aria-haspopup="listbox"
234
307
  bind:value={request.input}
235
- oninput={makeAcRequest}
308
+ oninput={(event) => debouncedMakeAcRequest(event.currentTarget.value)}
236
309
  />
310
+ <!-- oninput={makeAcRequest} -->
237
311
 
238
312
  {#if results.length > 0}
239
- <div class={options.classes.kbd_container}>
240
- <kbd class={options.classes.kbd_escape}>Esc</kbd>
313
+ <div class={options.classes?.kbd_container ?? ''}>
314
+ <kbd class={options.classes?.kbd_escape ?? ''}>Esc</kbd>
241
315
  <kbd class={cl.kbd_up}>&uArr;</kbd>
242
316
  <kbd class={cl.kbd_down}>&dArr;</kbd>
243
317
  </div>
244
318
 
245
- <ul class={options.classes.ul} id="options">
319
+ <ul class={options.classes?.ul ?? ''} id="options" role="listbox">
246
320
  {#each results as p, i}
247
321
  <li
248
- class={[options.classes.li, i === currentSuggestion && options.classes.li_current]}
322
+ role="option"
323
+ aria-selected={i === currentSuggestion}
324
+ class={[
325
+ options.classes?.li ?? '',
326
+ i === currentSuggestion && options.classes?.li_current
327
+ ]}
328
+ onmouseenter={() => (currentSuggestion = i)}
249
329
  id="option-{i + 1}"
250
330
  >
251
331
  <!-- svelte-ignore a11y_invalid_attribute -->
252
- <a
253
- href="javascript:void(0)"
332
+ <button
333
+ type="button"
254
334
  class={[
255
335
  options.classes?.li_a,
256
- i === currentSuggestion && options.classes.li_a_current
336
+ i === currentSuggestion && options.classes?.li_a_current
257
337
  ]}
258
- tabindex={i + 1}
259
338
  onclick={() => onPlaceSelected(p.place)}
260
339
  >
261
- <div class={[options.classes.li_div_container]}>
340
+ <div class={[options.classes?.li_div_container ?? '']}>
262
341
  <div
263
342
  class={[
264
- options.classes.li_div_one,
265
- i === currentSuggestion && options.classes.li_div_current
343
+ options.classes?.li_div_one ?? '',
344
+ i === currentSuggestion && options.classes?.li_div_current
266
345
  ]}
267
346
  >
268
347
  <p
269
348
  class={[
270
- i === currentSuggestion && options.classes.li_current,
271
- options.classes.li_div_one_p
349
+ i === currentSuggestion && options.classes?.li_current,
350
+ options.classes?.li_div_one_p
272
351
  ]}
273
352
  >
274
- {p.text}
353
+ <!-- {p.text} -->
354
+ {@html p.text}
275
355
  </p>
276
356
  </div>
277
357
  </div>
278
358
  {#if options.distance && p.distance}
279
- <div class={[options.classes.li_div_two]}>
359
+ <div class={[options.classes?.li_div_two]}>
280
360
  <p
281
361
  class={[
282
- i === currentSuggestion && options.classes.li_current,
283
- options.classes.li_div_two_p
362
+ i === currentSuggestion && options.classes?.li_current,
363
+ options.classes?.li_div_two_p
284
364
  ]}
285
365
  >
286
366
  {p.distance}
287
367
  </p>
288
368
  </div>
289
369
  {/if}
290
- </a>
370
+ </button>
291
371
  </li>
292
372
  {/each}
293
373
  </ul>
package/dist/helpers.js CHANGED
@@ -307,9 +307,10 @@ export const componentClasses = {
307
307
  li_a_current: 'text-white',
308
308
  li_div_container: 'flex min-w-0 gap-x-4',
309
309
  li_div_one: 'min-w-0 flex-auto',
310
- li_div_one_p: 'text-sm/6 font-semibold',
310
+ li_div_one_p: 'text-sm/6 ',
311
311
  li_div_two: 'shrink-0 flex flex-col items-end min-w-16',
312
- li_div_two_p: 'mt-1 text-xs/5'
312
+ li_div_two_p: 'mt-1 text-xs/5',
313
+ highlight: 'font-bold',
313
314
  };
314
315
  /**
315
316
  * Default component options
@@ -321,6 +322,8 @@ export const componentOptions = {
321
322
  distance: true,
322
323
  distance_units: 'km',
323
324
  classes: componentClasses,
325
+ label: '',
326
+ debounce: 100,
324
327
  };
325
328
  /**
326
329
  * Validate and cast component options
@@ -344,12 +347,18 @@ export const validateOptions = (options) => {
344
347
  case 'distance':
345
348
  validatedOptions.distance = Boolean(options.distance);
346
349
  break;
350
+ case 'label':
351
+ validatedOptions.label = String(options.label);
352
+ break;
347
353
  case 'distance_units':
348
354
  validatedOptions.distance_units = String(options.distance_units);
349
355
  break;
350
356
  case 'classes':
351
357
  if (options.classes && typeof options.classes === 'object') {
352
- validatedOptions.classes = { ...componentOptions.classes, ...options.classes };
358
+ validatedOptions.classes = {
359
+ ...componentOptions.classes,
360
+ ...options.classes ?? {}
361
+ };
353
362
  }
354
363
  break;
355
364
  }
@@ -33,16 +33,34 @@ export type DistanceUnits = "km" | "miles";
33
33
  export interface ComponentOptions {
34
34
  autofocus?: boolean;
35
35
  autocomplete?: AutoFill;
36
- classes: ComponentClasses;
36
+ classes?: ComponentClasses;
37
37
  placeholder?: string;
38
38
  distance?: boolean;
39
39
  distance_units?: DistanceUnits;
40
+ label?: string;
41
+ debounce?: number;
42
+ }
43
+ export interface PlaceResult {
44
+ formattedAddress: string;
45
+ addressComponents: {
46
+ longText: string;
47
+ shortText: string;
48
+ types: string[];
49
+ }[];
50
+ }
51
+ export interface FormattedAddress {
52
+ street_number: string;
53
+ street: string;
54
+ town: string;
55
+ county: string;
56
+ country_iso2: string;
57
+ postcode: string;
40
58
  }
41
59
  export interface Props {
42
60
  PUBLIC_GOOGLE_MAPS_API_KEY: string;
43
61
  options?: ComponentOptions;
44
62
  fetchFields?: string[];
45
63
  requestParams?: RequestParams;
46
- onResponse: (e: Event) => void;
64
+ onResponse: (response: PlaceResult) => void;
47
65
  onError: (error: string) => void;
48
66
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "places-autocomplete-svelte",
3
3
  "license": "MIT",
4
- "version": "2.2.4",
5
- "description": "A lightweight and customizable Svelte component for easy integration of Google Maps Places (New) Autocomplete in your Svelte/SvelteKit applications. Provides accessible autocomplete suggestions and detailed address retrieval.",
4
+ "version": "2.2.6",
5
+ "description": "A flexible and customizable Svelte component leveraging the Google Maps Places (New) Autocomplete API to provide a user-friendly way to search for and retrieve detailed address information within your SvelteKit applications.",
6
6
  "keywords": [
7
7
  "svelte",
8
8
  "sveltekit",
@@ -48,6 +48,11 @@
48
48
  "svelte": "./dist/PlaceAutocomplete.svelte",
49
49
  "default": "./dist/PlaceAutocomplete.svelte"
50
50
  },
51
+ "./interfaces": {
52
+ "types": "./dist/interfaces.d.ts",
53
+ "svelte": "./dist/interfaces.js",
54
+ "default": "./dist/interfaces.js"
55
+ },
51
56
  ".": {
52
57
  "types": "./dist/index.d.ts",
53
58
  "svelte": "./dist/index.js",
@@ -67,31 +72,31 @@
67
72
  "svelte": "^5.1.4"
68
73
  },
69
74
  "devDependencies": {
70
- "@sveltejs/adapter-auto": "^5.0.0",
71
- "@sveltejs/adapter-cloudflare": "^6.0.1",
72
- "@sveltejs/kit": "^2.20.2",
75
+ "@sveltejs/adapter-auto": "^6.0.0",
76
+ "@sveltejs/adapter-cloudflare": "^7.0.1",
77
+ "@sveltejs/kit": "^2.20.5",
73
78
  "@sveltejs/package": "^2.3.10",
74
79
  "@sveltejs/vite-plugin-svelte": "^5.0.3",
75
- "@tailwindcss/postcss": "^4.0.15",
80
+ "@tailwindcss/postcss": "^4.1.3",
76
81
  "@tailwindcss/typography": "^0.5.16",
77
- "@tailwindcss/vite": "^4.0.15",
82
+ "@tailwindcss/vite": "^4.1.3",
78
83
  "@types/eslint": "^9.6.1",
79
84
  "autoprefixer": "^10.4.21",
80
- "eslint": "^9.23.0",
81
- "eslint-config-prettier": "^10.1.1",
82
- "eslint-plugin-svelte": "^3.3.3",
85
+ "eslint": "^9.24.0",
86
+ "eslint-config-prettier": "^10.1.2",
87
+ "eslint-plugin-svelte": "^3.5.1",
83
88
  "globals": "^16.0.0",
84
89
  "postcss": "^8.5.3",
85
90
  "prettier": "^3.5.3",
86
91
  "prettier-plugin-svelte": "^3.3.3",
87
- "publint": "^0.3.9",
88
- "svelte": "^5.25.2",
89
- "svelte-check": "^4.1.5",
90
- "tailwindcss": "^4.0.15",
92
+ "publint": "^0.3.11",
93
+ "svelte": "^5.26.2",
94
+ "svelte-check": "^4.1.6",
95
+ "tailwindcss": "^4.1.3",
91
96
  "tslib": "^2.8.1",
92
- "typescript": "^5.8.2",
93
- "typescript-eslint": "^8.27.0",
94
- "vite": "^6.2.2"
97
+ "typescript": "^5.8.3",
98
+ "typescript-eslint": "^8.29.1",
99
+ "vite": "^6.2.6"
95
100
  },
96
101
  "svelte": "./dist/index.js",
97
102
  "types": "./dist/PlaceAutocomplete.svelte.d.ts",