places-autocomplete-svelte 2.1.8 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Places (New) Autocomplete Svelte
2
2
 
3
- This Svelte component provides a user-friendly way to search for and retrieve detailed address information within your [SvelteKit](https://kit.svelte.dev) applications, leveraging the power of the [Google Maps Places (New) Autocomplete API](https://developers.google.com/maps/documentation/javascript/place-autocomplete-overview). It comes with default styling using [Tailwind CSS](https://tailwindcss.com/), which you can fully customize.
3
+ This Svelte component provides a user-friendly way to search for and retrieve detailed address information within your [SvelteKit](https://kit.svelte.dev) applications, leveraging the power of the [Google Maps Places (New) Autocomplete API](https://developers.google.com/maps/documentation/javascript/place-autocomplete-overview). It comes with default styling using [Tailwind CSS](https://tailwindcss.com/), which you can fully customise.
4
4
 
5
5
 
6
6
 
@@ -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,11 +92,12 @@ 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` |
93
- | `classes` | `Object` | Object to override default Tailwind CSS classes.structure. | See [styling](https://places-autocomplete-demo.pages.dev/examples/styling) |
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` |
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
96
- Customize the component's appearance by providing an object to the classes property. This object should contain key-value pairs, where the keys correspond to the component's elements and the values are your custom CSS class names. See [styling](https://places-autocomplete-demo.pages.dev/examples/styling) for details.
100
+ Customise the component's appearance by providing an object to the classes property. This object should contain key-value pairs, where the keys correspond to the component's elements and the values are your custom CSS class names. See [styling](https://places-autocomplete-demo.pages.dev/examples/styling) for details.
97
101
 
98
102
 
99
103
  ### Request Parameters (requestParams)
@@ -103,12 +107,6 @@ Fine-tune the autocomplete search with the requestParams property. This property
103
107
  <script>
104
108
  // ... other imports
105
109
 
106
- /**
107
- * @type boolean optional
108
- * Boolean attribute indicating that an element should be focused on page load.
109
- * default: false
110
- * */
111
- const autofocus = false;
112
110
  /**
113
111
  * @type object optional
114
112
  * AutocompleteRequest properties
@@ -128,12 +126,13 @@ const requestParams = {
128
126
  * @type object optional
129
127
  * Options
130
128
  */
131
- const options = {
132
- autofocus: false,
133
- autocompete: 'off',
134
- placeholder: 'Start typing your address',
135
- show_distance: true,
136
- };
129
+ const options = {
130
+ autofocus: false,
131
+ autocompete: 'off',
132
+ placeholder: 'Start typing your address',
133
+ distance: true,
134
+ distance_units: 'km' // or miles
135
+ };
137
136
 
138
137
  /**
139
138
  * @type array optional
@@ -189,4 +188,7 @@ Contributions are welcome! Please open an issue or submit a pull request on the
189
188
 
190
189
  ## License
191
190
 
192
- [MIT](LICENSE)
191
+ [MIT](LICENSE)
192
+
193
+ etihad
194
+ 221b
@@ -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
@@ -43,7 +46,10 @@ export const requestParamsDefault = {
43
46
  * If neither are set, the results will be biased by IP address, meaning the IP address
44
47
  * will be mapped to an imprecise location and used as a biasing signal.
45
48
  */
46
- locationBias: null,
49
+ locationBias: {
50
+ lat: 0,
51
+ lng: 0
52
+ },
47
53
  /**
48
54
  * @param LocationRestriction optional
49
55
  * Restrict results to a specified location.
@@ -92,6 +98,23 @@ export const requestParamsDefault = {
92
98
  */
93
99
  sessionToken: ''
94
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
+ }
95
118
  /**
96
119
  * Validate and cast request parameters
97
120
  * @param requestParams
@@ -99,92 +122,71 @@ export const requestParamsDefault = {
99
122
  export const validateRequestParams = (requestParams) => {
100
123
  // https://developers.google.com/maps/documentation/javascript/reference/autocomplete-data
101
124
  /**
102
- * If requestParams is not an object, set it to an empty object
125
+ * create a new object to store validated parameters
103
126
  */
104
- if (typeof requestParams !== 'object' || Object.keys(requestParams).length === 0) {
105
- requestParams = {
106
- input: String(''),
107
- sessionToken: String(''),
108
- includedRegionCodes: ['GB'],
109
- language: 'en-GB',
110
- region: 'GB',
111
- };
112
- return requestParams;
113
- }
114
- // 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
115
135
  for (const key in requestParams) {
116
- if (!(key in requestParamsDefault)) {
117
- 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
+ }
118
185
  }
119
186
  }
120
- // merge requestParams with requestParamsDefault
121
- requestParams = Object.assign(requestParamsDefault, requestParams);
122
- // Reset sessionToken to empty string if passed to the component
123
- if (requestParams.sessionToken) {
124
- requestParams.sessionToken = String('');
125
- }
126
- /**
127
- * If requestParams.input is not a string, set it to an empty string
128
- */
129
- if (typeof requestParams.input !== 'string') {
130
- requestParams.input = String('');
131
- }
132
- /**
133
- * If requestParams.includedPrimaryTypes is not an array or is an empty array, remove it
134
- * If requestParams.includedPrimaryTypes is an array and has more than 5 items, slice it to 5
135
- */
136
- if (!Array.isArray(requestParams.includedPrimaryTypes)
137
- || (Array.isArray(requestParams.includedPrimaryTypes) && requestParams.includedPrimaryTypes.length === 0)) {
138
- delete requestParams.includedPrimaryTypes;
139
- }
140
- else if (Array.isArray(requestParams.includedPrimaryTypes) && requestParams.includedPrimaryTypes.length > 5) {
141
- requestParams.includedPrimaryTypes = requestParams.includedPrimaryTypes.slice(0, 5);
142
- }
143
- /**
144
- * If requestParams.includedRegionCodes is not an array or is an empty array, remove it
145
- * If requestParams.includedRegionCodes is an array and has more than 15 items, slice it to 15
146
- */
147
- if (!Array.isArray(requestParams.includedRegionCodes)
148
- || (Array.isArray(requestParams.includedPrimaryTypes) && requestParams.includedPrimaryTypes.length === 0)) {
149
- delete requestParams.includedRegionCodes;
150
- }
151
- else if (Array.isArray(requestParams.includedRegionCodes) && requestParams.includedRegionCodes.length > 15) {
152
- requestParams.includedRegionCodes = requestParams.includedRegionCodes.slice(0, 15);
153
- }
154
- /**
155
- * If requestParams.inputOffset is not a number or is less than 1, remove it
156
- */
157
- if (typeof requestParams.inputOffset !== 'number'
158
- || (typeof requestParams.inputOffset === 'number' && requestParams.inputOffset < 1)) {
159
- delete requestParams.inputOffset;
160
- }
161
- // If language is not a string, remove it
162
- if (typeof requestParams.language !== 'string') {
163
- delete requestParams.language;
164
- }
165
- // If locationBias is not a string, remove it
166
- if (typeof requestParams.locationBias !== 'string') {
167
- delete requestParams.locationBias;
168
- }
169
- /**
170
- * If locationRestriction is not set, remove it
171
- */
172
- if (requestParams.locationRestriction?.east === 0
173
- && requestParams.locationRestriction?.north === 0
174
- && requestParams.locationRestriction?.south === 0
175
- && requestParams.locationRestriction?.west === 0) {
176
- delete requestParams.locationRestriction;
177
- }
178
- // If origin is not set, remove it
179
- if (requestParams.origin?.lat === 0 && requestParams.origin?.lng === 0) {
180
- delete requestParams.origin;
181
- }
182
- // If region is not a string, remove it
183
- if (typeof requestParams.region !== 'string') {
184
- delete requestParams.region;
185
- }
186
- //console.log('requestParams:', Object.keys(requestParams));
187
- return requestParams;
187
+ //console.log('validatedParams:', Object.keys(validatedParams));
188
+ //console.log('validatedParams:', validatedParams);
189
+ return validatedParams;
188
190
  };
189
191
  /**
190
192
  * Default component classes
@@ -204,40 +206,61 @@ export const componentClasses = {
204
206
  li_current: 'bg-indigo-500 text-white',
205
207
  li_a: 'block w-full',
206
208
  };
209
+ /**
210
+ * Default component options
211
+ */
207
212
  export const componentOptions = {
208
213
  autofocus: false,
209
214
  autocomplete: 'off',
215
+ placeholder: 'Start typing your address',
216
+ distance: true,
217
+ distance_units: 'km',
210
218
  classes: componentClasses,
211
- placeholder: '',
212
- show_distance: false
213
219
  };
214
220
  /**
215
221
  * Validate and cast component options
216
222
  * @param options
217
223
  */
218
224
  export const validateOptions = (options) => {
219
- // If options is not an object, set it to an empty object
220
- if (typeof options !== 'object' || Object.keys(options).length === 0) {
221
- options = {
222
- autofocus: false,
223
- autocomplete: 'off',
224
- classes: componentClasses,
225
- placeholder: 'Start typing...',
226
- show_distance: false
227
- };
228
- return options;
229
- }
230
- // Find the missing options properties
231
- for (const key in componentOptions) {
232
- if (!(key in options)) {
233
- 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
+ }
234
252
  }
235
253
  }
236
- // Find the missing classes properties
237
- for (const key in componentClasses) {
238
- if (!(key in options.classes)) {
239
- options.classes[key] = componentClasses[key];
240
- }
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`;
241
265
  }
242
- return options;
243
266
  };
@@ -8,7 +8,10 @@ export interface RequestParams {
8
8
  includedRegionCodes?: string[];
9
9
  inputOffset?: number;
10
10
  language?: string;
11
- locationBias?: unknown | string;
11
+ locationBias?: {
12
+ lat: number;
13
+ lng: number;
14
+ };
12
15
  locationRestriction?: {
13
16
  west: number;
14
17
  south: number;
@@ -25,21 +28,20 @@ export interface RequestParams {
25
28
  export interface ComponentClasses {
26
29
  [key: string]: string;
27
30
  }
31
+ export type AutoFill = "on" | "off";
32
+ export type DistanceUnits = "km" | "miles";
28
33
  export interface ComponentOptions {
29
- autofocus: boolean;
30
- autocomplete: AutoFill;
34
+ autofocus?: boolean;
35
+ autocomplete?: AutoFill;
31
36
  classes: ComponentClasses;
32
- placeholder: string;
33
- show_distance: boolean;
37
+ placeholder?: string;
38
+ distance?: boolean;
39
+ distance_units?: DistanceUnits;
34
40
  }
35
41
  export interface Props {
36
42
  PUBLIC_GOOGLE_MAPS_API_KEY: string;
37
43
  options?: ComponentOptions;
38
44
  fetchFields?: string[];
39
- placeholder?: string;
40
- autofocus?: boolean;
41
- autocompete?: AutoFill;
42
- classes?: ComponentClasses;
43
45
  requestParams?: RequestParams;
44
46
  onResponse: (e: Event) => void;
45
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.8",
4
+ "version": "2.2.0",
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",
@@ -68,30 +68,30 @@
68
68
  },
69
69
  "devDependencies": {
70
70
  "@sveltejs/adapter-auto": "^4.0.0",
71
- "@sveltejs/adapter-cloudflare": "^5.0.1",
72
- "@sveltejs/kit": "^2.16.1",
73
- "@sveltejs/package": "^2.3.9",
71
+ "@sveltejs/adapter-cloudflare": "^5.0.2",
72
+ "@sveltejs/kit": "^2.17.1",
73
+ "@sveltejs/package": "^2.3.10",
74
74
  "@sveltejs/vite-plugin-svelte": "^5.0.3",
75
- "@tailwindcss/postcss": "^4.0.3",
75
+ "@tailwindcss/postcss": "^4.0.5",
76
76
  "@tailwindcss/typography": "^0.5.16",
77
- "@tailwindcss/vite": "^4.0.3",
77
+ "@tailwindcss/vite": "^4.0.5",
78
78
  "@types/eslint": "^9.6.1",
79
79
  "autoprefixer": "^10.4.20",
80
- "eslint": "^9.19.0",
80
+ "eslint": "^9.20.0",
81
81
  "eslint-config-prettier": "^10.0.1",
82
82
  "eslint-plugin-svelte": "^2.46.1",
83
83
  "globals": "^15.14.0",
84
84
  "postcss": "^8.5.1",
85
85
  "prettier": "^3.4.2",
86
86
  "prettier-plugin-svelte": "^3.3.3",
87
- "publint": "^0.3.2",
88
- "svelte": "^5.19.6",
87
+ "publint": "^0.3.4",
88
+ "svelte": "^5.19.9",
89
89
  "svelte-check": "^4.1.4",
90
- "tailwindcss": "^4.0.3",
90
+ "tailwindcss": "^4.0.5",
91
91
  "tslib": "^2.8.1",
92
92
  "typescript": "^5.7.3",
93
- "typescript-eslint": "^8.22.0",
94
- "vite": "^6.0.11"
93
+ "typescript-eslint": "^8.23.0",
94
+ "vite": "^6.1.0"
95
95
  },
96
96
  "svelte": "./dist/index.js",
97
97
  "types": "./dist/PlaceAutocomplete.svelte.d.ts",