places-autocomplete-svelte 2.2.4 → 2.2.5
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 +168 -106
- package/dist/PlaceAutocomplete.svelte +135 -55
- package/dist/helpers.js +12 -3
- package/dist/interfaces.d.ts +20 -2
- package/package.json +17 -17
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Places (New) Autocomplete Svelte
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/js/places-autocomplete-svelte)
|
|
4
|
+
[](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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
-
```
|
|
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
|
-
//
|
|
64
|
-
const PUBLIC_GOOGLE_MAPS_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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
120
|
+
{#if placesError}
|
|
121
|
+
<div class="error-message" role="alert">
|
|
122
|
+
Error: {placesError}
|
|
123
|
+
</div>
|
|
124
|
+
{/if}
|
|
74
125
|
|
|
75
|
-
<
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
144
|
-
*/
|
|
145
|
-
const fetchFields = ['formattedAddress', 'addressComponents'];
|
|
146
|
-
</script>
|
|
183
|
+
### Styling (`options.classes`)
|
|
184
|
+
---------------------------
|
|
147
185
|
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
{onError}
|
|
183
|
-
{PUBLIC_GOOGLE_MAPS_API_KEY} />
|
|
244
|
+
Google Places API & Billing
|
|
245
|
+
---------------------------
|
|
184
246
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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 {
|
|
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((
|
|
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
|
|
73
|
-
|
|
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
|
-
//
|
|
91
|
-
request.input =
|
|
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
|
-
|
|
99
|
-
//
|
|
126
|
+
|
|
127
|
+
// ieterate over suggestions and add results to an array
|
|
100
128
|
for (const suggestion of suggestions) {
|
|
101
|
-
//
|
|
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:
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
{
|
|
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
|
|
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={
|
|
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
|
|
240
|
-
<kbd class={options.classes
|
|
313
|
+
<div class={options.classes?.kbd_container ?? ''}>
|
|
314
|
+
<kbd class={options.classes?.kbd_escape ?? ''}>Esc</kbd>
|
|
241
315
|
<kbd class={cl.kbd_up}>⇑</kbd>
|
|
242
316
|
<kbd class={cl.kbd_down}>⇓</kbd>
|
|
243
317
|
</div>
|
|
244
318
|
|
|
245
|
-
<ul class={options.classes
|
|
319
|
+
<ul class={options.classes?.ul ?? ''} id="options" role="listbox">
|
|
246
320
|
{#each results as p, i}
|
|
247
321
|
<li
|
|
248
|
-
|
|
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
|
-
<
|
|
253
|
-
|
|
332
|
+
<button
|
|
333
|
+
type="button"
|
|
254
334
|
class={[
|
|
255
335
|
options.classes?.li_a,
|
|
256
|
-
i === currentSuggestion && options.classes
|
|
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
|
|
340
|
+
<div class={[options.classes?.li_div_container ?? '']}>
|
|
262
341
|
<div
|
|
263
342
|
class={[
|
|
264
|
-
options.classes
|
|
265
|
-
i === currentSuggestion && options.classes
|
|
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
|
|
271
|
-
options.classes
|
|
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
|
|
359
|
+
<div class={[options.classes?.li_div_two]}>
|
|
280
360
|
<p
|
|
281
361
|
class={[
|
|
282
|
-
i === currentSuggestion && options.classes
|
|
283
|
-
options.classes
|
|
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
|
-
</
|
|
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
|
|
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 = {
|
|
358
|
+
validatedOptions.classes = {
|
|
359
|
+
...componentOptions.classes,
|
|
360
|
+
...options.classes ?? {}
|
|
361
|
+
};
|
|
353
362
|
}
|
|
354
363
|
break;
|
|
355
364
|
}
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -33,16 +33,34 @@ export type DistanceUnits = "km" | "miles";
|
|
|
33
33
|
export interface ComponentOptions {
|
|
34
34
|
autofocus?: boolean;
|
|
35
35
|
autocomplete?: AutoFill;
|
|
36
|
-
classes
|
|
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: (
|
|
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.
|
|
5
|
-
"description": "A
|
|
4
|
+
"version": "2.2.5",
|
|
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",
|
|
@@ -67,31 +67,31 @@
|
|
|
67
67
|
"svelte": "^5.1.4"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@sveltejs/adapter-auto": "^
|
|
71
|
-
"@sveltejs/adapter-cloudflare": "^
|
|
72
|
-
"@sveltejs/kit": "^2.20.
|
|
70
|
+
"@sveltejs/adapter-auto": "^6.0.0",
|
|
71
|
+
"@sveltejs/adapter-cloudflare": "^7.0.1",
|
|
72
|
+
"@sveltejs/kit": "^2.20.5",
|
|
73
73
|
"@sveltejs/package": "^2.3.10",
|
|
74
74
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
|
75
|
-
"@tailwindcss/postcss": "^4.
|
|
75
|
+
"@tailwindcss/postcss": "^4.1.3",
|
|
76
76
|
"@tailwindcss/typography": "^0.5.16",
|
|
77
|
-
"@tailwindcss/vite": "^4.
|
|
77
|
+
"@tailwindcss/vite": "^4.1.3",
|
|
78
78
|
"@types/eslint": "^9.6.1",
|
|
79
79
|
"autoprefixer": "^10.4.21",
|
|
80
|
-
"eslint": "^9.
|
|
81
|
-
"eslint-config-prettier": "^10.1.
|
|
82
|
-
"eslint-plugin-svelte": "^3.
|
|
80
|
+
"eslint": "^9.24.0",
|
|
81
|
+
"eslint-config-prettier": "^10.1.2",
|
|
82
|
+
"eslint-plugin-svelte": "^3.5.1",
|
|
83
83
|
"globals": "^16.0.0",
|
|
84
84
|
"postcss": "^8.5.3",
|
|
85
85
|
"prettier": "^3.5.3",
|
|
86
86
|
"prettier-plugin-svelte": "^3.3.3",
|
|
87
|
-
"publint": "^0.3.
|
|
88
|
-
"svelte": "^5.
|
|
89
|
-
"svelte-check": "^4.1.
|
|
90
|
-
"tailwindcss": "^4.
|
|
87
|
+
"publint": "^0.3.11",
|
|
88
|
+
"svelte": "^5.26.2",
|
|
89
|
+
"svelte-check": "^4.1.6",
|
|
90
|
+
"tailwindcss": "^4.1.3",
|
|
91
91
|
"tslib": "^2.8.1",
|
|
92
|
-
"typescript": "^5.8.
|
|
93
|
-
"typescript-eslint": "^8.
|
|
94
|
-
"vite": "^6.2.
|
|
92
|
+
"typescript": "^5.8.3",
|
|
93
|
+
"typescript-eslint": "^8.29.1",
|
|
94
|
+
"vite": "^6.2.6"
|
|
95
95
|
},
|
|
96
96
|
"svelte": "./dist/index.js",
|
|
97
97
|
"types": "./dist/PlaceAutocomplete.svelte.d.ts",
|