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 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
- ![Places Autocomplete Svelte](places-autocomplete-svelte.gif)
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
- | `show_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` |
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
- show_distance: true,
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 { ComponentOptions, Props } from './interfaces.js';
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 and merge requestParams with requestParamsDefault
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: formatMeters(suggestion.placePrediction.distanceMeters)
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.show_distance && place.distance}
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
- * If requestParams is not an object, set it to an empty object
125
+ * create a new object to store validated parameters
106
126
  */
107
- if (typeof requestParams !== 'object' || Object.keys(requestParams).length === 0) {
108
- requestParams = {
109
- input: String(''),
110
- sessionToken: String(''),
111
- includedRegionCodes: ['GB'],
112
- language: 'en-GB',
113
- region: 'GB',
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 (!(key in requestParamsDefault)) {
120
- delete requestParams[key];
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
- // merge requestParams with requestParamsDefault
124
- //requestParams = Object.assign(requestParamsDefault, requestParams);
125
- // Reset sessionToken to empty string if passed to the component
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
- // If options is not an object, set it to an empty object
234
- if (typeof options !== 'object' || Object.keys(options).length === 0) {
235
- options = {
236
- autofocus: false,
237
- autocomplete: 'off',
238
- classes: componentClasses,
239
- placeholder: 'Start typing...',
240
- show_distance: false
241
- };
242
- return options;
243
- }
244
- // Find the missing options properties
245
- for (const key in componentOptions) {
246
- if (!(key in options)) {
247
- options[key] = componentOptions[key];
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
- // Find the missing classes properties
251
- for (const key in componentClasses) {
252
- if (!(key in options.classes)) {
253
- options.classes[key] = componentClasses[key];
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
  };
@@ -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: boolean;
33
- autocomplete: AutoFill;
34
+ autofocus?: boolean;
35
+ autocomplete?: AutoFill;
34
36
  classes: ComponentClasses;
35
- placeholder: string;
36
- show_distance: boolean;
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.9",
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",