google-places-core
Version:
A lightweight Google Places API logic library.
237 lines (229 loc) • 8.96 kB
JavaScript
;
const debounce = (func, delay) => {
let timeoutId;
const debouncedFunction = (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
debouncedFunction.cancel = () => {
clearTimeout(timeoutId);
};
return debouncedFunction;
};
class GooglePlacesManager {
constructor(service, debounceTime = 300) {
this.state = {
predictions: [],
isLoading: false,
error: null,
};
this.subscribers = new Set();
this.fetchPredictionsInternal = async (input) => {
if (!input) {
this.setState({ predictions: [], isLoading: false, error: null });
return;
}
this.setState({ isLoading: true, error: null });
try {
const predictions = await this.placesService.fetchPredictions(input);
this.setState({ predictions, isLoading: false });
}
catch (error) {
console.error('Error fetching predictions:', error);
this.setState({ predictions: [], isLoading: false, error });
}
};
this.placesService = service;
this.debouncedGetPredictions = debounce(this.fetchPredictionsInternal, debounceTime);
}
setState(newState) {
this.state = Object.assign(Object.assign({}, this.state), newState);
this.subscribers.forEach(callback => callback(this.state));
}
updateSearchInput(input) {
this.debouncedGetPredictions(input);
}
async getPlaceDetails(placeId) {
try {
return await this.placesService.fetchPlaceDetails(placeId);
}
catch (error) {
console.error('Error fetching place details:', error);
this.setState({ error });
throw error;
}
}
subscribe(callback) {
this.subscribers.add(callback);
callback(this.state);
return () => {
this.subscribers.delete(callback);
};
}
getState() {
return Object.assign({}, this.state);
}
destroy() {
this.debouncedGetPredictions.cancel();
this.subscribers.clear();
}
}
class Helpers {
constructor(apiKey) {
this.apiKey = apiKey;
}
checkBrowserEnvironment() {
return typeof window !== 'undefined' && typeof document !== 'undefined';
}
isLoaded() {
var _a, _b;
return !!((_b = (_a = window.google) === null || _a === void 0 ? void 0 : _a.maps) === null || _b === void 0 ? void 0 : _b.places);
}
isScriptInjected() {
return Array.from(document.scripts).some((s) => s.src.includes('maps.googleapis.com/maps/api/js'));
}
injectScript() {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${this.apiKey}&libraries=places`;
script.async = true;
script.defer = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load Google Maps API'));
document.head.appendChild(script);
});
}
waitForLoad() {
return new Promise((resolve) => {
const check = () => this.isLoaded() ? resolve() : setTimeout(check, 100);
setTimeout(() => resolve(), 10000); // Timeout after 10s
check();
});
}
}
class GooglePlacesWebService extends Helpers {
constructor(apiKey) {
if (!apiKey)
throw new Error('GoogleMapService: API Key must be provided.');
super(apiKey);
this.loadPromise = null;
this.isBrowser = this.checkBrowserEnvironment();
}
// Public API Methods
async fetchPredictions(input) {
await this.ensureReady();
return this.getPredictions(input);
}
async fetchPlaceDetails(placeId) {
await this.ensureReady();
return this.getPlaceDetails(placeId);
}
// Core Implementation
async ensureReady() {
if (!this.isBrowser)
throw new Error('Browser environment required');
if (this.isLoaded())
return;
if (!this.loadPromise)
this.loadPromise = this.loadGoogleMaps();
await this.loadPromise;
}
async loadGoogleMaps() {
if (!this.isScriptInjected())
await this.injectScript();
await this.waitForLoad();
this.initServices();
}
getPredictions(input) {
return new Promise((resolve, reject) => {
this.autocompleteService.getPlacePredictions({ input }, (predictions, status) => {
status === window.google.maps.places.PlacesServiceStatus.OK
? resolve(this.transformPredictions(predictions || []))
: reject(new Error(`Places API error: ${status}`));
});
});
}
getPlaceDetails(placeId) {
return new Promise((resolve, reject) => {
this.placesService.getDetails({ placeId, fields: ['geometry', 'formatted_address'] }, (place, status) => {
status === window.google.maps.places.PlacesServiceStatus.OK
? resolve(this.transformPlaceDetails(place))
: reject(new Error(`Places details error: ${status}`));
});
});
}
initServices() {
this.autocompleteService =
new window.google.maps.places.AutocompleteService();
this.placesService = new window.google.maps.places.PlacesService(document.createElement('div'));
}
transformPredictions(predictions) {
return predictions.map(p => ({
description: p.description,
place_id: p.place_id,
structured_formatting: {
main_text: p.structured_formatting.main_text,
secondary_text: p.structured_formatting.secondary_text,
},
types: p.types || [],
}));
}
transformPlaceDetails(place) {
return {
formatted_address: place.formatted_address,
geometry: {
location: {
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
},
viewport: place.geometry.viewport ? {
south: place.geometry.viewport.getSouthWest().lat(),
west: place.geometry.viewport.getSouthWest().lng(),
north: place.geometry.viewport.getNorthEast().lat(),
east: place.geometry.viewport.getNorthEast().lng(),
} : undefined,
},
name: place.name,
place_id: place.place_id,
};
}
}
// Here, we can use the HTTP API for the native implementation
class GooglePlacesNativeService {
constructor(apiKey) {
this.fetchPredictions = async (input) => {
const response = await fetch(`https://maps.googleapis.com/maps/api/place/autocomplete/json?input=${encodeURIComponent(input)}&language=en&key=${this.API_KEY}`);
if (!response.ok) {
const errorData = await response.json();
console.error('Google Places Autocomplete Error:', errorData);
throw new Error(`Failed to fetch predictions: ${errorData.error_message || response.statusText}`);
}
const data = await response.json();
return data.predictions || [];
};
this.fetchPlaceDetails = async (placeId) => {
const response = await fetch(`https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&fields=geometry,formatted_address&key=${this.API_KEY}`);
if (!response.ok) {
const errorData = await response.json();
console.error('Google Places Details Error:', errorData);
throw new Error(`Failed to fetch place details: ${errorData.error_message || response.statusText}`);
}
const data = await response.json();
return data.result;
};
if (!apiKey)
throw new Error('GoogleMapService: API Key must be provided.');
this.API_KEY = apiKey;
}
}
function createGooglePlacesManager(apiKey, platform = 'web', options) {
const service = platform === 'web'
? new GooglePlacesWebService(apiKey)
: new GooglePlacesNativeService(apiKey);
return new GooglePlacesManager(service, options === null || options === void 0 ? void 0 : options.debounceTime);
}
exports.GooglePlacesManager = GooglePlacesManager;
exports.GooglePlacesNativeService = GooglePlacesNativeService;
exports.GooglePlacesWebService = GooglePlacesWebService;
exports.createGooglePlacesManager = createGooglePlacesManager;
//# sourceMappingURL=index.js.map