places-autocomplete-svelte 2.1.9 → 2.2.1
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 +8 -3
- package/dist/PlaceAutocomplete.svelte +6 -20
- package/dist/helpers.d.ts +8 -1
- package/dist/helpers.js +125 -116
- package/dist/interfaces.d.ts +7 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,10 @@ See a live demo of the component in action: [Basic Example](https://places-autoc
|
|
|
24
24
|
[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.
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
<video src="https://github.com/user-attachments/assets/c2913d06-05d2-4b93-afba-379209d9ddc9" width="660" height="764" controls autoplay loop muted>
|
|
28
|
+
</video>
|
|
29
|
+
|
|
30
|
+
|
|
28
31
|
|
|
29
32
|
## Requirements
|
|
30
33
|
|
|
@@ -89,7 +92,8 @@ let onResponse = (response) => {
|
|
|
89
92
|
| `autofocus` | `boolean` | If `true`, the input field will be focused automatically when the component mounts. | `false` |
|
|
90
93
|
| `placeholder` | `String` | Placeholder text for the input field. | `"Search..."` |
|
|
91
94
|
| `autocomplete`| `string` | HTML `autocomplete` attribute for the input field. Set to `"off"` to disable browser autocomplete. | `"off"` |
|
|
92
|
-
| `
|
|
95
|
+
| `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` |
|
|
96
|
+
| `distance_units`| `km` or `miles` | Specified the distance units. | `km` |
|
|
93
97
|
| `classes` | `Object` | Object to override default Tailwind CSS classes. | See [styling](https://places-autocomplete-demo.pages.dev/examples/styling) |
|
|
94
98
|
|
|
95
99
|
### Styling
|
|
@@ -126,7 +130,8 @@ const options = {
|
|
|
126
130
|
autofocus: false,
|
|
127
131
|
autocompete: 'off',
|
|
128
132
|
placeholder: 'Start typing your address',
|
|
129
|
-
|
|
133
|
+
distance: true,
|
|
134
|
+
distance_units: 'km' // or miles
|
|
130
135
|
};
|
|
131
136
|
|
|
132
137
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
3
|
import * as GMaps from '@googlemaps/js-api-loader';
|
|
4
|
-
import type {
|
|
5
|
-
import { validateOptions, validateRequestParams } from './helpers.js';
|
|
4
|
+
import type {Props } from './interfaces.js';
|
|
5
|
+
import { validateOptions, validateRequestParams, formatDistance } from './helpers.js';
|
|
6
6
|
const { Loader } = GMaps;
|
|
7
7
|
|
|
8
8
|
let {
|
|
@@ -20,24 +20,11 @@
|
|
|
20
20
|
|
|
21
21
|
// validate options
|
|
22
22
|
options = validateOptions(options);
|
|
23
|
+
//console.log(options);
|
|
23
24
|
|
|
24
25
|
// set classes as state
|
|
25
26
|
let cl = $state(options.classes);
|
|
26
27
|
|
|
27
|
-
// format meters to km and meters
|
|
28
|
-
const formatMeters = function (meters: number): string|null {
|
|
29
|
-
if(typeof meters !== 'number') {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
const km = Math.floor(meters / 1000);
|
|
33
|
-
const remainingMeters = meters % 1000;
|
|
34
|
-
let formattedString = '';
|
|
35
|
-
if (km > 0) {
|
|
36
|
-
formattedString += km + 'km ';
|
|
37
|
-
}
|
|
38
|
-
formattedString += remainingMeters + 'm';
|
|
39
|
-
return formattedString;
|
|
40
|
-
};
|
|
41
28
|
|
|
42
29
|
// reset keyboard classes
|
|
43
30
|
const resetKbdClasses = () => {
|
|
@@ -54,8 +41,7 @@
|
|
|
54
41
|
|
|
55
42
|
|
|
56
43
|
//https://developers.google.com/maps/documentation/javascript/reference/autocomplete-data
|
|
57
|
-
// validate
|
|
58
|
-
//let request = $state(validateRequestParams(Object.assign(requestParamsDefault, requestParams)));
|
|
44
|
+
// validate requestParams
|
|
59
45
|
requestParams = validateRequestParams(requestParams);
|
|
60
46
|
let request = $state(requestParams);
|
|
61
47
|
//$inspect(request);
|
|
@@ -114,7 +100,7 @@
|
|
|
114
100
|
results.push({
|
|
115
101
|
to_pace: suggestion.placePrediction.toPlace(),
|
|
116
102
|
text: suggestion.placePrediction.text.toString(),
|
|
117
|
-
distance:
|
|
103
|
+
distance: formatDistance(suggestion.placePrediction.distanceMeters, options.distance_units ?? 'km'),
|
|
118
104
|
});
|
|
119
105
|
}
|
|
120
106
|
} catch (e: any) {
|
|
@@ -274,7 +260,7 @@
|
|
|
274
260
|
<!-- <p class="mt-1 truncate text-xs/5 text-gray-500">leslie.alexander@example.com</p> -->
|
|
275
261
|
</div>
|
|
276
262
|
</div>
|
|
277
|
-
{#if options.
|
|
263
|
+
{#if options.distance && place.distance}
|
|
278
264
|
<div class="shrink-0 flex flex-col items-end min-w-16">
|
|
279
265
|
<p class={[i === currentSuggestion && options.classes.li_current,'mt-1 text-xs/5 text-gray-500']}>
|
|
280
266
|
{place.distance}
|
package/dist/helpers.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { RequestParams, ComponentOptions, ComponentClasses } from './interfaces.js';
|
|
1
|
+
import type { RequestParams, ComponentOptions, ComponentClasses, DistanceUnits } from './interfaces.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default request parameters
|
|
4
|
+
*/
|
|
2
5
|
export declare const requestParamsDefault: RequestParams;
|
|
3
6
|
/**
|
|
4
7
|
* Validate and cast request parameters
|
|
@@ -9,9 +12,13 @@ export declare const validateRequestParams: (requestParams: RequestParams | unde
|
|
|
9
12
|
* Default component classes
|
|
10
13
|
*/
|
|
11
14
|
export declare const componentClasses: ComponentClasses;
|
|
15
|
+
/**
|
|
16
|
+
* Default component options
|
|
17
|
+
*/
|
|
12
18
|
export declare const componentOptions: ComponentOptions;
|
|
13
19
|
/**
|
|
14
20
|
* Validate and cast component options
|
|
15
21
|
* @param options
|
|
16
22
|
*/
|
|
17
23
|
export declare const validateOptions: (options: ComponentOptions | undefined) => ComponentOptions;
|
|
24
|
+
export declare const formatDistance: (distance: number, units: DistanceUnits) => string | null;
|
package/dist/helpers.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default request parameters
|
|
3
|
+
*/
|
|
1
4
|
export const requestParamsDefault = {
|
|
2
5
|
/**
|
|
3
6
|
* @type string required
|
|
@@ -95,6 +98,23 @@ export const requestParamsDefault = {
|
|
|
95
98
|
*/
|
|
96
99
|
sessionToken: ''
|
|
97
100
|
};
|
|
101
|
+
/**
|
|
102
|
+
* Check if a variable is a valid LatLng object
|
|
103
|
+
* @param latLng
|
|
104
|
+
*/
|
|
105
|
+
function isValidLatLngLiteral(latLng) {
|
|
106
|
+
return latLng && typeof latLng === 'object' && 'lat' in latLng && 'lng' in latLng &&
|
|
107
|
+
typeof latLng.lat === 'number' && typeof latLng.lng === 'number';
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if a variable is a valid LatLngBounds object
|
|
111
|
+
* @param bounds
|
|
112
|
+
* @returns
|
|
113
|
+
*/
|
|
114
|
+
function isValidLatLngBoundsLiteral(bounds) {
|
|
115
|
+
return bounds && typeof bounds === 'object' && 'north' in bounds && 'south' in bounds && 'east' in bounds && 'west' in bounds &&
|
|
116
|
+
typeof bounds.north === 'number' && typeof bounds.south === 'number' && typeof bounds.east === 'number' && typeof bounds.west === 'number';
|
|
117
|
+
}
|
|
98
118
|
/**
|
|
99
119
|
* Validate and cast request parameters
|
|
100
120
|
* @param requestParams
|
|
@@ -102,103 +122,71 @@ export const requestParamsDefault = {
|
|
|
102
122
|
export const validateRequestParams = (requestParams) => {
|
|
103
123
|
// https://developers.google.com/maps/documentation/javascript/reference/autocomplete-data
|
|
104
124
|
/**
|
|
105
|
-
*
|
|
125
|
+
* create a new object to store validated parameters
|
|
106
126
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return requestParams;
|
|
116
|
-
}
|
|
117
|
-
// Remove any keys that are not in requestParamsDefault
|
|
127
|
+
const validatedParams = {
|
|
128
|
+
input: String(''),
|
|
129
|
+
sessionToken: String(''),
|
|
130
|
+
includedRegionCodes: ['GB'],
|
|
131
|
+
language: 'en-GB',
|
|
132
|
+
region: 'GB',
|
|
133
|
+
};
|
|
134
|
+
// iterate over requestParams
|
|
118
135
|
for (const key in requestParams) {
|
|
119
|
-
if
|
|
120
|
-
|
|
136
|
+
// Check if key is in requestParamsDefault
|
|
137
|
+
if (key in requestParamsDefault) {
|
|
138
|
+
// Validate and sanitize
|
|
139
|
+
switch (key) {
|
|
140
|
+
case 'input':
|
|
141
|
+
validatedParams.input = String(requestParams.input);
|
|
142
|
+
break;
|
|
143
|
+
case 'includedPrimaryTypes':
|
|
144
|
+
if (Array.isArray(requestParams.includedPrimaryTypes) && requestParams.includedPrimaryTypes.length > 0) {
|
|
145
|
+
validatedParams.includedPrimaryTypes = requestParams.includedPrimaryTypes.slice(0, 5).map(String);
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
case 'includedRegionCodes':
|
|
149
|
+
if (Array.isArray(requestParams.includedRegionCodes) && requestParams.includedRegionCodes.length > 0) {
|
|
150
|
+
validatedParams.includedRegionCodes = requestParams.includedRegionCodes.slice(0, 15).map(String);
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
case 'inputOffset':
|
|
154
|
+
{
|
|
155
|
+
const offset = Number(requestParams.inputOffset);
|
|
156
|
+
if (!isNaN(offset) && offset >= 0) { // Allow 0 for offset
|
|
157
|
+
validatedParams.inputOffset = offset;
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case 'language':
|
|
162
|
+
validatedParams.language = String(requestParams.language);
|
|
163
|
+
break;
|
|
164
|
+
case 'locationBias':
|
|
165
|
+
if (isValidLatLngLiteral(requestParams.locationBias)) {
|
|
166
|
+
validatedParams.locationBias = requestParams.locationBias;
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
case 'locationRestriction':
|
|
170
|
+
if (isValidLatLngBoundsLiteral(requestParams.locationRestriction)) {
|
|
171
|
+
validatedParams.locationRestriction = requestParams.locationRestriction;
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
case 'origin':
|
|
175
|
+
if (isValidLatLngLiteral(requestParams.origin)) {
|
|
176
|
+
validatedParams.origin = requestParams.origin;
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case 'region':
|
|
180
|
+
validatedParams.region = String(requestParams.region);
|
|
181
|
+
break;
|
|
182
|
+
case 'sessionToken': // Session token should be generated on the client-side
|
|
183
|
+
break; // Ignore any provided sessionToken
|
|
184
|
+
}
|
|
121
185
|
}
|
|
122
186
|
}
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
if (requestParams.sessionToken) {
|
|
127
|
-
requestParams.sessionToken = String('');
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* If requestParams.input is not a string, set it to an empty string
|
|
131
|
-
*/
|
|
132
|
-
if (typeof requestParams.input !== 'string') {
|
|
133
|
-
requestParams.input = String('');
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* If requestParams.includedPrimaryTypes is not an array or is an empty array, remove it
|
|
137
|
-
* If requestParams.includedPrimaryTypes is an array and has more than 5 items, slice it to 5
|
|
138
|
-
*/
|
|
139
|
-
if (!Array.isArray(requestParams.includedPrimaryTypes)
|
|
140
|
-
|| (Array.isArray(requestParams.includedPrimaryTypes) && requestParams.includedPrimaryTypes.length === 0)) {
|
|
141
|
-
delete requestParams.includedPrimaryTypes;
|
|
142
|
-
}
|
|
143
|
-
else if (Array.isArray(requestParams.includedPrimaryTypes) && requestParams.includedPrimaryTypes.length > 5) {
|
|
144
|
-
requestParams.includedPrimaryTypes = requestParams.includedPrimaryTypes.slice(0, 5);
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* If requestParams.includedRegionCodes is not an array or is an empty array, remove it
|
|
148
|
-
* If requestParams.includedRegionCodes is an array and has more than 15 items, slice it to 15
|
|
149
|
-
*/
|
|
150
|
-
if (!Array.isArray(requestParams.includedRegionCodes)
|
|
151
|
-
|| (Array.isArray(requestParams.includedRegionCodes) && requestParams.includedRegionCodes.length === 0)) {
|
|
152
|
-
delete requestParams.includedRegionCodes;
|
|
153
|
-
}
|
|
154
|
-
else if (Array.isArray(requestParams.includedRegionCodes) && requestParams.includedRegionCodes.length > 15) {
|
|
155
|
-
requestParams.includedRegionCodes = requestParams.includedRegionCodes.slice(0, 15);
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* If requestParams.inputOffset is not a number or is less than 1, remove it
|
|
159
|
-
*/
|
|
160
|
-
if (typeof requestParams.inputOffset !== 'number'
|
|
161
|
-
|| (typeof requestParams.inputOffset === 'number' && requestParams.inputOffset < 1)) {
|
|
162
|
-
delete requestParams.inputOffset;
|
|
163
|
-
}
|
|
164
|
-
// If language is not a string, remove it
|
|
165
|
-
if (typeof requestParams.language !== 'string') {
|
|
166
|
-
delete requestParams.language;
|
|
167
|
-
}
|
|
168
|
-
// If locationBias is not a string, remove it
|
|
169
|
-
if (typeof requestParams.locationBias !== 'undefined'
|
|
170
|
-
&& (!Object.keys(requestParams.locationBias).includes('lat') || !Object.keys(requestParams.locationBias).includes('lng'))
|
|
171
|
-
|| requestParams.locationBias?.lat === 0
|
|
172
|
-
|| requestParams.locationBias?.lng === 0) {
|
|
173
|
-
delete requestParams.locationBias;
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* If locationRestriction is not set, remove it
|
|
177
|
-
*/
|
|
178
|
-
if (typeof requestParams.locationRestriction !== 'undefined'
|
|
179
|
-
&& (!Object.keys(requestParams.locationRestriction).includes('east')
|
|
180
|
-
|| !Object.keys(requestParams.locationRestriction).includes('north')
|
|
181
|
-
|| !Object.keys(requestParams.locationRestriction).includes('south')
|
|
182
|
-
|| !Object.keys(requestParams.locationRestriction).includes('west'))
|
|
183
|
-
|| requestParams.locationRestriction?.east === 0
|
|
184
|
-
|| requestParams.locationRestriction?.north === 0
|
|
185
|
-
|| requestParams.locationRestriction?.south === 0
|
|
186
|
-
|| requestParams.locationRestriction?.west === 0) {
|
|
187
|
-
delete requestParams.locationRestriction;
|
|
188
|
-
}
|
|
189
|
-
// If origin is not set, remove it
|
|
190
|
-
if (typeof requestParams.origin !== 'undefined'
|
|
191
|
-
&& (!Object.keys(requestParams.origin).includes('lat') || !Object.keys(requestParams.origin).includes('lng'))
|
|
192
|
-
|| requestParams.origin?.lat === 0 || requestParams.origin?.lng === 0) {
|
|
193
|
-
delete requestParams.origin;
|
|
194
|
-
}
|
|
195
|
-
// If region is not a string, remove it
|
|
196
|
-
if (typeof requestParams.region !== 'string') {
|
|
197
|
-
delete requestParams.region;
|
|
198
|
-
}
|
|
199
|
-
// console.log('requestParams:', Object.keys(requestParams));
|
|
200
|
-
// console.log('requestParams:', requestParams);
|
|
201
|
-
return requestParams;
|
|
187
|
+
//console.log('validatedParams:', Object.keys(validatedParams));
|
|
188
|
+
//console.log('validatedParams:', validatedParams);
|
|
189
|
+
return validatedParams;
|
|
202
190
|
};
|
|
203
191
|
/**
|
|
204
192
|
* Default component classes
|
|
@@ -218,40 +206,61 @@ export const componentClasses = {
|
|
|
218
206
|
li_current: 'bg-indigo-500 text-white',
|
|
219
207
|
li_a: 'block w-full',
|
|
220
208
|
};
|
|
209
|
+
/**
|
|
210
|
+
* Default component options
|
|
211
|
+
*/
|
|
221
212
|
export const componentOptions = {
|
|
222
213
|
autofocus: false,
|
|
223
214
|
autocomplete: 'off',
|
|
215
|
+
placeholder: 'Start typing your address',
|
|
216
|
+
distance: true,
|
|
217
|
+
distance_units: 'km',
|
|
224
218
|
classes: componentClasses,
|
|
225
|
-
placeholder: '',
|
|
226
|
-
show_distance: false
|
|
227
219
|
};
|
|
228
220
|
/**
|
|
229
221
|
* Validate and cast component options
|
|
230
222
|
* @param options
|
|
231
223
|
*/
|
|
232
224
|
export const validateOptions = (options) => {
|
|
233
|
-
|
|
234
|
-
if (typeof options
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
225
|
+
const validatedOptions = { ...componentOptions };
|
|
226
|
+
if (options && typeof options === 'object') {
|
|
227
|
+
for (const key in validatedOptions) {
|
|
228
|
+
if (key in options) {
|
|
229
|
+
switch (key) {
|
|
230
|
+
case 'autofocus':
|
|
231
|
+
validatedOptions.autofocus = Boolean(options.autofocus);
|
|
232
|
+
break;
|
|
233
|
+
case 'autocomplete':
|
|
234
|
+
validatedOptions.autocomplete = String(options.autocomplete);
|
|
235
|
+
break;
|
|
236
|
+
case 'placeholder':
|
|
237
|
+
validatedOptions.placeholder = String(options.placeholder);
|
|
238
|
+
break;
|
|
239
|
+
case 'distance':
|
|
240
|
+
validatedOptions.distance = Boolean(options.distance);
|
|
241
|
+
break;
|
|
242
|
+
case 'distance_units':
|
|
243
|
+
validatedOptions.distance_units = String(options.distance_units);
|
|
244
|
+
break;
|
|
245
|
+
case 'classes':
|
|
246
|
+
if (options.classes && typeof options.classes === 'object') {
|
|
247
|
+
validatedOptions.classes = { ...componentOptions.classes, ...options.classes };
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
248
252
|
}
|
|
249
253
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
254
|
+
return validatedOptions;
|
|
255
|
+
};
|
|
256
|
+
export const formatDistance = function (distance, units) {
|
|
257
|
+
if (typeof distance !== 'number') {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
if (units === 'km') {
|
|
261
|
+
return `${(distance / 1000).toFixed(2)} km`;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
return `${(distance / 1609.34).toFixed(2)} miles`;
|
|
255
265
|
}
|
|
256
|
-
return options;
|
|
257
266
|
};
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -28,21 +28,20 @@ export interface RequestParams {
|
|
|
28
28
|
export interface ComponentClasses {
|
|
29
29
|
[key: string]: string;
|
|
30
30
|
}
|
|
31
|
+
export type AutoFill = "on" | "off";
|
|
32
|
+
export type DistanceUnits = "km" | "miles";
|
|
31
33
|
export interface ComponentOptions {
|
|
32
|
-
autofocus
|
|
33
|
-
autocomplete
|
|
34
|
+
autofocus?: boolean;
|
|
35
|
+
autocomplete?: AutoFill;
|
|
34
36
|
classes: ComponentClasses;
|
|
35
|
-
placeholder
|
|
36
|
-
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
distance?: boolean;
|
|
39
|
+
distance_units?: DistanceUnits;
|
|
37
40
|
}
|
|
38
41
|
export interface Props {
|
|
39
42
|
PUBLIC_GOOGLE_MAPS_API_KEY: string;
|
|
40
43
|
options?: ComponentOptions;
|
|
41
44
|
fetchFields?: string[];
|
|
42
|
-
placeholder?: string;
|
|
43
|
-
autofocus?: boolean;
|
|
44
|
-
autocompete?: AutoFill;
|
|
45
|
-
classes?: ComponentClasses;
|
|
46
45
|
requestParams?: RequestParams;
|
|
47
46
|
onResponse: (e: Event) => void;
|
|
48
47
|
onError: (error: string) => void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "places-autocomplete-svelte",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "2.1
|
|
4
|
+
"version": "2.2.1",
|
|
5
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.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"svelte",
|