places-autocomplete-svelte 2.2.14 → 2.2.16
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 +150 -142
- package/dist/PlaceAutocomplete.svelte +42 -60
- package/dist/gmaps.d.ts +11 -0
- package/dist/gmaps.js +23 -0
- package/dist/helpers.d.ts +22 -0
- package/dist/helpers.js +46 -0
- package/dist/interfaces.d.ts +6 -0
- package/package.json +25 -19
package/README.md
CHANGED
|
@@ -3,31 +3,28 @@
|
|
|
3
3
|
[](https://badge.fury.io/js/places-autocomplete-svelte)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
A flexible and
|
|
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.
|
|
6
|
+
A flexible, accessible, and secure [Svelte](https://kit.svelte.dev) component leveraging the [Google Maps Places Autocomplete API (New)](https://developers.google.com/maps/documentation/javascript/place-autocomplete-overview).
|
|
9
7
|
|
|
8
|
+
The component handles API loading, session tokens, debounced fetching, and accessibility, allowing you to focus on building your application. It intelligently manages the Google Maps API loader, creating a shared instance that prevents conflicts with other map components on the same page.
|
|
10
9
|
|
|
11
10
|
## Available: Standalone JavaScript Library
|
|
12
11
|
|
|
13
12
|
Need this functionality for a non-Svelte project? Check out our companion vanilla JavaScript library, `places-autocomplete-js`, which offers the same core Google Places (New) Autocomplete features.
|
|
14
13
|
[View `places-autocomplete-js` on GitHub](https://github.com/alexpechkarev/places-autocomplete-js)
|
|
15
14
|
|
|
16
|
-
|
|
17
15
|
## Features
|
|
18
16
|
|
|
19
|
-
* Integrates with the modern **
|
|
20
|
-
*
|
|
17
|
+
* Integrates with the modern **Google Maps Places Autocomplete API (New)**.
|
|
18
|
+
* **Automatic Shared Loader:** Intelligently creates a single Google Maps loader instance and shares it via Svelte's context.
|
|
19
|
+
* **Highly Accessible:** Follows WAI-ARIA patterns for comboboxes, with full keyboard navigation and screen reader support.
|
|
20
|
+
* **Secure:** Safely renders suggestions to protect against XSS attacks.
|
|
21
|
+
* Automatically handles **session tokens** for cost management.
|
|
21
22
|
* **Debounced Input:** Limits API calls while the user is typing (configurable).
|
|
22
|
-
* **Suggestion Highlighting:** Automatically highlights the portion of text matching the user's input
|
|
23
|
-
* **Imperative API:** Exposes `clear()`, `focus()`, and `getRequestParams()` methods for direct control
|
|
24
|
-
* **
|
|
23
|
+
* **Suggestion Highlighting:** Automatically highlights the portion of text matching the user's input.
|
|
24
|
+
* **Imperative API:** Exposes `clear()`, `focus()`, and `getRequestParams()` methods for direct control.
|
|
25
|
+
* **Customisable Styling:** Easily override default styles using the `options.classes` prop.
|
|
25
26
|
* **TypeScript Support:** Fully written in TypeScript with included type definitions.
|
|
26
27
|
* **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
|
-
|
|
30
|
-
|
|
31
28
|
|
|
32
29
|
## Demo
|
|
33
30
|
|
|
@@ -38,21 +35,13 @@ See a live demo of the component in action: [Basic Example](https://places-autoc
|
|
|
38
35
|
[Customise request parameters](https://places-autocomplete-demo.pages.dev/examples/customise-request-parameters) - construct a `requestParams` object and control various aspects of the search, including language, region, and more.
|
|
39
36
|
|
|
40
37
|
[Retain Input Value After Selection](https://places-autocomplete-demo.pages.dev/examples/retain-input-value) -
|
|
41
|
-
This example demonstrates how to configure the
|
|
42
|
-
|
|
38
|
+
This example demonstrates how to configure the component to keep the selected address visible in the input field after a suggestion is chosen.
|
|
43
39
|
|
|
44
40
|
<img src="places-autocomplete-svelte.gif" alt="A video demonstrating the Places Autocomplete Svelte component in action, showing address suggestions and selection.">
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
42
|
## Requirements
|
|
54
43
|
|
|
55
|
-
|
|
44
|
+
* **Google Maps API Key** with the "Places API" enabled. Refer to [Use API Keys](https://developers.google.com/maps/documentation/javascript/get-api-key) for detailed instructions.
|
|
56
45
|
|
|
57
46
|
## Installation
|
|
58
47
|
|
|
@@ -62,26 +51,22 @@ npm install places-autocomplete-svelte
|
|
|
62
51
|
yarn add places-autocomplete-svelte
|
|
63
52
|
```
|
|
64
53
|
|
|
54
|
+
## Usage
|
|
65
55
|
|
|
56
|
+
Provide your Google Maps API key to the component. It will automatically handle loading the required `places` library.
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
1. Replace `'___YOUR_API_KEY___'` with your actual **Google Maps API Key**.
|
|
70
|
-
2. Use the `onResponse` callback to **handle the response**.
|
|
71
|
-
|
|
72
|
-
```svelte
|
|
73
|
-
<script>
|
|
58
|
+
```javascript
|
|
59
|
+
<script lang="ts">
|
|
74
60
|
import { PlaceAutocomplete } from 'places-autocomplete-svelte';
|
|
75
|
-
import type { PlaceResult, ComponentOptions, RequestParams } from 'places-autocomplete-svelte/interfaces';
|
|
76
|
-
|
|
61
|
+
import type { PlaceResult, ComponentOptions, RequestParams } from 'places-autocomplete-svelte/interfaces';
|
|
77
62
|
|
|
78
63
|
// Get API Key securely (e.g., from environment variables)
|
|
79
64
|
const PUBLIC_GOOGLE_MAPS_API_KEY = import.meta.env.VITE_PUBLIC_GOOGLE_MAPS_API_KEY;
|
|
65
|
+
|
|
66
|
+
// --- Event Handlers ---
|
|
80
67
|
let fullResponse: PlaceResult | null = $state(null);
|
|
81
68
|
let placesError = $state('');
|
|
82
69
|
|
|
83
|
-
|
|
84
|
-
// --- Event Handlers ---
|
|
85
70
|
const handleResponse = (response: PlaceResult) => {
|
|
86
71
|
console.log('Place Selected:', response);
|
|
87
72
|
fullResponse = response;
|
|
@@ -95,31 +80,20 @@ const handleError = (error: string) => {
|
|
|
95
80
|
};
|
|
96
81
|
|
|
97
82
|
// --- Configuration (Optional) ---
|
|
98
|
-
|
|
99
|
-
// Control API request parameters
|
|
100
83
|
const requestParams: Partial<RequestParams> = $state({
|
|
101
|
-
region: 'GB',
|
|
84
|
+
region: 'GB',
|
|
102
85
|
language: 'en-GB',
|
|
103
|
-
// includedRegionCodes: ['GB'], // Example: Only show results in the specified regions,
|
|
104
|
-
// includedPrimaryTypes: ['address'], // Example: Only show addresses
|
|
105
86
|
});
|
|
106
|
-
|
|
107
|
-
// Control which data fields are fetched for Place Details (affects cost!)
|
|
108
87
|
const fetchFields: string[] = $state(['formattedAddress', 'addressComponents', 'displayName']);
|
|
109
|
-
|
|
110
|
-
// Control component appearance and behavior
|
|
111
88
|
const options: Partial<ComponentOptions> = $state({
|
|
112
89
|
placeholder: 'Start typing your address...',
|
|
113
|
-
debounce: 200,
|
|
114
|
-
distance: true, // Show distance if origin is provided in requestParams
|
|
90
|
+
debounce: 200,
|
|
115
91
|
classes: {
|
|
116
|
-
// Example: Override input styling and highlight class
|
|
117
92
|
input: 'my-custom-input-class border-blue-500',
|
|
118
|
-
highlight: 'bg-yellow-200 text-black',
|
|
93
|
+
highlight: 'bg-yellow-200 text-black',
|
|
119
94
|
},
|
|
120
|
-
clear_input: false,
|
|
95
|
+
clear_input: false,
|
|
121
96
|
});
|
|
122
|
-
|
|
123
97
|
</script>
|
|
124
98
|
|
|
125
99
|
{#if placesError}
|
|
@@ -143,12 +117,10 @@ const options: Partial<ComponentOptions> = $state({
|
|
|
143
117
|
{/if}
|
|
144
118
|
|
|
145
119
|
<style>
|
|
146
|
-
/* Example of styling an overridden class */
|
|
147
120
|
:global(.my-custom-input-class) {
|
|
148
121
|
padding: 0.75rem;
|
|
149
122
|
border-radius: 0.25rem;
|
|
150
123
|
width: 100%;
|
|
151
|
-
/* Add other styles */
|
|
152
124
|
}
|
|
153
125
|
.error-message {
|
|
154
126
|
color: red;
|
|
@@ -156,131 +128,167 @@ const options: Partial<ComponentOptions> = $state({
|
|
|
156
128
|
}
|
|
157
129
|
</style>
|
|
158
130
|
```
|
|
159
|
-
## Props
|
|
160
|
-
| Prop | Type | Required | Default | Description |
|
|
161
|
-
|----------------------------|---------------------------------|----------|-------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
|
|
162
|
-
| PUBLIC_GOOGLE_MAPS_API_KEY | string | Yes | - | Your Google Maps API Key with Places API enabled. |
|
|
163
|
-
| fetchFields | string[] | No | ['formattedAddress', 'addressComponents'] | Array of Place Data Fields to request when a place is selected. Affects API cost. |
|
|
164
|
-
| requestParams | Partial<RequestParams> | No | { inputOffset: 3, ... } | Parameters for the Autocomplete request. See AutocompletionRequest options. |
|
|
165
|
-
| options | Partial<ComponentOptions> | No | { debounce: 100, ... } | Options to control component behavior and appearance. See details below. |
|
|
166
|
-
| onResponse | (response: PlaceResult) => void | Yes | - | Callback function triggered with the selected place details (PlaceResult object) after fetchFields is complete. |
|
|
167
|
-
| onError | (error: string) => void | Yes | - | Callback function triggered when an error occurs (API loading, fetching suggestions, fetching details). |
|
|
168
131
|
|
|
169
|
-
|
|
170
|
-
----------------------------------
|
|
132
|
+
### Advanced: Using with other Google Maps Libraries
|
|
171
133
|
|
|
172
|
-
|
|
134
|
+
You can reuse the shared Google Maps loader created by the `PlaceAutocomplete` component to load other libraries (like `maps`). Because the loader instance is shared, you can access it from any other component to load additional libraries without causing conflicts.
|
|
173
135
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
136
|
+
The `PlaceAutocomplete` component only loads the `places` library by default.
|
|
137
|
+
```javascript
|
|
138
|
+
// In a parent component, e.g., src/routes/+page.svelte
|
|
177
139
|
<script lang="ts">
|
|
178
|
-
|
|
179
|
-
|
|
140
|
+
import { onMount } from 'svelte';
|
|
141
|
+
import { getGMapsLoader } from 'places-autocomplete-svelte/gmaps';
|
|
142
|
+
import PlaceAutocomplete from '$lib/PlaceAutocomplete.svelte';
|
|
143
|
+
|
|
144
|
+
const PUBLIC_GOOGLE_MAPS_API_KEY = import.meta.env.VITE_PUBLIC_GOOGLE_MAPS_API_KEY;
|
|
145
|
+
|
|
146
|
+
// Pre-initialise the loader with all libraries needed for this page.
|
|
147
|
+
onMount(async () => {
|
|
148
|
+
const loader = getGMapsLoader(PUBLIC_GOOGLE_MAPS_API_KEY);
|
|
149
|
+
const { Map } = await loader.importLibrary('maps');
|
|
150
|
+
...
|
|
151
|
+
});
|
|
180
152
|
</script>
|
|
181
153
|
|
|
182
|
-
|
|
154
|
+
<!-- The component will now use the loader you created above -->
|
|
155
|
+
<PlaceAutocomplete
|
|
156
|
+
{PUBLIC_GOOGLE_MAPS_API_KEY}
|
|
157
|
+
onResponse={...}
|
|
158
|
+
onError={...}
|
|
159
|
+
/>
|
|
183
160
|
|
|
184
|
-
|
|
185
|
-
<
|
|
186
|
-
<button onclick={() => console.log(JSON.stringify(autocompleteComponent.getRequestParams()))}>Get Request Params</button>
|
|
161
|
+
<!-- You can now use other Google Maps services, e.g., a map -->
|
|
162
|
+
<div id="map"></div>
|
|
187
163
|
```
|
|
188
164
|
|
|
189
|
-
|
|
165
|
+
## Security
|
|
190
166
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
167
|
+
### API Key Security
|
|
168
|
+
|
|
169
|
+
Your Google Maps API Key is a sensitive credential. To prevent unauthorised use and unexpected charges, you **must** restrict it.
|
|
170
|
+
|
|
171
|
+
1. Go to the [Google Cloud Console](https://console.cloud.google.com/google/maps-apis/credentials).
|
|
172
|
+
2. Select your API key.
|
|
173
|
+
3. Under **Application restrictions**, select **HTTP referrers (web sites)** and add your application's domain(s) (e.g., `your-domain.com/*`).
|
|
174
|
+
4. Under **API restrictions**, select **Restrict key** and choose the APIs you are using (e.g., **Places API**, **Maps JavaScript API**).
|
|
175
|
+
|
|
176
|
+
### XSS Protection
|
|
177
|
+
|
|
178
|
+
This component is designed to be secure out-of-the-box. It safely renders user-input and API responses to prevent Cross-Site Scripting (XSS) vulnerabilities.
|
|
179
|
+
|
|
180
|
+
## Accessibility
|
|
181
|
+
|
|
182
|
+
This component is built to be accessible and follows the [WAI-ARIA Authoring Practices for a Combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
|
|
183
|
+
|
|
184
|
+
* **Keyboard Navigation:** Users can navigate suggestions using `ArrowUp`, `ArrowDown`, select with `Enter`, and close the list with `Escape`.
|
|
185
|
+
* **Screen Reader Support:** Uses `role="combobox"`, `aria-autocomplete`, `aria-expanded`, and `aria-activedescendant` to provide a clear experience for screen reader users.
|
|
186
|
+
* **Focus Management:** Focus remains on the input field while navigating the suggestion list.
|
|
187
|
+
|
|
188
|
+
## Props
|
|
196
189
|
|
|
197
|
-
|
|
190
|
+
| Prop | Type | Required | Default | Description |
|
|
191
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
192
|
+
| `PUBLIC_GOOGLE_MAPS_API_KEY` | `string` | Yes | - | Your restricted Google Maps API Key. |
|
|
193
|
+
| `fetchFields` | `string[]` | No | `['formattedAddress', 'addressComponents']` | Place Data Fields to request. **Affects API cost.** |
|
|
194
|
+
| `requestParams` | `Partial<RequestParams>` | No | `{ inputOffset: 3, ... }` | Parameters for the Autocomplete API request. |
|
|
195
|
+
| `options` | `Partial<ComponentOptions>` | No | `{ debounce: 100, ... }` | Options to control component behavior and appearance. |
|
|
196
|
+
| `onResponse` | `(response: PlaceResult) => void` | Yes | - | Callback triggered with the selected place details. |
|
|
197
|
+
| `onError` | `(error: string) => void` | Yes | - | Callback triggered when an error occurs. |
|
|
198
198
|
|
|
199
|
+
## Component Methods (Imperative API)
|
|
199
200
|
|
|
200
|
-
|
|
201
|
-
|----------------|---------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------|
|
|
202
|
-
| placeholder | string | '' | Placeholder text for the input field. |
|
|
203
|
-
| debounce | number | 100 | Delay in milliseconds before triggering autocomplete API request after user stops typing. Set to 0 to disable debouncing. |
|
|
204
|
-
| distance | boolean | true | Show distance from requestParams.origin in suggestions (if origin is provided). |
|
|
205
|
-
| distance_units | 'km' \| 'miles' | 'km' | Units to display distance in. |
|
|
206
|
-
| label | string | '' | Optional label text displayed above the input field. |
|
|
207
|
-
| autofocus | boolean | false | Automatically focus the input field on mount. |
|
|
208
|
-
| autocomplete | string | 'off' | Standard HTML autocomplete attribute for the input field. |
|
|
209
|
-
| classes | Partial<ComponentClasses> | {} | Object to override default CSS classes for various component parts. See Styling (options.classes) section for keys. | |
|
|
210
|
-
| clear_input | Boolean | true | If `true` (default), clears the input field after a suggestion is selected. If `false`, the input field retains the `formattedAddress` of the selected place. |
|
|
201
|
+
Get a reference to the component instance using `bind:this` to call its methods directly.
|
|
211
202
|
|
|
203
|
+
**Example:**
|
|
212
204
|
|
|
205
|
+
```javascript
|
|
206
|
+
<script lang="ts">
|
|
207
|
+
import PlaceAutocomplete from 'places-autocomplete-svelte';
|
|
208
|
+
let autocompleteComponent: PlaceAutocomplete | undefined = $state(undefined);
|
|
209
|
+
</script>
|
|
210
|
+
|
|
211
|
+
<PlaceAutocomplete bind:this={autocompleteComponent} ... />
|
|
212
|
+
|
|
213
|
+
<button onclick={() => autocompleteComponent?.clear()}>Clear</button>
|
|
214
|
+
<button onclick={() => autocompleteComponent?.focus()}>Focus</button>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
| Method | Signature | Description |
|
|
218
|
+
| :--- | :--- | :--- |
|
|
219
|
+
| `clear()` | `() => void` | Clears the input, removes suggestions, and resets the session token. |
|
|
220
|
+
| `focus()` | `() => void` | Sets focus on the text input field. |
|
|
221
|
+
| `getRequestParams()` | `() => RequestParams` | Returns the current internal `requestParams` object. |
|
|
213
222
|
|
|
223
|
+
## Options
|
|
214
224
|
|
|
215
|
-
|
|
216
|
-
|
|
225
|
+
| Option | Type | Default | Description |
|
|
226
|
+
| :--- | :--- | :--- | :--- |
|
|
227
|
+
| `placeholder` | `string` | `''` | Placeholder text for the input field. |
|
|
228
|
+
| `debounce` | `number` | `100` | Delay in ms before firing API request. Set to `0` to disable. |
|
|
229
|
+
| `distance` | `boolean` | `true` | Show distance from `requestParams.origin` (if provided). |
|
|
230
|
+
| `distance_units` | `'km' \| 'miles'` | `'km'` | Units for displaying distance. |
|
|
231
|
+
| `label` | `string` | `''` | Optional label text displayed above the input. |
|
|
232
|
+
| `autofocus` | `boolean` | `false` | Automatically focus the input on mount. |
|
|
233
|
+
| `autocomplete` | `string` | `'off'` | The `autocomplete` attribute for the input field. |
|
|
234
|
+
| `classes` | `Partial<ComponentClasses>` | `{}` | Object to override default CSS classes. See Styling section. |
|
|
235
|
+
| `clear_input` | `boolean` | `true` | If `false`, retains the `formattedAddress` in the input after selection. |
|
|
217
236
|
|
|
218
|
-
|
|
237
|
+
## Styling (`options.classes`)
|
|
219
238
|
|
|
239
|
+
Customise the component by providing your own CSS classes via `options.classes`.
|
|
220
240
|
|
|
221
241
|
**Available Class Keys:**
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
- `highlight`: The class applied to the `<span>` wrapping the matched text within suggestions. Defaults to `'font-bold'`.
|
|
245
|
-
|
|
246
|
-
### Example:
|
|
242
|
+
|
|
243
|
+
* `section`: The main container `section`.
|
|
244
|
+
* `container`: The `div` containing the input and suggestions list.
|
|
245
|
+
* `label`: The `label` element.
|
|
246
|
+
* `input`: The main text `input` element.
|
|
247
|
+
* `icon_container`: Container for the optional icon.
|
|
248
|
+
* `icon`: SVG string for the icon.
|
|
249
|
+
* `ul`: The `<ul>` element for the suggestions list.
|
|
250
|
+
* `li`: Each `<li>` suggestion item.
|
|
251
|
+
* `li_current`: Class added to the currently highlighted `<li>`.
|
|
252
|
+
* `li_div_container`: Container `div` within each list item.
|
|
253
|
+
* `li_div_one`: First inner `div` (contains the main text).
|
|
254
|
+
* `li_div_one_p`: The `<p>` tag containing the main suggestion text.
|
|
255
|
+
* `li_div_two`: Second inner `div` (contains the distance).
|
|
256
|
+
* `li_div_two_p`: The `<p>` tag containing the distance text.
|
|
257
|
+
* `kbd_container`: Container for the keyboard hint keys.
|
|
258
|
+
* `kbd_escape`: The `<kbd>` tag for the 'Esc' hint.
|
|
259
|
+
* `kbd_up`: The `<kbd>` tag for the 'Up Arrow' hint.
|
|
260
|
+
* `kbd_down`: The `<kbd>` tag for the 'Down Arrow' hint.
|
|
261
|
+
* `highlight`: The class applied to the `<span>` wrapping the matched text. Defaults to `'font-bold'`.
|
|
262
|
+
|
|
263
|
+
**Example:**
|
|
247
264
|
|
|
248
265
|
```javascript
|
|
249
266
|
const options = {
|
|
250
267
|
classes: {
|
|
251
|
-
input: 'form-input w-full rounded-md shadow-sm',
|
|
252
|
-
ul: 'absolute bg-white shadow-lg rounded-md mt-1 w-full z-10',
|
|
253
|
-
li_current: 'bg-blue-500 text-white',
|
|
254
|
-
highlight: 'text-blue-700 font-semibold'
|
|
268
|
+
input: 'form-input w-full rounded-md shadow-sm',
|
|
269
|
+
ul: 'absolute bg-white shadow-lg rounded-md mt-1 w-full z-10',
|
|
270
|
+
li_current: 'bg-blue-500 text-white',
|
|
271
|
+
highlight: 'text-blue-700 font-semibold'
|
|
255
272
|
}
|
|
256
273
|
};
|
|
257
274
|
```
|
|
258
275
|
|
|
259
|
-
Events
|
|
260
|
-
------
|
|
261
|
-
|
|
262
|
-
- **`onResponse`**: `(response: PlaceResult) => void`
|
|
263
|
-
- Fired after a user selects a suggestion and the requested `fetchFields` have been successfully retrieved.
|
|
264
|
-
- 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.
|
|
265
|
-
- **`onError`**: `(error: string) => void`
|
|
266
|
-
- Fired if there's an error loading the Google Maps API, fetching autocomplete suggestions, or fetching place details.
|
|
267
|
-
- The `error` argument is a string describing the error.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
TypeScript
|
|
271
|
-
----------
|
|
276
|
+
## Events
|
|
272
277
|
|
|
273
|
-
|
|
278
|
+
* **`onResponse`**: `(response: PlaceResult) => void`
|
|
279
|
+
* Fired after a user selects a suggestion and `fetchFields` are retrieved.
|
|
280
|
+
* **`onError`**: `(error: string) => void`
|
|
281
|
+
* Fired on any error (API loading, fetching suggestions, etc.).
|
|
274
282
|
|
|
283
|
+
## TypeScript
|
|
275
284
|
|
|
285
|
+
This component is written in TypeScript. Import types from `places-autocomplete-svelte/interfaces` and helpers from `places-autocomplete-svelte/gmaps`.
|
|
276
286
|
|
|
277
|
-
Google Places API & Billing
|
|
278
|
-
---------------------------
|
|
287
|
+
## Google Places API & Billing
|
|
279
288
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
- 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).
|
|
289
|
+
* This component uses the Google Maps JavaScript API (Places library). Usage is subject to Google's terms and pricing.
|
|
290
|
+
* It uses **Session Tokens** automatically to group Autocomplete requests, which can reduce costs.
|
|
291
|
+
* Place Details requests (via `fetchFields`) are billed separately. Only request the fields you need to manage costs.
|
|
284
292
|
|
|
285
293
|
## Contributing
|
|
286
294
|
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
|
-
import * as GMaps from '@googlemaps/js-api-loader';
|
|
4
3
|
import type { PlaceResult, Props } from './interfaces.js';
|
|
5
4
|
import {
|
|
6
5
|
validateOptions,
|
|
7
6
|
validateRequestParams,
|
|
8
7
|
formatDistance,
|
|
9
8
|
validateFetchFields,
|
|
9
|
+
createHighlightedSegments,
|
|
10
|
+
debounce
|
|
10
11
|
} from './helpers.js';
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
import { getGMapsLoader, type GMapsLoaderType } from './gmaps.js';
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
let {
|
|
14
17
|
/**
|
|
@@ -33,14 +36,15 @@
|
|
|
33
36
|
fetchFields = validateFetchFields(fetchFields);
|
|
34
37
|
//console.log(fetchFields);
|
|
35
38
|
|
|
39
|
+
|
|
36
40
|
let kbdAction = $state(''); // 'up', 'down', or 'escape'
|
|
37
41
|
|
|
38
42
|
// Local variables
|
|
39
43
|
let inputRef: HTMLInputElement;
|
|
40
44
|
let currentSuggestion = $state(-1);
|
|
41
45
|
let results: any[] = $state([]);
|
|
42
|
-
let loader: GMaps.Loader;
|
|
43
46
|
let placesApi: { [key: string]: any } = {};
|
|
47
|
+
let loader: GMapsLoaderType;
|
|
44
48
|
|
|
45
49
|
//https://developers.google.com/maps/documentation/javascript/reference/autocomplete-data
|
|
46
50
|
// validate requestParams
|
|
@@ -102,28 +106,6 @@
|
|
|
102
106
|
return request;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
/**
|
|
106
|
-
* Debounce function to limit the rate at which a function can fire.
|
|
107
|
-
* @param func
|
|
108
|
-
* @param wait
|
|
109
|
-
*/
|
|
110
|
-
function debounce<T extends (...args: any[]) => any>(
|
|
111
|
-
func: T,
|
|
112
|
-
wait: number
|
|
113
|
-
): (...args: Parameters<T>) => void {
|
|
114
|
-
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
115
|
-
return function executedFunction(...args: Parameters<T>) {
|
|
116
|
-
const later = () => {
|
|
117
|
-
timeout = null;
|
|
118
|
-
func(...args);
|
|
119
|
-
};
|
|
120
|
-
if (timeout !== null) {
|
|
121
|
-
clearTimeout(timeout);
|
|
122
|
-
}
|
|
123
|
-
timeout = setTimeout(later, wait);
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
109
|
/**
|
|
128
110
|
* Make request and get autocomplete suggestions.
|
|
129
111
|
* @param event
|
|
@@ -164,30 +146,15 @@
|
|
|
164
146
|
const matches = predictionText.matches;
|
|
165
147
|
|
|
166
148
|
//Highlighting Logic
|
|
167
|
-
let highlightedText =
|
|
168
|
-
let lastIndex = 0;
|
|
149
|
+
let highlightedText: { text: string; highlighted: boolean }[] = [];
|
|
169
150
|
|
|
170
151
|
// Sort matches just in case they aren't ordered (though they usually are)
|
|
171
152
|
matches.sort(
|
|
172
153
|
(a: { startOffset: number }, b: { startOffset: number }) => a.startOffset - b.startOffset
|
|
173
154
|
);
|
|
174
|
-
for (const match of matches) {
|
|
175
|
-
// Append text before the current match
|
|
176
|
-
highlightedText += originalText.substring(lastIndex, match.startOffset);
|
|
177
|
-
|
|
178
|
-
// Append the highlighted match segment
|
|
179
|
-
// Choose your highlighting class (e.g., 'font-bold' or a custom one)
|
|
180
|
-
highlightedText += `<span class="${options.classes?.highlight ?? 'font-bold'}">`;
|
|
181
|
-
highlightedText += originalText.substring(match.startOffset, match.endOffset);
|
|
182
|
-
highlightedText += `</span>`;
|
|
183
|
-
|
|
184
|
-
// Update the last index processed
|
|
185
|
-
lastIndex = match.endOffset;
|
|
186
|
-
}
|
|
187
155
|
|
|
188
|
-
//
|
|
189
|
-
highlightedText
|
|
190
|
-
// --- End Highlighting Logic ---
|
|
156
|
+
// Create highlighted segments
|
|
157
|
+
highlightedText = createHighlightedSegments(originalText, matches);
|
|
191
158
|
|
|
192
159
|
results.push({
|
|
193
160
|
place: suggestion.placePrediction.toPlace(),
|
|
@@ -256,25 +223,24 @@
|
|
|
256
223
|
inputRef.focus();
|
|
257
224
|
}
|
|
258
225
|
|
|
259
|
-
// load the Google Maps API
|
|
260
226
|
try {
|
|
261
|
-
loader =
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
227
|
+
loader = getGMapsLoader(PUBLIC_GOOGLE_MAPS_API_KEY);
|
|
228
|
+
} catch (e: any) {
|
|
229
|
+
onError(
|
|
230
|
+
(e.name || 'An error occurred') + ' - ' + (e.message || 'Error loading Google Maps API')
|
|
231
|
+
);
|
|
232
|
+
}
|
|
266
233
|
|
|
234
|
+
try {
|
|
267
235
|
const { AutocompleteSessionToken, AutocompleteSuggestion } =
|
|
268
236
|
await loader.importLibrary('places');
|
|
269
237
|
|
|
270
238
|
placesApi.AutocompleteSessionToken = AutocompleteSessionToken;
|
|
271
239
|
placesApi.AutocompleteSuggestion = AutocompleteSuggestion;
|
|
272
240
|
|
|
273
|
-
// const {Geocoder} = await loader.importLibrary("geocoding");
|
|
274
|
-
// placesApi.Geocoder = new Geocoder();
|
|
275
|
-
|
|
276
241
|
setSessionToken();
|
|
277
242
|
} catch (e: any) {
|
|
243
|
+
console.log(e);
|
|
278
244
|
onError(
|
|
279
245
|
(e.name || 'An error occurred') + ' - ' + (e.message || 'Error loading Google Maps API')
|
|
280
246
|
);
|
|
@@ -318,11 +284,19 @@
|
|
|
318
284
|
}
|
|
319
285
|
</script>
|
|
320
286
|
|
|
321
|
-
<svelte:window
|
|
322
|
-
|
|
323
|
-
<section
|
|
287
|
+
<svelte:window onclick={handleClickOutside} />
|
|
288
|
+
|
|
289
|
+
<section
|
|
290
|
+
class={options.classes?.section}
|
|
291
|
+
role="combobox"
|
|
292
|
+
aria-controls="autocomplete-listbox"
|
|
293
|
+
tabindex="0"
|
|
294
|
+
aria-haspopup="listbox"
|
|
295
|
+
aria-expanded={results.length > 0}
|
|
296
|
+
aria-owns="autocomplete-listbox"
|
|
297
|
+
>
|
|
324
298
|
{#if options?.label ?? ''}
|
|
325
|
-
<label for="
|
|
299
|
+
<label for="places-autocomplete-input" class={options.classes?.label ?? ''}>
|
|
326
300
|
{options.label}
|
|
327
301
|
</label>
|
|
328
302
|
{/if}
|
|
@@ -334,6 +308,7 @@
|
|
|
334
308
|
{/if}
|
|
335
309
|
|
|
336
310
|
<input
|
|
311
|
+
id="places-autocomplete-input"
|
|
337
312
|
type="text"
|
|
338
313
|
name="search"
|
|
339
314
|
bind:this={inputRef}
|
|
@@ -346,7 +321,9 @@
|
|
|
346
321
|
aria-labelledby="search"
|
|
347
322
|
aria-label="Search"
|
|
348
323
|
aria-haspopup="listbox"
|
|
324
|
+
aria-activedescendant={currentSuggestion > -1 ? `option-${currentSuggestion + 1}` : undefined}
|
|
349
325
|
bind:value={request.input}
|
|
326
|
+
onkeydown={onKeyDown}
|
|
350
327
|
oninput={(event) => debouncedMakeAcRequest(event.currentTarget.value)}
|
|
351
328
|
/>
|
|
352
329
|
<!-- oninput={makeAcRequest} -->
|
|
@@ -362,7 +339,7 @@
|
|
|
362
339
|
>⇓</kbd
|
|
363
340
|
>
|
|
364
341
|
</div>
|
|
365
|
-
<ul class={options.classes?.ul ?? ''} id="
|
|
342
|
+
<ul class={options.classes?.ul ?? ''} id="autocomplete-listbox" role="listbox">
|
|
366
343
|
{#each results as p, i}
|
|
367
344
|
<li
|
|
368
345
|
role="option"
|
|
@@ -374,7 +351,6 @@
|
|
|
374
351
|
onmouseenter={() => (currentSuggestion = i)}
|
|
375
352
|
id="option-{i + 1}"
|
|
376
353
|
>
|
|
377
|
-
<!-- svelte-ignore a11y_invalid_attribute -->
|
|
378
354
|
<button
|
|
379
355
|
type="button"
|
|
380
356
|
class={[
|
|
@@ -396,8 +372,14 @@
|
|
|
396
372
|
options.classes?.li_div_one_p
|
|
397
373
|
]}
|
|
398
374
|
>
|
|
399
|
-
|
|
400
|
-
|
|
375
|
+
{#each p.text as segment}
|
|
376
|
+
{#if segment.highlighted}
|
|
377
|
+
<span class={options.classes?.highlight ?? 'font-bold'}>{segment.text}</span
|
|
378
|
+
>
|
|
379
|
+
{:else}
|
|
380
|
+
{segment.text}
|
|
381
|
+
{/if}
|
|
382
|
+
{/each}
|
|
401
383
|
</p>
|
|
402
384
|
</div>
|
|
403
385
|
</div>
|
package/dist/gmaps.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as GMaps from '@googlemaps/js-api-loader';
|
|
2
|
+
/**
|
|
3
|
+
* Defines the shape of the context object that will be shared.
|
|
4
|
+
* This can be expanded if you need to share more than just the loader.
|
|
5
|
+
*/
|
|
6
|
+
export type GMapsLoaderType = GMaps.Loader;
|
|
7
|
+
/**
|
|
8
|
+
* Gets the Google Maps Loader instance from Svelte's context.
|
|
9
|
+
* @returns {typeof Loader}
|
|
10
|
+
*/
|
|
11
|
+
export declare const getGMapsLoader: (PUBLIC_GOOGLE_MAPS_API_KEY: string, version?: string | undefined) => GMaps.Loader;
|
package/dist/gmaps.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getContext, setContext, hasContext } from 'svelte';
|
|
2
|
+
import * as GMaps from '@googlemaps/js-api-loader';
|
|
3
|
+
const { Loader } = GMaps;
|
|
4
|
+
/**
|
|
5
|
+
* A unique key for setting and getting the Google Maps Loader instance from Svelte's context.
|
|
6
|
+
* This allows multiple components to share a single loader instance.
|
|
7
|
+
*/
|
|
8
|
+
const gmapsContextKey = Symbol('pacgmaps');
|
|
9
|
+
/**
|
|
10
|
+
* Gets the Google Maps Loader instance from Svelte's context.
|
|
11
|
+
* @returns {typeof Loader}
|
|
12
|
+
*/
|
|
13
|
+
export const getGMapsLoader = (PUBLIC_GOOGLE_MAPS_API_KEY, version) => {
|
|
14
|
+
version = version || 'weekly';
|
|
15
|
+
if (!hasContext(gmapsContextKey)) {
|
|
16
|
+
const loader = new Loader({
|
|
17
|
+
apiKey: PUBLIC_GOOGLE_MAPS_API_KEY,
|
|
18
|
+
version: version,
|
|
19
|
+
});
|
|
20
|
+
setContext(gmapsContextKey, loader);
|
|
21
|
+
}
|
|
22
|
+
return getContext(gmapsContextKey);
|
|
23
|
+
};
|
package/dist/helpers.d.ts
CHANGED
|
@@ -41,3 +41,25 @@ export declare const validateOptions: (options: ComponentOptions | undefined) =>
|
|
|
41
41
|
* @returns
|
|
42
42
|
*/
|
|
43
43
|
export declare const formatDistance: (distance: number, units: DistanceUnits) => string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Create highlighted segments from the original text based on the provided matches.
|
|
46
|
+
* @param originalText The original text to segment.
|
|
47
|
+
* @param matches An array of match objects containing start and end offsets.
|
|
48
|
+
* @returns An array of text segments with highlighting information.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createHighlightedSegments(originalText: string, matches: {
|
|
51
|
+
startOffset: number;
|
|
52
|
+
endOffset: number;
|
|
53
|
+
}[]): {
|
|
54
|
+
text: string;
|
|
55
|
+
highlighted: boolean;
|
|
56
|
+
}[];
|
|
57
|
+
/**
|
|
58
|
+
* Debounce function that takes a function and a delay
|
|
59
|
+
* and returns a new function that will only execute
|
|
60
|
+
* after the delay has passed without any new calls.
|
|
61
|
+
* This version is generic and preserves the types of the original function.
|
|
62
|
+
* @param func The function to debounce.
|
|
63
|
+
* @param delay The debounce delay in milliseconds.
|
|
64
|
+
*/
|
|
65
|
+
export declare const debounce: <T extends (...args: any[]) => void>(func: T, delay: number) => (...args: Parameters<T>) => void;
|
package/dist/helpers.js
CHANGED
|
@@ -363,3 +363,49 @@ export const formatDistance = function (distance, units) {
|
|
|
363
363
|
return `${(distance / 1609.34).toFixed(2)} miles`;
|
|
364
364
|
}
|
|
365
365
|
};
|
|
366
|
+
/**
|
|
367
|
+
* Create highlighted segments from the original text based on the provided matches.
|
|
368
|
+
* @param originalText The original text to segment.
|
|
369
|
+
* @param matches An array of match objects containing start and end offsets.
|
|
370
|
+
* @returns An array of text segments with highlighting information.
|
|
371
|
+
*/
|
|
372
|
+
export function createHighlightedSegments(originalText, matches) {
|
|
373
|
+
const segments = [];
|
|
374
|
+
if (!originalText || !matches)
|
|
375
|
+
return segments;
|
|
376
|
+
let lastIndex = 0;
|
|
377
|
+
// Sort matches just in case they aren't ordered
|
|
378
|
+
matches.sort((a, b) => a.startOffset - b.startOffset);
|
|
379
|
+
for (const match of matches) {
|
|
380
|
+
// Add text before the match
|
|
381
|
+
if (match.startOffset > lastIndex) {
|
|
382
|
+
segments.push({ text: originalText.substring(lastIndex, match.startOffset), highlighted: false });
|
|
383
|
+
}
|
|
384
|
+
// Add the matched text
|
|
385
|
+
segments.push({ text: originalText.substring(match.startOffset, match.endOffset), highlighted: true });
|
|
386
|
+
lastIndex = match.endOffset;
|
|
387
|
+
}
|
|
388
|
+
// Add any remaining text after the last match
|
|
389
|
+
if (lastIndex < originalText.length) {
|
|
390
|
+
segments.push({ text: originalText.substring(lastIndex), highlighted: false });
|
|
391
|
+
}
|
|
392
|
+
return segments;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Debounce function that takes a function and a delay
|
|
396
|
+
* and returns a new function that will only execute
|
|
397
|
+
* after the delay has passed without any new calls.
|
|
398
|
+
* This version is generic and preserves the types of the original function.
|
|
399
|
+
* @param func The function to debounce.
|
|
400
|
+
* @param delay The debounce delay in milliseconds.
|
|
401
|
+
*/
|
|
402
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
403
|
+
export const debounce = (func, delay) => {
|
|
404
|
+
let timeout;
|
|
405
|
+
return (...args) => {
|
|
406
|
+
clearTimeout(timeout);
|
|
407
|
+
timeout = setTimeout(() => {
|
|
408
|
+
func(...args);
|
|
409
|
+
}, delay);
|
|
410
|
+
};
|
|
411
|
+
};
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -48,6 +48,11 @@ export interface PlaceResult {
|
|
|
48
48
|
shortText: string;
|
|
49
49
|
types: string[];
|
|
50
50
|
}[];
|
|
51
|
+
location?: {
|
|
52
|
+
lat: number;
|
|
53
|
+
lng: number;
|
|
54
|
+
};
|
|
55
|
+
[key: string]: unknown;
|
|
51
56
|
}
|
|
52
57
|
export interface FormattedAddress {
|
|
53
58
|
street_number: string;
|
|
@@ -61,6 +66,7 @@ export interface Props {
|
|
|
61
66
|
PUBLIC_GOOGLE_MAPS_API_KEY: string;
|
|
62
67
|
options?: ComponentOptions;
|
|
63
68
|
fetchFields?: string[];
|
|
69
|
+
libraries?: string[];
|
|
64
70
|
requestParams?: RequestParams;
|
|
65
71
|
onResponse: (response: PlaceResult) => void;
|
|
66
72
|
onError: (error: string) => void;
|
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 flexible and
|
|
4
|
+
"version": "2.2.16",
|
|
5
|
+
"description": "A flexible, accessible, and secure Svelte component leveraging the Google Maps Places Autocomplete API (New) to provide a user-friendly way to search for and retrieve detailed address information.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"svelte",
|
|
8
8
|
"sveltekit",
|
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
"svelte": "./dist/interfaces.js",
|
|
53
53
|
"default": "./dist/interfaces.js"
|
|
54
54
|
},
|
|
55
|
+
"./gmaps": {
|
|
56
|
+
"types": "./dist/gmaps.d.ts",
|
|
57
|
+
"svelte": "./dist/gmaps.js"
|
|
58
|
+
},
|
|
55
59
|
".": {
|
|
56
60
|
"types": "./dist/index.d.ts",
|
|
57
61
|
"svelte": "./dist/index.js",
|
|
@@ -71,36 +75,38 @@
|
|
|
71
75
|
"svelte": "^5.0.0"
|
|
72
76
|
},
|
|
73
77
|
"devDependencies": {
|
|
74
|
-
"@sveltejs/adapter-auto": "^6.0
|
|
75
|
-
"@sveltejs/adapter-cloudflare": "^7.
|
|
76
|
-
"@sveltejs/kit": "^2.
|
|
77
|
-
"@sveltejs/package": "^2.
|
|
78
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
79
|
-
"@tailwindcss/postcss": "^4.1.
|
|
78
|
+
"@sveltejs/adapter-auto": "^6.1.0",
|
|
79
|
+
"@sveltejs/adapter-cloudflare": "^7.2.2",
|
|
80
|
+
"@sveltejs/kit": "^2.36.1",
|
|
81
|
+
"@sveltejs/package": "^2.5.0",
|
|
82
|
+
"@sveltejs/vite-plugin-svelte": "^6.1.3",
|
|
83
|
+
"@tailwindcss/postcss": "^4.1.12",
|
|
80
84
|
"@tailwindcss/typography": "^0.5.16",
|
|
81
|
-
"@tailwindcss/vite": "^4.1.
|
|
85
|
+
"@tailwindcss/vite": "^4.1.12",
|
|
82
86
|
"@types/eslint": "^9.6.1",
|
|
87
|
+
"@types/google.maps": "^3.58.1",
|
|
88
|
+
"@types/node": "^24.3.0",
|
|
83
89
|
"autoprefixer": "^10.4.21",
|
|
84
|
-
"eslint": "^9.
|
|
85
|
-
"eslint-config-prettier": "^10.1.
|
|
86
|
-
"eslint-plugin-svelte": "^3.
|
|
90
|
+
"eslint": "^9.34.0",
|
|
91
|
+
"eslint-config-prettier": "^10.1.8",
|
|
92
|
+
"eslint-plugin-svelte": "^3.11.0",
|
|
87
93
|
"globals": "^16.3.0",
|
|
88
94
|
"postcss": "^8.5.6",
|
|
89
95
|
"prettier": "^3.6.2",
|
|
90
96
|
"prettier-plugin-svelte": "^3.4.0",
|
|
91
97
|
"publint": "^0.3.12",
|
|
92
|
-
"svelte": "^5.
|
|
93
|
-
"svelte-check": "^4.
|
|
94
|
-
"tailwindcss": "^4.1.
|
|
98
|
+
"svelte": "^5.38.2",
|
|
99
|
+
"svelte-check": "^4.3.1",
|
|
100
|
+
"tailwindcss": "^4.1.12",
|
|
95
101
|
"tslib": "^2.8.1",
|
|
96
|
-
"typescript": "^5.
|
|
97
|
-
"typescript-eslint": "^8.
|
|
98
|
-
"vite": "^
|
|
102
|
+
"typescript": "^5.9.2",
|
|
103
|
+
"typescript-eslint": "^8.40.0",
|
|
104
|
+
"vite": "^7.1.3"
|
|
99
105
|
},
|
|
100
106
|
"svelte": "./dist/index.js",
|
|
101
107
|
"types": "./dist/PlaceAutocomplete.svelte.d.ts",
|
|
102
108
|
"type": "module",
|
|
103
109
|
"dependencies": {
|
|
104
|
-
"@googlemaps/js-api-loader": "^1.16.
|
|
110
|
+
"@googlemaps/js-api-loader": "^1.16.10"
|
|
105
111
|
}
|
|
106
112
|
}
|