places-autocomplete-svelte
Version:
A flexible and customisable Svelte component leveraging the Google Maps Places (New) Autocomplete API to provide a user-friendly way to search for and retrieve detailed address information within your SvelteKit applications.
366 lines (365 loc) • 13.7 kB
JavaScript
/**
* Default request parameters
*/
export const requestParamsDefault = {
/**
* @type string required
* The text string on which to search.
*/
input: '',
/**
* @type Array<string>
* A Place is only returned if its primary type is included in this list.
* Up to 5 values can be specified.
* If no types are specified, all Place types are returned.
* https://developers.google.com/maps/documentation/javascript/place-types
*
* ['postal_code','premise','street_address','route']
* FY83DD 72
*/
//includedPrimaryTypes: ['postal_code','premise','street_address','route'],
includedPrimaryTypes: [],
/**
* @type Array<string> optional
* Only include results in the specified regions, specified as up to 15 CLDR two-character region codes.
* An empty set will not restrict the results.
* If both locationRestriction and includedRegionCodes are set, the results will be located in the area of intersection.
*/
includedRegionCodes: ['GB'],
/**
* @type number optional
* A zero-based Unicode character offset of input indicating the cursor position in input.
* The cursor position may influence what predictions are returned. If not specified, defaults to the length of input.
*/
inputOffset: 0,
/**
* @type string optional
* The language in which to return results. Will default to the browser's language preference.
*
*/
language: 'en-GB',
/**
* @type LocationBias optional
* Bias results to a specified location.
*
* At most one of locationBias or locationRestriction should be set.
* If neither are set, the results will be biased by IP address, meaning the IP address
* will be mapped to an imprecise location and used as a biasing signal.
*/
locationBias: {
lat: 0,
lng: 0
},
/**
* @param LocationRestriction optional
* Restrict results to a specified location.
*
* At most one of locationBias or locationRestriction should be set.
* If neither are set, the results will be biased by IP address, meaning the IP address will
* be mapped to an imprecise location and used as a biasing signal.
*/
locationRestriction: {
west: 0,
south: 0,
east: 0,
north: 0
},
/**
* @type LatLng|LatLngLiteral optional
* The point around which you wish to retrieve place information.
*/
origin: {
lat: 0,
lng: 0
},
/**
* @type string optional
* The region code, specified as a CLDR two-character region code. T
* his affects address formatting, result ranking, and may influence what results are returned.
* This does not restrict results to the specified region.
*/
region: 'GB',
/**
* @type AutocompleteSessionToken optional
*
* A token which identifies an Autocomplete session for billing purposes.
* Generate a new session token via AutocompleteSessionToken The session begins when the user starts typing a query, and concludes
* when they select a place and call Place.fetchFields. Each session can have multiple queries, followed by one fetchFields call.
* The credentials used for each request within a session must belong to the same Google Cloud Console project. Once a session has
* concluded, the token is no longer valid; your app must generate a fresh token for each session. If the sessionToken parameter is
* omitted, or if you reuse a session token, the session is charged as if no session token was provided (each request is billed
* separately).
*
* We recommend the following guidelines:
* Use session tokens for all Place Autocomplete calls.
* Generate a fresh token for each session.
* Be sure to pass a unique session token for each new session. Using the same token for more than one session will result in each
* request being billed individually.
*/
sessionToken: ''
};
/**
* Default fetch fields values
* https://developers.google.com/maps/documentation/javascript/place-class-data-fields
*
* unsupported field values
* geometry, icon, name, permanentlyClosed, photo, placeId, url, utcOffset, vicinity, openingHours, icon, name
*/
export const defaultFetchFields = [
'formattedAddress',
'addressComponents',
'accessibilityOptions',
'allowsDogs',
'businessStatus',
'hasCurbsidePickup',
'hasDelivery',
'hasDineIn',
'displayName',
'displayNameLanguageCode',
'editorialSummary',
'evChargeOptions',
'adrFormatAddress',
'fuelOptions',
'isGoodForChildren',
'isGoodForGroups',
'isGoodForWatchingSports',
'svgIconMaskURI',
'iconBackgroundColor',
'internationalPhoneNumber',
'hasLiveMusic',
'location',
'hasMenuForChildren',
'regularOpeningHours',
'hasOutdoorSeating',
'parkingOptions',
'paymentOptions',
'photos',
'nationalPhoneNumber',
'id',
'plusCode',
'priceLevel',
'primaryType',
'primaryTypeDisplayName',
'primaryTypeDisplayNameLanguageCode',
'rating',
'userRatingCount',
'isReservable',
'hasRestroom',
'reviews',
'servesBeer',
'servesBreakfast',
'servesBrunch',
'servesCocktails',
'servesCoffee',
'servesDessert',
'servesDinner',
'servesLunch',
'servesVegetarianFood',
'servesWine',
'hasTakeout',
'types',
'websiteURI',
'utcOffsetMinutes',
'viewport',
'websiteURI'
];
/**
* Check if a variable is a valid LatLng object
* @param latLng
*/
function isValidLatLngLiteral(latLng) {
return latLng && typeof latLng === 'object' && 'lat' in latLng && 'lng' in latLng &&
typeof latLng.lat === 'number' && typeof latLng.lng === 'number';
}
/**
* Check if a variable is a valid LatLngBounds object
* @param bounds
* @returns
*/
function isValidLatLngBoundsLiteral(bounds) {
return bounds && typeof bounds === 'object' && 'north' in bounds && 'south' in bounds && 'east' in bounds && 'west' in bounds &&
typeof bounds.north === 'number' && typeof bounds.south === 'number' && typeof bounds.east === 'number' && typeof bounds.west === 'number';
}
/**
* Validate and cast request parameters
* @param requestParams
*/
export const validateRequestParams = (requestParams) => {
// https://developers.google.com/maps/documentation/javascript/reference/autocomplete-data
/**
* create a new object to store validated parameters
*/
const validatedParams = {
input: String(''),
sessionToken: String(''),
includedRegionCodes: ['GB'],
language: 'en-GB',
region: 'GB',
};
// iterate over requestParams
for (const key in requestParams) {
// Check if key is in requestParamsDefault
if (key in requestParamsDefault) {
// Validate and sanitize
switch (key) {
case 'input':
validatedParams.input = String(requestParams.input);
break;
case 'includedPrimaryTypes':
if (Array.isArray(requestParams.includedPrimaryTypes) && requestParams.includedPrimaryTypes.length > 0) {
validatedParams.includedPrimaryTypes = requestParams.includedPrimaryTypes.slice(0, 5).map(String);
}
break;
case 'includedRegionCodes':
if (Array.isArray(requestParams.includedRegionCodes) && requestParams.includedRegionCodes.length > 0) {
validatedParams.includedRegionCodes = requestParams.includedRegionCodes.slice(0, 15).map(String);
}
break;
case 'inputOffset':
{
const offset = Number(requestParams.inputOffset);
if (!isNaN(offset) && offset >= 0) { // Allow 0 for offset
validatedParams.inputOffset = offset;
}
break;
}
case 'language':
validatedParams.language = String(requestParams.language);
break;
case 'locationBias':
if (isValidLatLngLiteral(requestParams.locationBias)) {
validatedParams.locationBias = requestParams.locationBias;
}
break;
case 'locationRestriction':
if (isValidLatLngBoundsLiteral(requestParams.locationRestriction)) {
validatedParams.locationRestriction = requestParams.locationRestriction;
}
break;
case 'origin':
if (isValidLatLngLiteral(requestParams.origin)) {
validatedParams.origin = requestParams.origin;
}
break;
case 'region':
validatedParams.region = String(requestParams.region);
break;
case 'sessionToken': // Session token should be generated on the client-side
break; // Ignore any provided sessionToken
}
}
}
//console.log('validatedParams:', Object.keys(validatedParams));
//console.log('validatedParams:', validatedParams);
return validatedParams;
};
/**
* Validate fetchFields array parameters
* @param fetchFields
*/
export const validateFetchFields = (fetchFields) => {
//https://developers.google.com/maps/documentation/javascript/place-class-data-fields
/**
* create a new object to store validated parameters
*/
const validatedFetchFields = [];
if (typeof fetchFields === 'undefined' || fetchFields.length === 0) {
return [
'formattedAddress',
'addressComponents'
];
}
// iterate over requestParams
for (const key of fetchFields) {
// Check if key is in requestParamsDefault
if (defaultFetchFields.includes(key)) {
validatedFetchFields.push(key);
}
}
if (validateFetchFields.length === 0) {
return [
'formattedAddress',
'addressComponents'
];
}
//console.log('validatedParams:', Object.keys(validatedParams));
//console.log('validatedParams:', validatedParams);
return validatedFetchFields;
};
/**
* Default component classes
*/
export const componentClasses = {
section: '',
container: 'relative z-10 transform rounded-xl mt-4',
icon_container: 'pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3',
icon: '<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>',
input: 'border-1 w-full rounded-md border-0 shadow-sm bg-gray-100 px-4 py-2.5 pl-10 pr-20 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 sm:text-sm',
kbd_container: 'absolute inset-y-0 right-0 flex py-1.5 pr-1.5',
kbd_escape: 'inline-flex items-center rounded border border-gray-300 px-1 font-sans text-xs text-gray-500 w-8 mr-1',
kbd_up: 'inline-flex items-center justify-center rounded border border-gray-300 px-1 font-sans text-xs text-gray-500 w-6',
kbd_down: 'inline-flex items-center rounded border border-gray-400 px-1 font-sans text-xs text-gray-500 justify-center w-6',
kbd_active: 'bg-indigo-500 text-white',
ul: 'absolute z-50 -mb-2 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm divide-y divide-gray-100',
li: 'z-50 cursor-default select-none py-2 px-2 lg:px-4 text-gray-900 hover:bg-indigo-500 hover:text-white',
li_current: 'bg-indigo-500',
li_a: 'block w-full flex justify-between',
li_a_current: 'text-white',
li_div_container: 'flex min-w-0 gap-x-4',
li_div_one: 'min-w-0 flex-auto',
li_div_one_p: 'text-sm/6 ',
li_div_two: 'shrink-0 flex flex-col items-end min-w-16',
li_div_two_p: 'mt-1 text-xs/5',
highlight: 'font-bold',
};
/**
* Default component options
*/
export const componentOptions = {
autofocus: false,
autocomplete: 'off',
placeholder: 'Start typing your address',
distance: true,
distance_units: 'km',
classes: componentClasses,
label: '',
debounce: 100,
clear_input: true
};
/**
* Validate and cast component options
* @param options
*/
export const validateOptions = (options) => {
if (!options) {
return componentOptions;
}
// Perform a deep merge for the 'classes' object, inspired by the JS library
const mergedClasses = {
...componentOptions.classes,
...(options.classes ?? {})
};
const validated = {
...componentOptions, // Start with all defaults
...options, // Override with user-provided options
classes: mergedClasses // Apply the specifically merged classes
};
return validated;
};
/**
* Display distance in km or miles
* @param distance
* @param units
* @returns
*/
export const formatDistance = function (distance, units) {
if (typeof distance !== 'number') {
return null;
}
if (units === 'km') {
return `${(distance / 1000).toFixed(2)} km`;
}
else {
return `${(distance / 1609.34).toFixed(2)} miles`;
}
};