@vis.gl/react-google-maps
Advanced tools
| import React from 'react'; | ||
| export declare const AuthFailureMessage: () => React.JSX.Element; |
| /// <reference types="google.maps" /> | ||
| import { CameraStateRef } from './use-tracked-camera-state-ref'; | ||
| import { MapProps } from '../map'; | ||
| export declare function useMapCameraParams(map: google.maps.Map | null, cameraStateRef: CameraStateRef, mapProps: MapProps): void; |
| /// <reference types="google.maps" /> | ||
| import { Ref } from 'react'; | ||
| import { MapProps } from '../map'; | ||
| import { APIProviderContextValue } from '../api-provider'; | ||
| /** | ||
| * The main hook takes care of creating map-instances and registering them in | ||
| * the api-provider context. | ||
| * @return a tuple of the map-instance created (or null) and the callback | ||
| * ref that will be used to pass the map-container into this hook. | ||
| * @internal | ||
| */ | ||
| export declare function useMapInstance(props: MapProps, context: APIProviderContextValue): readonly [map: google.maps.Map | null, containerRef: Ref<HTMLDivElement>]; |
| /// <reference types="google.maps" /> | ||
| import { MutableRefObject } from 'react'; | ||
| export type CameraState = { | ||
| center: google.maps.LatLngLiteral; | ||
| heading: number; | ||
| tilt: number; | ||
| zoom: number; | ||
| }; | ||
| export type CameraStateRef = MutableRefObject<CameraState>; | ||
| /** | ||
| * Creates a mutable ref object to track the last known state of the map camera. | ||
| * This is used in `useMapCameraParams` to reduce stuttering in normal operation | ||
| * by avoiding updates of the map camera with values that have already been processed. | ||
| */ | ||
| export declare function useTrackedCameraStateRef(map: google.maps.Map | null): CameraStateRef; |
| import { DependencyList, EffectCallback } from 'react'; | ||
| export declare function useDeepCompareEffect(effect: EffectCallback, deps: DependencyList): void; |
| export declare function useForceUpdate(): () => void; |
| import React, {CSSProperties} from 'react'; | ||
| export const AuthFailureMessage = () => { | ||
| const style: CSSProperties = { | ||
| position: 'absolute', | ||
| top: 0, | ||
| left: 0, | ||
| bottom: 0, | ||
| right: 0, | ||
| zIndex: 999, | ||
| display: 'flex', | ||
| flexFlow: 'column nowrap', | ||
| textAlign: 'center', | ||
| justifyContent: 'center', | ||
| fontSize: '.8rem', | ||
| color: 'rgba(0,0,0,0.6)', | ||
| background: '#dddddd', | ||
| padding: '1rem 1.5rem' | ||
| }; | ||
| return ( | ||
| <div style={style}> | ||
| <h2>Error: AuthFailure</h2> | ||
| <p> | ||
| A problem with your API key prevents the map from rendering correctly. | ||
| Please make sure the value of the <code>APIProvider.apiKey</code> prop | ||
| is correct. Check the error-message in the console for further details. | ||
| </p> | ||
| </div> | ||
| ); | ||
| }; |
| import {useLayoutEffect} from 'react'; | ||
| import {CameraStateRef} from './use-tracked-camera-state-ref'; | ||
| import {toLatLngLiteral} from '../../libraries/lat-lng-utils'; | ||
| import {MapProps} from '../map'; | ||
| export function useMapCameraParams( | ||
| map: google.maps.Map | null, | ||
| cameraStateRef: CameraStateRef, | ||
| mapProps: MapProps | ||
| ) { | ||
| const center = mapProps.center ? toLatLngLiteral(mapProps.center) : null; | ||
| let lat: number | null = null; | ||
| let lng: number | null = null; | ||
| if (center && Number.isFinite(center.lat) && Number.isFinite(center.lng)) { | ||
| lat = center.lat as number; | ||
| lng = center.lng as number; | ||
| } | ||
| const zoom: number | null = Number.isFinite(mapProps.zoom) | ||
| ? (mapProps.zoom as number) | ||
| : null; | ||
| const heading: number | null = Number.isFinite(mapProps.heading) | ||
| ? (mapProps.heading as number) | ||
| : null; | ||
| const tilt: number | null = Number.isFinite(mapProps.tilt) | ||
| ? (mapProps.tilt as number) | ||
| : null; | ||
| // the following effect runs for every render of the map component and checks | ||
| // if there are differences between the known state of the map instance | ||
| // (cameraStateRef, which is updated by all bounds_changed events) and the | ||
| // desired state in the props. | ||
| useLayoutEffect(() => { | ||
| if (!map) return; | ||
| const nextCamera: google.maps.CameraOptions = {}; | ||
| let needsUpdate = false; | ||
| if ( | ||
| lat !== null && | ||
| lng !== null && | ||
| (cameraStateRef.current.center.lat !== lat || | ||
| cameraStateRef.current.center.lng !== lng) | ||
| ) { | ||
| nextCamera.center = {lat, lng}; | ||
| needsUpdate = true; | ||
| } | ||
| if (zoom !== null && cameraStateRef.current.zoom !== zoom) { | ||
| nextCamera.zoom = zoom as number; | ||
| needsUpdate = true; | ||
| } | ||
| if (heading !== null && cameraStateRef.current.heading !== heading) { | ||
| nextCamera.heading = heading as number; | ||
| needsUpdate = true; | ||
| } | ||
| if (tilt !== null && cameraStateRef.current.tilt !== tilt) { | ||
| nextCamera.tilt = tilt as number; | ||
| needsUpdate = true; | ||
| } | ||
| if (needsUpdate) { | ||
| map.moveCamera(nextCamera); | ||
| } | ||
| }); | ||
| } |
| import {Ref, useEffect, useRef, useState} from 'react'; | ||
| import {MapProps} from '../map'; | ||
| import {APIProviderContextValue} from '../api-provider'; | ||
| import {useCallbackRef} from '../../libraries/use-callback-ref'; | ||
| import {useApiIsLoaded} from '../../hooks/use-api-is-loaded'; | ||
| /** | ||
| * The main hook takes care of creating map-instances and registering them in | ||
| * the api-provider context. | ||
| * @return a tuple of the map-instance created (or null) and the callback | ||
| * ref that will be used to pass the map-container into this hook. | ||
| * @internal | ||
| */ | ||
| export function useMapInstance( | ||
| props: MapProps, | ||
| context: APIProviderContextValue | ||
| ): readonly [map: google.maps.Map | null, containerRef: Ref<HTMLDivElement>] { | ||
| const apiIsLoaded = useApiIsLoaded(); | ||
| const [map, setMap] = useState<google.maps.Map | null>(null); | ||
| const [container, containerRef] = useCallbackRef<HTMLDivElement>(); | ||
| const { | ||
| id, | ||
| defaultBounds, | ||
| defaultCenter, | ||
| defaultZoom, | ||
| defaultHeading, | ||
| defaultTilt, | ||
| ...mapOptions | ||
| } = props; | ||
| // apply default camera props if available and not overwritten by controlled props | ||
| if (!mapOptions.center && defaultCenter) mapOptions.center = defaultCenter; | ||
| if (!mapOptions.zoom && Number.isFinite(defaultZoom)) | ||
| mapOptions.zoom = defaultZoom; | ||
| if (!mapOptions.heading && Number.isFinite(defaultHeading)) | ||
| mapOptions.heading = defaultHeading; | ||
| if (!mapOptions.tilt && Number.isFinite(defaultTilt)) | ||
| mapOptions.tilt = defaultTilt; | ||
| // create the map instance and register it in the context | ||
| useEffect( | ||
| () => { | ||
| if (!container || !apiIsLoaded) return; | ||
| const {addMapInstance, removeMapInstance} = context; | ||
| const newMap = new google.maps.Map(container, mapOptions); | ||
| setMap(newMap); | ||
| addMapInstance(newMap, id); | ||
| if (defaultBounds) { | ||
| newMap.fitBounds(defaultBounds); | ||
| } | ||
| // FIXME: When the mapId is changed, we need to maintain the current camera params. | ||
| return () => { | ||
| if (!container || !apiIsLoaded) return; | ||
| // remove all event-listeners to minimize memory-leaks | ||
| google.maps.event.clearInstanceListeners(newMap); | ||
| setMap(null); | ||
| removeMapInstance(id); | ||
| }; | ||
| }, | ||
| // some dependencies are ignored in the list below: | ||
| // - defaultBounds and the default* camera props will only be used once, and | ||
| // changes should be ignored | ||
| // - mapOptions has special hooks that take care of updating the options | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| [container, apiIsLoaded, id, props.mapId] | ||
| ); | ||
| return [map, containerRef] as const; | ||
| } |
| import {MutableRefObject, useEffect, useRef} from 'react'; | ||
| import {useForceUpdate} from '../../libraries/use-force-update'; | ||
| export type CameraState = { | ||
| center: google.maps.LatLngLiteral; | ||
| heading: number; | ||
| tilt: number; | ||
| zoom: number; | ||
| }; | ||
| export type CameraStateRef = MutableRefObject<CameraState>; | ||
| function handleBoundsChange(map: google.maps.Map, ref: CameraStateRef) { | ||
| const center = map.getCenter(); | ||
| const zoom = map.getZoom(); | ||
| const heading = map.getHeading() || 0; | ||
| const tilt = map.getTilt() || 0; | ||
| const bounds = map.getBounds(); | ||
| if (!center || !bounds || !Number.isFinite(zoom)) { | ||
| console.warn( | ||
| '[useTrackedCameraState] at least one of the values from the map ' + | ||
| 'returned undefined. This is not expected to happen. Please ' + | ||
| 'report an issue at https://github.com/visgl/react-google-maps/issues/new' | ||
| ); | ||
| } | ||
| // fixme: do we need the `undefined` cases for the camera-params? When are they used in the maps API? | ||
| Object.assign(ref.current, { | ||
| center: center?.toJSON() || {lat: 0, lng: 0}, | ||
| zoom: (zoom as number) || 0, | ||
| heading: heading as number, | ||
| tilt: tilt as number | ||
| }); | ||
| } | ||
| /** | ||
| * Creates a mutable ref object to track the last known state of the map camera. | ||
| * This is used in `useMapCameraParams` to reduce stuttering in normal operation | ||
| * by avoiding updates of the map camera with values that have already been processed. | ||
| */ | ||
| export function useTrackedCameraStateRef( | ||
| map: google.maps.Map | null | ||
| ): CameraStateRef { | ||
| const forceUpdate = useForceUpdate(); | ||
| const ref = useRef<CameraState>({ | ||
| center: {lat: 0, lng: 0}, | ||
| heading: 0, | ||
| tilt: 0, | ||
| zoom: 0 | ||
| }); | ||
| // Record camera state with every bounds_changed event dispatched by the map. | ||
| // This data is used to prevent feeding these values back to the | ||
| // map-instance when a typical "controlled component" setup (state variable is | ||
| // fed into and updated by the map). | ||
| useEffect(() => { | ||
| if (!map) return; | ||
| const listener = google.maps.event.addListener( | ||
| map, | ||
| 'bounds_changed', | ||
| () => { | ||
| handleBoundsChange(map, ref); | ||
| // When an event is occured, we have to update during the next cycle. | ||
| // The application could decide to ignore the event and not update any | ||
| // camera props of the map, meaning that in that case we will have to | ||
| // 'undo' the change to the camera. | ||
| forceUpdate(); | ||
| } | ||
| ); | ||
| return () => listener.remove(); | ||
| }, [map, forceUpdate]); | ||
| return ref; | ||
| } |
| import {DependencyList, EffectCallback, useEffect, useRef} from 'react'; | ||
| import isDeepEqual from 'fast-deep-equal/react'; | ||
| export function useDeepCompareEffect( | ||
| effect: EffectCallback, | ||
| deps: DependencyList | ||
| ) { | ||
| const ref = useRef<DependencyList | undefined>(undefined); | ||
| if (!ref.current || !isDeepEqual(deps, ref.current)) { | ||
| ref.current = deps; | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| useEffect(effect, ref.current); | ||
| } |
| import {useReducer} from 'react'; | ||
| export function useForceUpdate(): () => void { | ||
| const [, forceUpdate] = useReducer(x => x + 1, 0); | ||
| return forceUpdate; | ||
| } |
| /// <reference types="google.maps" /> | ||
| import React, { CSSProperties, PropsWithChildren } from 'react'; | ||
| import { MapEventProps } from './use-map-events'; | ||
| import { DeckGlCompatProps } from './use-deckgl-camera-update'; | ||
| export interface GoogleMapsContextValue { | ||
@@ -9,37 +10,38 @@ map: google.maps.Map | null; | ||
| export type { MapCameraChangedEvent, MapEvent, MapEventProps, MapMouseEvent } from './use-map-events'; | ||
| export type MapCameraProps = { | ||
| center: google.maps.LatLngLiteral; | ||
| zoom: number; | ||
| heading?: number; | ||
| tilt?: number; | ||
| }; | ||
| /** | ||
| * Props for the Google Maps Map Component | ||
| */ | ||
| export type MapProps = google.maps.MapOptions & MapEventProps & { | ||
| style?: CSSProperties; | ||
| export type MapProps = google.maps.MapOptions & MapEventProps & DeckGlCompatProps & { | ||
| /** | ||
| * Adds custom style to the map by passing a css class. | ||
| * An id for the map, this is required when multiple maps are present | ||
| * in the same APIProvider context. | ||
| */ | ||
| className?: string; | ||
| id?: string; | ||
| /** | ||
| * Adds initial bounds to the map as an alternative to specifying the center/zoom of the map. | ||
| * Calls the fitBounds method internally https://developers.google.com/maps/documentation/javascript/reference/map?hl=en#Map-Methods | ||
| * Additional style rules to apply to the map dom-element. | ||
| */ | ||
| initialBounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral; | ||
| style?: CSSProperties; | ||
| /** | ||
| * An id that is added to the map. Needed when using more than one Map component. | ||
| * This is also needed to reference the map inside the useMap hook. | ||
| * Additional css class-name to apply to the element containing the map. | ||
| */ | ||
| id?: string; | ||
| className?: string; | ||
| /** | ||
| * Viewport from deck.gl | ||
| * Indicates that the map will be controlled externally. Disables all controls provided by the map itself. | ||
| */ | ||
| viewport?: unknown; | ||
| controlled?: boolean; | ||
| defaultCenter?: google.maps.LatLngLiteral; | ||
| defaultZoom?: number; | ||
| defaultHeading?: number; | ||
| defaultTilt?: number; | ||
| /** | ||
| * View state from deck.gl | ||
| * Alternative way to specify the default camera props as a geographic region that should be fully visible | ||
| */ | ||
| viewState?: Record<string, unknown>; | ||
| /** | ||
| * Initial View State from deck.gl | ||
| */ | ||
| initialViewState?: Record<string, unknown>; | ||
| defaultBounds?: google.maps.LatLngBoundsLiteral; | ||
| }; | ||
| /** | ||
| * Component to render a Google Maps map | ||
| */ | ||
| export declare const Map: { | ||
@@ -46,0 +48,0 @@ (props: PropsWithChildren<MapProps>): React.JSX.Element; |
| /// <reference types="google.maps" /> | ||
| export type DeckGlCompatProps = { | ||
| /** | ||
| * Viewport from deck.gl | ||
| */ | ||
| viewport?: unknown; | ||
| /** | ||
| * View state from deck.gl | ||
| */ | ||
| viewState?: Record<string, unknown>; | ||
| /** | ||
| * Initial View State from deck.gl | ||
| */ | ||
| initialViewState?: Record<string, unknown>; | ||
| }; | ||
| /** | ||
@@ -6,2 +20,2 @@ * Internal hook that updates the camera when deck.gl viewState changes. | ||
| */ | ||
| export declare function useDeckGLCameraUpdate(map: google.maps.Map | null, viewState: Record<string, unknown> | undefined): void; | ||
| export declare function useDeckGLCameraUpdate(map: google.maps.Map | null, props: DeckGlCompatProps): boolean; |
| /// <reference types="google.maps" /> | ||
| import { InternalCameraStateRef } from './use-internal-camera-state'; | ||
| /** | ||
@@ -13,2 +12,3 @@ * Handlers for all events that could be emitted by map-instances. | ||
| onProjectionChanged: (event: MapCameraChangedEvent) => void; | ||
| onCameraChanged: (event: MapCameraChangedEvent) => void; | ||
| onClick: (event: MapMouseEvent) => void; | ||
@@ -34,3 +34,3 @@ onDblclick: (event: MapMouseEvent) => void; | ||
| */ | ||
| export declare function useMapEvents(map: google.maps.Map | null, cameraStateRef: InternalCameraStateRef, props: MapEventProps): void; | ||
| export declare function useMapEvents(map: google.maps.Map | null, props: MapEventProps): void; | ||
| export type MapEvent<T = unknown> = { | ||
@@ -37,0 +37,0 @@ type: string; |
| /// <reference types="google.maps" /> | ||
| import { MapProps } from '@vis.gl/react-google-maps'; | ||
| import { InternalCameraStateRef } from './use-internal-camera-state'; | ||
| import { MapProps } from '../map'; | ||
| /** | ||
| * Internal hook to update the map-options and camera parameters when | ||
| * props are changed. | ||
| * Internal hook to update the map-options when props are changed. | ||
| * | ||
| * @param map the map instance | ||
| * @param cameraStateRef stores the last values seen during dispatch into the | ||
| * react-application in useMapEvents(). We can safely assume that we | ||
| * don't need to feed these values back into the map. | ||
| * @param mapProps the props to update the map-instance with | ||
| * @internal | ||
| */ | ||
| export declare function useMapOptions(map: google.maps.Map | null, cameraStateRef: InternalCameraStateRef, mapProps: MapProps): void; | ||
| export declare function useMapOptions(map: google.maps.Map | null, mapProps: MapProps): void; |
+44
-0
@@ -23,2 +23,7 @@ .control-panel { | ||
| .control-panel h4 { | ||
| font-weight: 500; | ||
| margin: 8px 0; | ||
| } | ||
| .control-panel p { | ||
@@ -47,1 +52,40 @@ margin-bottom: 16px; | ||
| } | ||
| .autocomplete-container input, | ||
| .autocomplete-control { | ||
| box-sizing: border-box; | ||
| } | ||
| .autocomplete-control { | ||
| margin: 24px; | ||
| background: #fff; | ||
| } | ||
| .autocomplete-container { | ||
| width: 300px; | ||
| } | ||
| .autocomplete-container input { | ||
| width: 100%; | ||
| height: 40px; | ||
| padding: 0 12px; | ||
| font-size: 18px; | ||
| } | ||
| .autocomplete-container .custom-list { | ||
| width: 100%; | ||
| list-style: none; | ||
| padding: 0; | ||
| margin: 0; | ||
| } | ||
| .autocomplete-container .custom-list-item { | ||
| padding: 8px; | ||
| } | ||
| .autocomplete-container .custom-list-item:hover { | ||
| background: lightgrey; | ||
| cursor: pointer; | ||
| } | ||
| .autocomplete-mode { | ||
| margin: 8px 0; | ||
| } |
+0
-1
@@ -12,3 +12,2 @@ export * from './components/advanced-marker'; | ||
| export * from './hooks/use-map'; | ||
| export * from './hooks/autocomplete'; | ||
| export * from './hooks/directions-service'; | ||
@@ -15,0 +14,0 @@ export * from './hooks/street-view-panorama'; |
+328
-290
@@ -1,4 +0,19 @@ | ||
| import React, { useState, useReducer, useMemo, useCallback, useEffect, useContext, useRef, useLayoutEffect, forwardRef, useImperativeHandle, Children } from 'react'; | ||
| import React, { useState, useReducer, useMemo, useCallback, useEffect, useRef, useContext, useLayoutEffect, forwardRef, useImperativeHandle, Children } from 'react'; | ||
| import { createPortal } from 'react-dom'; | ||
| import isDeepEqual from 'fast-deep-equal/react'; | ||
| function _toPrimitive(t, r) { | ||
| if ("object" != typeof t || !t) return t; | ||
| var e = t[Symbol.toPrimitive]; | ||
| if (void 0 !== e) { | ||
| var i = e.call(t, r || "default"); | ||
| if ("object" != typeof i) return i; | ||
| throw new TypeError("@@toPrimitive must return a primitive value."); | ||
| } | ||
| return ("string" === r ? String : Number)(t); | ||
| } | ||
| function _toPropertyKey(t) { | ||
| var i = _toPrimitive(t, "string"); | ||
| return "symbol" == typeof i ? i : String(i); | ||
| } | ||
| function _extends() { | ||
@@ -30,16 +45,2 @@ _extends = Object.assign ? Object.assign.bind() : function (target) { | ||
| } | ||
| function _toPrimitive(input, hint) { | ||
| if (typeof input !== "object" || input === null) return input; | ||
| var prim = input[Symbol.toPrimitive]; | ||
| if (prim !== undefined) { | ||
| var res = prim.call(input, hint || "default"); | ||
| if (typeof res !== "object") return res; | ||
| throw new TypeError("@@toPrimitive must return a primitive value."); | ||
| } | ||
| return (hint === "string" ? String : Number)(input); | ||
| } | ||
| function _toPropertyKey(arg) { | ||
| var key = _toPrimitive(arg, "string"); | ||
| return typeof key === "symbol" ? key : String(key); | ||
| } | ||
@@ -152,4 +153,4 @@ const APILoadingStatus = { | ||
| const _excluded$4 = ["onLoad", "apiKey", "version", "libraries"], | ||
| _excluded2$1 = ["children"]; | ||
| const _excluded$3 = ["onLoad", "apiKey", "version", "libraries"], | ||
| _excluded2 = ["children"]; | ||
| const APIProviderContext = React.createContext(null); | ||
@@ -194,3 +195,3 @@ /** | ||
| } = props, | ||
| otherApiParams = _objectWithoutPropertiesLoose(props, _excluded$4); | ||
| otherApiParams = _objectWithoutPropertiesLoose(props, _excluded$3); | ||
| const [status, setStatus] = useState(GoogleMapsApiLoader.loadingStatus); | ||
@@ -257,3 +258,3 @@ const [loadedLibraries, addLoadedLibrary] = useReducer((loadedLibraries, action) => { | ||
| } = props, | ||
| loaderProps = _objectWithoutPropertiesLoose(props, _excluded2$1); | ||
| loaderProps = _objectWithoutPropertiesLoose(props, _excluded2); | ||
| const { | ||
@@ -283,73 +284,7 @@ mapInstances, | ||
| function useApiLoadingStatus() { | ||
| var _useContext; | ||
| return ((_useContext = useContext(APIProviderContext)) == null ? void 0 : _useContext.status) || APILoadingStatus.NOT_LOADED; | ||
| } | ||
| /** | ||
| * Hook to check if the Google Maps API is loaded | ||
| */ | ||
| function useApiIsLoaded() { | ||
| const status = useApiLoadingStatus(); | ||
| return status === APILoadingStatus.LOADED; | ||
| } | ||
| const shownMessages = new Set(); | ||
| function logErrorOnce(...args) { | ||
| const key = JSON.stringify(args); | ||
| if (!shownMessages.has(key)) { | ||
| shownMessages.add(key); | ||
| console.error(...args); | ||
| } | ||
| } | ||
| function useCallbackRef() { | ||
| const [el, setEl] = useState(null); | ||
| const ref = useCallback(value => setEl(value), [setEl]); | ||
| return [el, ref]; | ||
| } | ||
| /** | ||
| * Creates a mutable ref object to track the last known state of the map camera. | ||
| * This is updated by `trackDispatchedEvent` and used in `useMapOptions`. | ||
| */ | ||
| function useInternalCameraState() { | ||
| return useRef({ | ||
| center: { | ||
| lat: 0, | ||
| lng: 0 | ||
| }, | ||
| heading: 0, | ||
| tilt: 0, | ||
| zoom: 0 | ||
| }); | ||
| } | ||
| /** | ||
| * Records camera data from the last event dispatched to the React application | ||
| * in a mutable `IternalCameraStateRef`. | ||
| * This data can then be used to prevent feeding these values back to the | ||
| * map-instance when a typical "controlled component" setup (state variable is | ||
| * fed into and updated by the map). | ||
| */ | ||
| function trackDispatchedEvent(ev, cameraStateRef) { | ||
| const cameraEvent = ev; | ||
| // we're only interested in the camera-events here | ||
| if (!cameraEvent.detail.center) return; | ||
| const { | ||
| center, | ||
| zoom, | ||
| heading, | ||
| tilt | ||
| } = cameraEvent.detail; | ||
| cameraStateRef.current.center = center; | ||
| cameraStateRef.current.heading = heading; | ||
| cameraStateRef.current.tilt = tilt; | ||
| cameraStateRef.current.zoom = zoom; | ||
| } | ||
| /** | ||
| * Sets up effects to bind event-handlers for all event-props in MapEventProps. | ||
| * @internal | ||
| */ | ||
| function useMapEvents(map, cameraStateRef, props) { | ||
| function useMapEvents(map, props) { | ||
| // note: calling a useEffect hook from within a loop is prohibited by the | ||
@@ -369,8 +304,6 @@ // rules of hooks, but it's ok here since it's unconditional and the number | ||
| const listener = google.maps.event.addListener(map, eventType, ev => { | ||
| const mapEvent = createMapEvent(eventType, map, ev); | ||
| trackDispatchedEvent(mapEvent, cameraStateRef); | ||
| handler(mapEvent); | ||
| handler(createMapEvent(eventType, map, ev)); | ||
| }); | ||
| return () => listener.remove(); | ||
| }, [map, cameraStateRef, eventType, handler]); | ||
| }, [map, eventType, handler]); | ||
| } | ||
@@ -407,3 +340,3 @@ } | ||
| }, | ||
| zoom: zoom, | ||
| zoom: zoom || 0, | ||
| heading: heading, | ||
@@ -459,3 +392,7 @@ tilt: tilt, | ||
| onTiltChanged: 'tilt_changed', | ||
| onZoomChanged: 'zoom_changed' | ||
| onZoomChanged: 'zoom_changed', | ||
| // note: onCameraChanged is an alias for the bounds_changed event, | ||
| // since that is going to be fired in every situation where the camera is | ||
| // updated. | ||
| onCameraChanged: 'bounds_changed' | ||
| }; | ||
@@ -466,45 +403,20 @@ const cameraEventTypes = ['bounds_changed', 'center_changed', 'heading_changed', 'projection_changed', 'tilt_changed', 'zoom_changed']; | ||
| function isLatLngLiteral(obj) { | ||
| if (!obj || typeof obj !== 'object') return false; | ||
| if (!('lat' in obj && 'lng' in obj)) return false; | ||
| return Number.isFinite(obj.lat) && Number.isFinite(obj.lng); | ||
| function useDeepCompareEffect(effect, deps) { | ||
| const ref = useRef(undefined); | ||
| if (!ref.current || !isDeepEqual(deps, ref.current)) { | ||
| ref.current = deps; | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| useEffect(effect, ref.current); | ||
| } | ||
| function latLngEquals(a, b) { | ||
| if (!a || !b) return false; | ||
| const A = toLatLngLiteral(a); | ||
| const B = toLatLngLiteral(b); | ||
| if (A.lat !== B.lat || A.lng !== B.lng) return false; | ||
| return true; | ||
| } | ||
| function toLatLngLiteral(obj) { | ||
| return { | ||
| lat: typeof obj.lat === 'function' ? obj.lat() : obj.lat, | ||
| lng: typeof obj.lng === 'function' ? obj.lng() : obj.lng | ||
| }; | ||
| } | ||
| const _excluded$3 = ["center", "zoom", "heading", "tilt"], | ||
| _excluded2 = ["mapId"]; | ||
| const mapOptionKeys = new Set(['backgroundColor', 'clickableIcons', 'controlSize', 'disableDefaultUI', 'disableDoubleClickZoom', 'draggable', 'draggableCursor', 'draggingCursor', 'fullscreenControl', 'fullscreenControlOptions', 'gestureHandling', 'isFractionalZoomEnabled', 'keyboardShortcuts', 'mapTypeControl', 'mapTypeControlOptions', 'mapTypeId', 'maxZoom', 'minZoom', 'noClear', 'panControl', 'panControlOptions', 'restriction', 'rotateControl', 'rotateControlOptions', 'scaleControl', 'scaleControlOptions', 'scrollwheel', 'streetView', 'streetViewControl', 'streetViewControlOptions', 'styles', 'zoomControl', 'zoomControlOptions']); | ||
| /** | ||
| * Internal hook to update the map-options and camera parameters when | ||
| * props are changed. | ||
| * Internal hook to update the map-options when props are changed. | ||
| * | ||
| * @param map the map instance | ||
| * @param cameraStateRef stores the last values seen during dispatch into the | ||
| * react-application in useMapEvents(). We can safely assume that we | ||
| * don't need to feed these values back into the map. | ||
| * @param mapProps the props to update the map-instance with | ||
| * @internal | ||
| */ | ||
| function useMapOptions(map, cameraStateRef, mapProps) { | ||
| const { | ||
| center: rawCenter, | ||
| zoom, | ||
| heading, | ||
| tilt | ||
| } = mapProps, | ||
| mapOptions = _objectWithoutPropertiesLoose(mapProps, _excluded$3); | ||
| const center = rawCenter ? isLatLngLiteral(rawCenter) ? rawCenter : rawCenter.toJSON() : null; | ||
| const lat = center && center.lat; | ||
| const lng = center && center.lng; | ||
| function useMapOptions(map, mapProps) { | ||
| /* eslint-disable react-hooks/exhaustive-deps -- | ||
@@ -514,68 +426,98 @@ * | ||
| * In that case, the values will be or have been passed to the map | ||
| * constructor as mapOptions. | ||
| * constructor via mapOptions. | ||
| */ | ||
| const mapOptions = {}; | ||
| const keys = Object.keys(mapProps); | ||
| for (const key of keys) { | ||
| if (!mapOptionKeys.has(key)) continue; | ||
| mapOptions[key] = mapProps[key]; | ||
| } | ||
| // update the map options when mapOptions is changed | ||
| // Note: due to the destructuring above, mapOptions will be seen as changed | ||
| // with every re-render, so we're boldly assuming the maps-api will properly | ||
| // with every re-render, so we're assuming the maps-api will properly | ||
| // deal with unchanged option-values passed into setOptions. | ||
| useEffect(() => { | ||
| useDeepCompareEffect(() => { | ||
| if (!map) return; | ||
| // Changing the mapId via setOptions will trigger an error-message. | ||
| // We will re-create the map-instance in that case anyway, so we | ||
| // remove it here to avoid this error-message. | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| const opts = _objectWithoutPropertiesLoose(mapOptions, _excluded2); | ||
| map.setOptions(opts); | ||
| map.setOptions(mapOptions); | ||
| }, [mapOptions]); | ||
| useLayoutEffect(() => { | ||
| if (!map || !Number.isFinite(lat) || !Number.isFinite(lng)) return; | ||
| if (cameraStateRef.current.center.lat === lat && cameraStateRef.current.center.lng === lng) return; | ||
| map.moveCamera({ | ||
| center: { | ||
| lat: lat, | ||
| lng: lng | ||
| } | ||
| }); | ||
| }, [lat, lng]); | ||
| useLayoutEffect(() => { | ||
| if (!map || !Number.isFinite(zoom)) return; | ||
| if (cameraStateRef.current.zoom === zoom) return; | ||
| map.moveCamera({ | ||
| zoom: zoom | ||
| }); | ||
| }, [zoom]); | ||
| useLayoutEffect(() => { | ||
| if (!map || !Number.isFinite(heading)) return; | ||
| if (cameraStateRef.current.heading === heading) return; | ||
| map.moveCamera({ | ||
| heading: heading | ||
| }); | ||
| }, [heading]); | ||
| useLayoutEffect(() => { | ||
| if (!map || !Number.isFinite(tilt)) return; | ||
| if (cameraStateRef.current.tilt === tilt) return; | ||
| map.moveCamera({ | ||
| tilt: tilt | ||
| }); | ||
| }, [tilt]); | ||
| /* eslint-enable react-hooks/exhaustive-deps */ | ||
| } | ||
| function useForceUpdate() { | ||
| const [, forceUpdate] = useReducer(x => x + 1, 0); | ||
| return forceUpdate; | ||
| } | ||
| function handleBoundsChange(map, ref) { | ||
| const center = map.getCenter(); | ||
| const zoom = map.getZoom(); | ||
| const heading = map.getHeading() || 0; | ||
| const tilt = map.getTilt() || 0; | ||
| const bounds = map.getBounds(); | ||
| if (!center || !bounds || !Number.isFinite(zoom)) { | ||
| console.warn('[useTrackedCameraState] at least one of the values from the map ' + 'returned undefined. This is not expected to happen. Please ' + 'report an issue at https://github.com/visgl/react-google-maps/issues/new'); | ||
| } | ||
| // fixme: do we need the `undefined` cases for the camera-params? When are they used in the maps API? | ||
| Object.assign(ref.current, { | ||
| center: (center == null ? void 0 : center.toJSON()) || { | ||
| lat: 0, | ||
| lng: 0 | ||
| }, | ||
| zoom: zoom || 0, | ||
| heading: heading, | ||
| tilt: tilt | ||
| }); | ||
| } | ||
| /** | ||
| * Creates a mutable ref object to track the last known state of the map camera. | ||
| * This is used in `useMapCameraParams` to reduce stuttering in normal operation | ||
| * by avoiding updates of the map camera with values that have already been processed. | ||
| */ | ||
| function useTrackedCameraStateRef(map) { | ||
| const forceUpdate = useForceUpdate(); | ||
| const ref = useRef({ | ||
| center: { | ||
| lat: 0, | ||
| lng: 0 | ||
| }, | ||
| heading: 0, | ||
| tilt: 0, | ||
| zoom: 0 | ||
| }); | ||
| // Record camera state with every bounds_changed event dispatched by the map. | ||
| // This data is used to prevent feeding these values back to the | ||
| // map-instance when a typical "controlled component" setup (state variable is | ||
| // fed into and updated by the map). | ||
| useEffect(() => { | ||
| if (!map) return; | ||
| const listener = google.maps.event.addListener(map, 'bounds_changed', () => { | ||
| handleBoundsChange(map, ref); | ||
| // When an event is occured, we have to update during the next cycle. | ||
| // The application could decide to ignore the event and not update any | ||
| // camera props of the map, meaning that in that case we will have to | ||
| // 'undo' the change to the camera. | ||
| forceUpdate(); | ||
| }); | ||
| return () => listener.remove(); | ||
| }, [map, forceUpdate]); | ||
| return ref; | ||
| } | ||
| function useApiLoadingStatus() { | ||
| var _useContext; | ||
| return ((_useContext = useContext(APIProviderContext)) == null ? void 0 : _useContext.status) || APILoadingStatus.NOT_LOADED; | ||
| } | ||
| /** | ||
| * Internal hook that updates the camera when deck.gl viewState changes. | ||
| * @internal | ||
| */ | ||
| function useDeckGLCameraUpdate(map, viewState) { | ||
| function useDeckGLCameraUpdate(map, props) { | ||
| const { | ||
| viewport, | ||
| viewState | ||
| } = props; | ||
| const isDeckGlControlled = !!viewport; | ||
| useLayoutEffect(() => { | ||
| if (!map || !viewState) { | ||
| return; | ||
| } | ||
| // FIXME: this should probably be extracted into a seperate hook that only | ||
| // runs once when first seeing a deck.gl viewState update and resets | ||
| // again. Maybe even use a seperate prop (`<Map controlled />`) instead. | ||
| map.setOptions({ | ||
| gestureHandling: 'none', | ||
| keyboardShortcuts: false, | ||
| disableDefaultUI: true | ||
| }); | ||
| if (!map || !viewState) return; | ||
| const { | ||
@@ -598,57 +540,66 @@ latitude, | ||
| }, [map, viewState]); | ||
| return isDeckGlControlled; | ||
| } | ||
| const _excluded$2 = ["id", "initialBounds"]; | ||
| const GoogleMapsContext = React.createContext(null); | ||
| /** | ||
| * Component to render a Google Maps map | ||
| */ | ||
| const Map = props => { | ||
| const { | ||
| children, | ||
| id, | ||
| className, | ||
| style, | ||
| viewState, | ||
| viewport | ||
| } = props; | ||
| const context = useContext(APIProviderContext); | ||
| const loadingStatus = useApiLoadingStatus(); | ||
| if (!context) { | ||
| throw new Error('<Map> can only be used inside an <ApiProvider> component.'); | ||
| function isLatLngLiteral(obj) { | ||
| if (!obj || typeof obj !== 'object') return false; | ||
| if (!('lat' in obj && 'lng' in obj)) return false; | ||
| return Number.isFinite(obj.lat) && Number.isFinite(obj.lng); | ||
| } | ||
| function latLngEquals(a, b) { | ||
| if (!a || !b) return false; | ||
| const A = toLatLngLiteral(a); | ||
| const B = toLatLngLiteral(b); | ||
| if (A.lat !== B.lat || A.lng !== B.lng) return false; | ||
| return true; | ||
| } | ||
| function toLatLngLiteral(obj) { | ||
| if (isLatLngLiteral(obj)) return obj; | ||
| return obj.toJSON(); | ||
| } | ||
| function useMapCameraParams(map, cameraStateRef, mapProps) { | ||
| const center = mapProps.center ? toLatLngLiteral(mapProps.center) : null; | ||
| let lat = null; | ||
| let lng = null; | ||
| if (center && Number.isFinite(center.lat) && Number.isFinite(center.lng)) { | ||
| lat = center.lat; | ||
| lng = center.lng; | ||
| } | ||
| const [map, mapRef] = useMapInstance(props, context); | ||
| const cameraStateRef = useInternalCameraState(); | ||
| useMapOptions(map, cameraStateRef, props); | ||
| useMapEvents(map, cameraStateRef, props); | ||
| useDeckGLCameraUpdate(map, viewState); | ||
| const isViewportSet = useMemo(() => Boolean(viewport), [viewport]); | ||
| const combinedStyle = useMemo(() => _extends({ | ||
| width: '100%', | ||
| height: '100%', | ||
| // when using deckgl, the map should be sent to the back | ||
| zIndex: isViewportSet ? -1 : 0 | ||
| }, style), [style, isViewportSet]); | ||
| if (loadingStatus === APILoadingStatus.AUTH_FAILURE) { | ||
| return /*#__PURE__*/React.createElement("div", { | ||
| style: _extends({ | ||
| position: 'relative' | ||
| }, className ? {} : combinedStyle), | ||
| className: className | ||
| }, /*#__PURE__*/React.createElement(AuthFailureMessage, null)); | ||
| } | ||
| return /*#__PURE__*/React.createElement("div", _extends({ | ||
| ref: mapRef, | ||
| "data-testid": 'map', | ||
| style: className ? undefined : combinedStyle, | ||
| className: className | ||
| }, id ? { | ||
| id | ||
| } : {}), map ? /*#__PURE__*/React.createElement(GoogleMapsContext.Provider, { | ||
| value: { | ||
| map | ||
| const zoom = Number.isFinite(mapProps.zoom) ? mapProps.zoom : null; | ||
| const heading = Number.isFinite(mapProps.heading) ? mapProps.heading : null; | ||
| const tilt = Number.isFinite(mapProps.tilt) ? mapProps.tilt : null; | ||
| // the following effect runs for every render of the map component and checks | ||
| // if there are differences between the known state of the map instance | ||
| // (cameraStateRef, which is updated by all bounds_changed events) and the | ||
| // desired state in the props. | ||
| useLayoutEffect(() => { | ||
| if (!map) return; | ||
| const nextCamera = {}; | ||
| let needsUpdate = false; | ||
| if (lat !== null && lng !== null && (cameraStateRef.current.center.lat !== lat || cameraStateRef.current.center.lng !== lng)) { | ||
| nextCamera.center = { | ||
| lat, | ||
| lng | ||
| }; | ||
| needsUpdate = true; | ||
| } | ||
| }, children) : null); | ||
| }; | ||
| Map.deckGLViewProps = true; | ||
| if (zoom !== null && cameraStateRef.current.zoom !== zoom) { | ||
| nextCamera.zoom = zoom; | ||
| needsUpdate = true; | ||
| } | ||
| if (heading !== null && cameraStateRef.current.heading !== heading) { | ||
| nextCamera.heading = heading; | ||
| needsUpdate = true; | ||
| } | ||
| if (tilt !== null && cameraStateRef.current.tilt !== tilt) { | ||
| nextCamera.tilt = tilt; | ||
| needsUpdate = true; | ||
| } | ||
| if (needsUpdate) { | ||
| map.moveCamera(nextCamera); | ||
| } | ||
| }); | ||
| } | ||
| const AuthFailureMessage = () => { | ||
@@ -675,3 +626,19 @@ const style = { | ||
| }; | ||
| function useCallbackRef() { | ||
| const [el, setEl] = useState(null); | ||
| const ref = useCallback(value => setEl(value), [setEl]); | ||
| return [el, ref]; | ||
| } | ||
| /** | ||
| * Hook to check if the Google Maps API is loaded | ||
| */ | ||
| function useApiIsLoaded() { | ||
| const status = useApiLoadingStatus(); | ||
| return status === APILoadingStatus.LOADED; | ||
| } | ||
| const _excluded$2 = ["id", "defaultBounds", "defaultCenter", "defaultZoom", "defaultHeading", "defaultTilt"]; | ||
| /** | ||
| * The main hook takes care of creating map-instances and registering them in | ||
@@ -689,5 +656,14 @@ * the api-provider context. | ||
| id, | ||
| initialBounds | ||
| defaultBounds, | ||
| defaultCenter, | ||
| defaultZoom, | ||
| defaultHeading, | ||
| defaultTilt | ||
| } = props, | ||
| mapOptions = _objectWithoutPropertiesLoose(props, _excluded$2); | ||
| // apply default camera props if available and not overwritten by controlled props | ||
| if (!mapOptions.center && defaultCenter) mapOptions.center = defaultCenter; | ||
| if (!mapOptions.zoom && Number.isFinite(defaultZoom)) mapOptions.zoom = defaultZoom; | ||
| if (!mapOptions.heading && Number.isFinite(defaultHeading)) mapOptions.heading = defaultHeading; | ||
| if (!mapOptions.tilt && Number.isFinite(defaultTilt)) mapOptions.tilt = defaultTilt; | ||
| // create the map instance and register it in the context | ||
@@ -703,5 +679,6 @@ useEffect(() => { | ||
| addMapInstance(newMap, id); | ||
| if (initialBounds) { | ||
| newMap.fitBounds(initialBounds); | ||
| if (defaultBounds) { | ||
| newMap.fitBounds(defaultBounds); | ||
| } | ||
| // FIXME: When the mapId is changed, we need to maintain the current camera params. | ||
| return () => { | ||
@@ -715,22 +692,114 @@ if (!container || !apiIsLoaded) return; | ||
| }, | ||
| // FIXME: we should rethink if it could be possible to keep the state | ||
| // around when a map gets re-initialized (id or mapId changed). This | ||
| // should keep the viewport as it is (so also no initial viewport in | ||
| // this case) and any added features should of course get re-added as | ||
| // well. | ||
| // some dependencies are ignored in the list below: | ||
| // - defaultBounds and the default* camera props will only be used once, and | ||
| // changes should be ignored | ||
| // - mapOptions has special hooks that take care of updating the options | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| [id, container, apiIsLoaded, props.mapId]); | ||
| // report an error if the same map-id is used multiple times | ||
| useEffect(() => { | ||
| if (!id) return; | ||
| const { | ||
| mapInstances | ||
| } = context; | ||
| if (mapInstances[id] && mapInstances[id] !== map) { | ||
| logErrorOnce(`The map id '${id}' seems to have been used multiple times. ` + 'This can lead to unexpected problems when accessing the maps. ' + 'Please use unique ids for all <Map> components.'); | ||
| } | ||
| }, [id, context, map]); | ||
| [container, apiIsLoaded, id, props.mapId]); | ||
| return [map, containerRef]; | ||
| } | ||
| const GoogleMapsContext = React.createContext(null); | ||
| const Map = props => { | ||
| const { | ||
| children, | ||
| id, | ||
| className, | ||
| style | ||
| } = props; | ||
| const context = useContext(APIProviderContext); | ||
| const loadingStatus = useApiLoadingStatus(); | ||
| if (!context) { | ||
| throw new Error('<Map> can only be used inside an <ApiProvider> component.'); | ||
| } | ||
| const [map, mapRef] = useMapInstance(props, context); | ||
| const cameraStateRef = useTrackedCameraStateRef(map); | ||
| useMapCameraParams(map, cameraStateRef, props); | ||
| useMapEvents(map, props); | ||
| useMapOptions(map, props); | ||
| const isDeckGlControlled = useDeckGLCameraUpdate(map, props); | ||
| const isControlledExternally = !!props.controlled; | ||
| // disable interactions with the map for externally controlled maps | ||
| useEffect(() => { | ||
| if (!map) return; | ||
| // fixme: this doesn't seem to belong here (and it's mostly there for convenience anyway). | ||
| // The reasoning is that a deck.gl canvas will be put on top of the map, rendering | ||
| // any default map controls pretty much useless | ||
| if (isDeckGlControlled) { | ||
| map.setOptions({ | ||
| disableDefaultUI: true | ||
| }); | ||
| } | ||
| // disable all control-inputs when the map is controlled externally | ||
| if (isDeckGlControlled || isControlledExternally) { | ||
| map.setOptions({ | ||
| gestureHandling: 'none', | ||
| keyboardShortcuts: false | ||
| }); | ||
| } | ||
| return () => { | ||
| map.setOptions({ | ||
| gestureHandling: props.gestureHandling, | ||
| keyboardShortcuts: props.keyboardShortcuts | ||
| }); | ||
| }; | ||
| }, [map, isDeckGlControlled, isControlledExternally, props.gestureHandling, props.keyboardShortcuts]); | ||
| // setup a stable cameraOptions object that can be used as dependency | ||
| const center = props.center ? toLatLngLiteral(props.center) : null; | ||
| let lat = null; | ||
| let lng = null; | ||
| if (center && Number.isFinite(center.lat) && Number.isFinite(center.lng)) { | ||
| lat = center.lat; | ||
| lng = center.lng; | ||
| } | ||
| const cameraOptions = useMemo(() => { | ||
| var _lat, _lng, _props$zoom, _props$heading, _props$tilt; | ||
| return { | ||
| center: { | ||
| lat: (_lat = lat) != null ? _lat : 0, | ||
| lng: (_lng = lng) != null ? _lng : 0 | ||
| }, | ||
| zoom: (_props$zoom = props.zoom) != null ? _props$zoom : 0, | ||
| heading: (_props$heading = props.heading) != null ? _props$heading : 0, | ||
| tilt: (_props$tilt = props.tilt) != null ? _props$tilt : 0 | ||
| }; | ||
| }, [lat, lng, props.zoom, props.heading, props.tilt]); | ||
| // externally controlled mode: reject all camera changes that don't correspond to changes in props | ||
| useLayoutEffect(() => { | ||
| if (!map || !isControlledExternally) return; | ||
| map.moveCamera(cameraOptions); | ||
| const listener = map.addListener('bounds_changed', () => { | ||
| map.moveCamera(cameraOptions); | ||
| }); | ||
| return () => listener.remove(); | ||
| }, [map, isControlledExternally, cameraOptions]); | ||
| const combinedStyle = useMemo(() => _extends({ | ||
| width: '100%', | ||
| height: '100%', | ||
| // when using deckgl, the map should be sent to the back | ||
| zIndex: isDeckGlControlled ? -1 : 0 | ||
| }, style), [style, isDeckGlControlled]); | ||
| if (loadingStatus === APILoadingStatus.AUTH_FAILURE) { | ||
| return /*#__PURE__*/React.createElement("div", { | ||
| style: _extends({ | ||
| position: 'relative' | ||
| }, className ? {} : combinedStyle), | ||
| className: className | ||
| }, /*#__PURE__*/React.createElement(AuthFailureMessage, null)); | ||
| } | ||
| return /*#__PURE__*/React.createElement("div", _extends({ | ||
| ref: mapRef, | ||
| "data-testid": 'map', | ||
| style: className ? undefined : combinedStyle, | ||
| className: className | ||
| }, id ? { | ||
| id | ||
| } : {}), map ? /*#__PURE__*/React.createElement(GoogleMapsContext.Provider, { | ||
| value: { | ||
| map | ||
| } | ||
| }, children) : null); | ||
| }; | ||
| Map.deckGLViewProps = true; | ||
| function useMapsLibrary(name) { | ||
@@ -918,2 +987,11 @@ const apiIsLoaded = useApiIsLoaded(); | ||
| const shownMessages = new Set(); | ||
| function logErrorOnce(...args) { | ||
| const key = JSON.stringify(args); | ||
| if (!shownMessages.has(key)) { | ||
| shownMessages.add(key); | ||
| console.error(...args); | ||
| } | ||
| } | ||
| /** | ||
@@ -1116,42 +1194,2 @@ * Retrieves a map-instance from the context. This is either an instance | ||
| /** | ||
| * Hook to get a Google Maps Places Autocomplete instance | ||
| * monitoring an input field | ||
| */ | ||
| const useAutocomplete = props => { | ||
| const { | ||
| inputField, | ||
| options, | ||
| onPlaceChanged | ||
| } = props; | ||
| const googleMapsAPIIsLoaded = useApiIsLoaded(); | ||
| const placeChangedHandler = useRef(onPlaceChanged); | ||
| const [autocomplete, setAutocomplete] = useState(null); | ||
| // Initializes the Google Maps Places Autocomplete | ||
| useEffect(() => { | ||
| // Wait for the Google Maps API and input element to be initialized | ||
| if (!googleMapsAPIIsLoaded || !inputField) return; | ||
| // FIXME: add dynamic loading for required libraries | ||
| if (!google.maps.places) { | ||
| console.error('Google Maps Places library is missing. ' + 'Please add the places library to the props of the <ApiProvider> ' + 'component.'); | ||
| return; | ||
| } | ||
| // Create Autocomplete instance | ||
| const autocompleteInstance = new google.maps.places.Autocomplete(inputField, options); | ||
| setAutocomplete(autocompleteInstance); | ||
| // Add places change listener to Autocomplete | ||
| autocompleteInstance.addListener('place_changed', () => { | ||
| const place = autocompleteInstance.getPlace(); | ||
| if (placeChangedHandler.current) placeChangedHandler.current(place); | ||
| }); | ||
| // Clear listeners on unmount | ||
| return () => { | ||
| if (autocompleteInstance && typeof google.maps === 'object') { | ||
| google.maps.event.clearInstanceListeners(autocompleteInstance); | ||
| } | ||
| }; | ||
| }, [googleMapsAPIIsLoaded, inputField, options]); | ||
| return autocomplete; | ||
| }; | ||
| /** | ||
| * A typescript assertion function used in cases where typescript has to be | ||
@@ -1304,3 +1342,3 @@ * convinced that the object in question can not be null. | ||
| export { APIProvider, APIProviderContext, AdvancedMarker, AdvancedMarkerContext, ControlPosition, GoogleMapsContext, InfoWindow, Map, MapControl, Marker, Pin, isLatLngLiteral, latLngEquals, limitTiltRange, toLatLngLiteral, useAdvancedMarkerRef, useApiIsLoaded, useApiLoadingStatus, useAutocomplete, useDirectionsService, useMap, useMapsLibrary, useMarkerRef, useStreetViewPanorama }; | ||
| export { APIProvider, APIProviderContext, AdvancedMarker, AdvancedMarkerContext, ControlPosition, GoogleMapsContext, InfoWindow, Map, MapControl, Marker, Pin, isLatLngLiteral, latLngEquals, limitTiltRange, toLatLngLiteral, useAdvancedMarkerRef, useApiIsLoaded, useApiLoadingStatus, useDirectionsService, useMap, useMapsLibrary, useMarkerRef, useStreetViewPanorama }; | ||
| //# sourceMappingURL=index.modern.mjs.map |
+3
-2
| { | ||
| "name": "@vis.gl/react-google-maps", | ||
| "version": "0.5.4", | ||
| "version": "0.6.0", | ||
| "description": "React components and hooks for Google Maps.", | ||
@@ -53,3 +53,4 @@ "source": "src/index.ts", | ||
| "dependencies": { | ||
| "@types/google.maps": "^3.54.10" | ||
| "@types/google.maps": "^3.54.10", | ||
| "fast-deep-equal": "^3.1.3" | ||
| }, | ||
@@ -56,0 +57,0 @@ "peerDependencies": { |
@@ -45,2 +45,6 @@ import React from 'react'; | ||
| }; | ||
| // no idea why the implementation in @googlemaps/jest-mocks doesn't work as it is, | ||
| // but this helps: | ||
| google.maps.event.addListener = jest.fn(() => ({remove: jest.fn()})); | ||
| }); | ||
@@ -47,0 +51,0 @@ |
+112
-145
@@ -5,20 +5,23 @@ /* eslint-disable complexity */ | ||
| PropsWithChildren, | ||
| Ref, | ||
| useContext, | ||
| useEffect, | ||
| useMemo, | ||
| useState | ||
| useLayoutEffect, | ||
| useMemo | ||
| } from 'react'; | ||
| import {APIProviderContext, APIProviderContextValue} from '../api-provider'; | ||
| import {APIProviderContext} from '../api-provider'; | ||
| import {useApiIsLoaded} from '../../hooks/use-api-is-loaded'; | ||
| import {logErrorOnce} from '../../libraries/errors'; | ||
| import {useCallbackRef} from '../../libraries/use-callback-ref'; | ||
| import {MapEventProps, useMapEvents} from './use-map-events'; | ||
| import {useMapOptions} from './use-map-options'; | ||
| import {useDeckGLCameraUpdate} from './use-deckgl-camera-update'; | ||
| import {useInternalCameraState} from './use-internal-camera-state'; | ||
| import {useTrackedCameraStateRef} from './use-tracked-camera-state-ref'; | ||
| import {useApiLoadingStatus} from '../../hooks/use-api-loading-status'; | ||
| import {APILoadingStatus} from '../../libraries/api-loading-status'; | ||
| import { | ||
| DeckGlCompatProps, | ||
| useDeckGLCameraUpdate | ||
| } from './use-deckgl-camera-update'; | ||
| import {toLatLngLiteral} from '../../libraries/lat-lng-utils'; | ||
| import {useMapCameraParams} from './use-map-camera-params'; | ||
| import {AuthFailureMessage} from './auth-failure-message'; | ||
| import {useMapInstance} from './use-map-instance'; | ||
@@ -38,2 +41,9 @@ export interface GoogleMapsContextValue { | ||
| export type MapCameraProps = { | ||
| center: google.maps.LatLngLiteral; | ||
| zoom: number; | ||
| heading?: number; | ||
| tilt?: number; | ||
| }; | ||
| /** | ||
@@ -43,38 +53,34 @@ * Props for the Google Maps Map Component | ||
| export type MapProps = google.maps.MapOptions & | ||
| MapEventProps & { | ||
| style?: CSSProperties; | ||
| MapEventProps & | ||
| DeckGlCompatProps & { | ||
| /** | ||
| * Adds custom style to the map by passing a css class. | ||
| * An id for the map, this is required when multiple maps are present | ||
| * in the same APIProvider context. | ||
| */ | ||
| className?: string; | ||
| id?: string; | ||
| /** | ||
| * Adds initial bounds to the map as an alternative to specifying the center/zoom of the map. | ||
| * Calls the fitBounds method internally https://developers.google.com/maps/documentation/javascript/reference/map?hl=en#Map-Methods | ||
| * Additional style rules to apply to the map dom-element. | ||
| */ | ||
| initialBounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral; | ||
| style?: CSSProperties; | ||
| /** | ||
| * An id that is added to the map. Needed when using more than one Map component. | ||
| * This is also needed to reference the map inside the useMap hook. | ||
| * Additional css class-name to apply to the element containing the map. | ||
| */ | ||
| id?: string; | ||
| className?: string; | ||
| /** | ||
| * Viewport from deck.gl | ||
| * Indicates that the map will be controlled externally. Disables all controls provided by the map itself. | ||
| */ | ||
| viewport?: unknown; | ||
| controlled?: boolean; | ||
| defaultCenter?: google.maps.LatLngLiteral; | ||
| defaultZoom?: number; | ||
| defaultHeading?: number; | ||
| defaultTilt?: number; | ||
| /** | ||
| * View state from deck.gl | ||
| * Alternative way to specify the default camera props as a geographic region that should be fully visible | ||
| */ | ||
| viewState?: Record<string, unknown>; | ||
| /** | ||
| * Initial View State from deck.gl | ||
| */ | ||
| initialViewState?: Record<string, unknown>; | ||
| defaultBounds?: google.maps.LatLngBoundsLiteral; | ||
| }; | ||
| /** | ||
| * Component to render a Google Maps map | ||
| */ | ||
| export const Map = (props: PropsWithChildren<MapProps>) => { | ||
| const {children, id, className, style, viewState, viewport} = props; | ||
| const {children, id, className, style} = props; | ||
| const context = useContext(APIProviderContext); | ||
@@ -90,8 +96,74 @@ const loadingStatus = useApiLoadingStatus(); | ||
| const [map, mapRef] = useMapInstance(props, context); | ||
| const cameraStateRef = useInternalCameraState(); | ||
| useMapOptions(map, cameraStateRef, props); | ||
| useMapEvents(map, cameraStateRef, props); | ||
| useDeckGLCameraUpdate(map, viewState); | ||
| const cameraStateRef = useTrackedCameraStateRef(map); | ||
| const isViewportSet = useMemo(() => Boolean(viewport), [viewport]); | ||
| useMapCameraParams(map, cameraStateRef, props); | ||
| useMapEvents(map, props); | ||
| useMapOptions(map, props); | ||
| const isDeckGlControlled = useDeckGLCameraUpdate(map, props); | ||
| const isControlledExternally = !!props.controlled; | ||
| // disable interactions with the map for externally controlled maps | ||
| useEffect(() => { | ||
| if (!map) return; | ||
| // fixme: this doesn't seem to belong here (and it's mostly there for convenience anyway). | ||
| // The reasoning is that a deck.gl canvas will be put on top of the map, rendering | ||
| // any default map controls pretty much useless | ||
| if (isDeckGlControlled) { | ||
| map.setOptions({disableDefaultUI: true}); | ||
| } | ||
| // disable all control-inputs when the map is controlled externally | ||
| if (isDeckGlControlled || isControlledExternally) { | ||
| map.setOptions({ | ||
| gestureHandling: 'none', | ||
| keyboardShortcuts: false | ||
| }); | ||
| } | ||
| return () => { | ||
| map.setOptions({ | ||
| gestureHandling: props.gestureHandling, | ||
| keyboardShortcuts: props.keyboardShortcuts | ||
| }); | ||
| }; | ||
| }, [ | ||
| map, | ||
| isDeckGlControlled, | ||
| isControlledExternally, | ||
| props.gestureHandling, | ||
| props.keyboardShortcuts | ||
| ]); | ||
| // setup a stable cameraOptions object that can be used as dependency | ||
| const center = props.center ? toLatLngLiteral(props.center) : null; | ||
| let lat: number | null = null; | ||
| let lng: number | null = null; | ||
| if (center && Number.isFinite(center.lat) && Number.isFinite(center.lng)) { | ||
| lat = center.lat as number; | ||
| lng = center.lng as number; | ||
| } | ||
| const cameraOptions: google.maps.CameraOptions = useMemo(() => { | ||
| return { | ||
| center: {lat: lat ?? 0, lng: lng ?? 0}, | ||
| zoom: props.zoom ?? 0, | ||
| heading: props.heading ?? 0, | ||
| tilt: props.tilt ?? 0 | ||
| }; | ||
| }, [lat, lng, props.zoom, props.heading, props.tilt]); | ||
| // externally controlled mode: reject all camera changes that don't correspond to changes in props | ||
| useLayoutEffect(() => { | ||
| if (!map || !isControlledExternally) return; | ||
| map.moveCamera(cameraOptions); | ||
| const listener = map.addListener('bounds_changed', () => { | ||
| map.moveCamera(cameraOptions); | ||
| }); | ||
| return () => listener.remove(); | ||
| }, [map, isControlledExternally, cameraOptions]); | ||
| const combinedStyle: CSSProperties = useMemo( | ||
@@ -101,8 +173,8 @@ () => ({ | ||
| height: '100%', | ||
| // when using deckgl, the map should be sent to the back | ||
| zIndex: isDeckGlControlled ? -1 : 0, | ||
| // when using deckgl, the map should be sent to the back | ||
| zIndex: isViewportSet ? -1 : 0, | ||
| ...style | ||
| }), | ||
| [style, isViewportSet] | ||
| [style, isDeckGlControlled] | ||
| ); | ||
@@ -136,106 +208,1 @@ | ||
| Map.deckGLViewProps = true; | ||
| const AuthFailureMessage = () => { | ||
| const style: CSSProperties = { | ||
| position: 'absolute', | ||
| top: 0, | ||
| left: 0, | ||
| bottom: 0, | ||
| right: 0, | ||
| zIndex: 999, | ||
| display: 'flex', | ||
| flexFlow: 'column nowrap', | ||
| textAlign: 'center', | ||
| justifyContent: 'center', | ||
| fontSize: '.8rem', | ||
| color: 'rgba(0,0,0,0.6)', | ||
| background: '#dddddd', | ||
| padding: '1rem 1.5rem' | ||
| }; | ||
| return ( | ||
| <div style={style}> | ||
| <h2>Error: AuthFailure</h2> | ||
| <p> | ||
| A problem with your API key prevents the map from rendering correctly. | ||
| Please make sure the value of the <code>APIProvider.apiKey</code> prop | ||
| is correct. Check the error-message in the console for further details. | ||
| </p> | ||
| </div> | ||
| ); | ||
| }; | ||
| /** | ||
| * The main hook takes care of creating map-instances and registering them in | ||
| * the api-provider context. | ||
| * @return a tuple of the map-instance created (or null) and the callback | ||
| * ref that will be used to pass the map-container into this hook. | ||
| * @internal | ||
| */ | ||
| function useMapInstance( | ||
| props: MapProps, | ||
| context: APIProviderContextValue | ||
| ): readonly [map: google.maps.Map | null, containerRef: Ref<HTMLDivElement>] { | ||
| const apiIsLoaded = useApiIsLoaded(); | ||
| const [map, setMap] = useState<google.maps.Map | null>(null); | ||
| const [container, containerRef] = useCallbackRef<HTMLDivElement>(); | ||
| const { | ||
| id, | ||
| initialBounds, | ||
| ...mapOptions | ||
| } = props; | ||
| // create the map instance and register it in the context | ||
| useEffect( | ||
| () => { | ||
| if (!container || !apiIsLoaded) return; | ||
| const {addMapInstance, removeMapInstance} = context; | ||
| const newMap = new google.maps.Map(container, mapOptions); | ||
| setMap(newMap); | ||
| addMapInstance(newMap, id); | ||
| if (initialBounds) { | ||
| newMap.fitBounds(initialBounds); | ||
| } | ||
| return () => { | ||
| if (!container || !apiIsLoaded) return; | ||
| // remove all event-listeners to minimize memory-leaks | ||
| google.maps.event.clearInstanceListeners(newMap); | ||
| setMap(null); | ||
| removeMapInstance(id); | ||
| }; | ||
| }, | ||
| // FIXME: we should rethink if it could be possible to keep the state | ||
| // around when a map gets re-initialized (id or mapId changed). This | ||
| // should keep the viewport as it is (so also no initial viewport in | ||
| // this case) and any added features should of course get re-added as | ||
| // well. | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| [id, container, apiIsLoaded, props.mapId] | ||
| ); | ||
| // report an error if the same map-id is used multiple times | ||
| useEffect(() => { | ||
| if (!id) return; | ||
| const {mapInstances} = context; | ||
| if (mapInstances[id] && mapInstances[id] !== map) { | ||
| logErrorOnce( | ||
| `The map id '${id}' seems to have been used multiple times. ` + | ||
| 'This can lead to unexpected problems when accessing the maps. ' + | ||
| 'Please use unique ids for all <Map> components.' | ||
| ); | ||
| } | ||
| }, [id, context, map]); | ||
| return [map, containerRef] as const; | ||
| } |
| import {useLayoutEffect} from 'react'; | ||
| export type DeckGlCompatProps = { | ||
| /** | ||
| * Viewport from deck.gl | ||
| */ | ||
| viewport?: unknown; | ||
| /** | ||
| * View state from deck.gl | ||
| */ | ||
| viewState?: Record<string, unknown>; | ||
| /** | ||
| * Initial View State from deck.gl | ||
| */ | ||
| initialViewState?: Record<string, unknown>; | ||
| }; | ||
| /** | ||
@@ -9,18 +24,10 @@ * Internal hook that updates the camera when deck.gl viewState changes. | ||
| map: google.maps.Map | null, | ||
| viewState: Record<string, unknown> | undefined | ||
| props: DeckGlCompatProps | ||
| ) { | ||
| const {viewport, viewState} = props; | ||
| const isDeckGlControlled = !!viewport; | ||
| useLayoutEffect(() => { | ||
| if (!map || !viewState) { | ||
| return; | ||
| } | ||
| if (!map || !viewState) return; | ||
| // FIXME: this should probably be extracted into a seperate hook that only | ||
| // runs once when first seeing a deck.gl viewState update and resets | ||
| // again. Maybe even use a seperate prop (`<Map controlled />`) instead. | ||
| map.setOptions({ | ||
| gestureHandling: 'none', | ||
| keyboardShortcuts: false, | ||
| disableDefaultUI: true | ||
| }); | ||
| const { | ||
@@ -41,2 +48,4 @@ latitude, | ||
| }, [map, viewState]); | ||
| return isDeckGlControlled; | ||
| } |
| import {useEffect} from 'react'; | ||
| import { | ||
| InternalCameraStateRef, | ||
| trackDispatchedEvent | ||
| } from './use-internal-camera-state'; | ||
@@ -18,2 +14,3 @@ /** | ||
| onProjectionChanged: (event: MapCameraChangedEvent) => void; | ||
| onCameraChanged: (event: MapCameraChangedEvent) => void; | ||
@@ -48,3 +45,2 @@ // mouse / touch / pointer events | ||
| map: google.maps.Map | null, | ||
| cameraStateRef: InternalCameraStateRef, | ||
| props: MapEventProps | ||
@@ -72,6 +68,3 @@ ) { | ||
| (ev?: google.maps.MapMouseEvent | google.maps.IconMouseEvent) => { | ||
| const mapEvent = createMapEvent(eventType, map, ev); | ||
| trackDispatchedEvent(mapEvent, cameraStateRef); | ||
| handler(mapEvent); | ||
| handler(createMapEvent(eventType, map, ev)); | ||
| } | ||
@@ -81,3 +74,3 @@ ); | ||
| return () => listener.remove(); | ||
| }, [map, cameraStateRef, eventType, handler]); | ||
| }, [map, eventType, handler]); | ||
| } | ||
@@ -124,3 +117,3 @@ } | ||
| center: center?.toJSON() || {lat: 0, lng: 0}, | ||
| zoom: zoom as number, | ||
| zoom: (zoom as number) || 0, | ||
| heading: heading as number, | ||
@@ -182,3 +175,8 @@ tilt: tilt as number, | ||
| onTiltChanged: 'tilt_changed', | ||
| onZoomChanged: 'zoom_changed' | ||
| onZoomChanged: 'zoom_changed', | ||
| // note: onCameraChanged is an alias for the bounds_changed event, | ||
| // since that is going to be fired in every situation where the camera is | ||
| // updated. | ||
| onCameraChanged: 'bounds_changed' | ||
| } as const; | ||
@@ -185,0 +183,0 @@ |
@@ -1,31 +0,48 @@ | ||
| import {useEffect, useLayoutEffect} from 'react'; | ||
| import {MapProps} from '@vis.gl/react-google-maps'; | ||
| import {InternalCameraStateRef} from './use-internal-camera-state'; | ||
| import {isLatLngLiteral} from '../../libraries/lat-lng-utils'; | ||
| import {MapProps} from '../map'; | ||
| import {useDeepCompareEffect} from '../../libraries/use-deep-compare-effect'; | ||
| const mapOptionKeys = new Set([ | ||
| 'backgroundColor', | ||
| 'clickableIcons', | ||
| 'controlSize', | ||
| 'disableDefaultUI', | ||
| 'disableDoubleClickZoom', | ||
| 'draggable', | ||
| 'draggableCursor', | ||
| 'draggingCursor', | ||
| 'fullscreenControl', | ||
| 'fullscreenControlOptions', | ||
| 'gestureHandling', | ||
| 'isFractionalZoomEnabled', | ||
| 'keyboardShortcuts', | ||
| 'mapTypeControl', | ||
| 'mapTypeControlOptions', | ||
| 'mapTypeId', | ||
| 'maxZoom', | ||
| 'minZoom', | ||
| 'noClear', | ||
| 'panControl', | ||
| 'panControlOptions', | ||
| 'restriction', | ||
| 'rotateControl', | ||
| 'rotateControlOptions', | ||
| 'scaleControl', | ||
| 'scaleControlOptions', | ||
| 'scrollwheel', | ||
| 'streetView', | ||
| 'streetViewControl', | ||
| 'streetViewControlOptions', | ||
| 'styles', | ||
| 'zoomControl', | ||
| 'zoomControlOptions' | ||
| ]); | ||
| /** | ||
| * Internal hook to update the map-options and camera parameters when | ||
| * props are changed. | ||
| * Internal hook to update the map-options when props are changed. | ||
| * | ||
| * @param map the map instance | ||
| * @param cameraStateRef stores the last values seen during dispatch into the | ||
| * react-application in useMapEvents(). We can safely assume that we | ||
| * don't need to feed these values back into the map. | ||
| * @param mapProps the props to update the map-instance with | ||
| * @internal | ||
| */ | ||
| export function useMapOptions( | ||
| map: google.maps.Map | null, | ||
| cameraStateRef: InternalCameraStateRef, | ||
| mapProps: MapProps | ||
| ) { | ||
| const {center: rawCenter, zoom, heading, tilt, ...mapOptions} = mapProps; | ||
| const center = rawCenter | ||
| ? isLatLngLiteral(rawCenter) | ||
| ? rawCenter | ||
| : rawCenter.toJSON() | ||
| : null; | ||
| const lat = center && center.lat; | ||
| const lng = center && center.lng; | ||
| export function useMapOptions(map: google.maps.Map | null, mapProps: MapProps) { | ||
| /* eslint-disable react-hooks/exhaustive-deps -- | ||
@@ -35,52 +52,23 @@ * | ||
| * In that case, the values will be or have been passed to the map | ||
| * constructor as mapOptions. | ||
| * constructor via mapOptions. | ||
| */ | ||
| const mapOptions: google.maps.MapOptions = {}; | ||
| const keys = Object.keys(mapProps) as (keyof google.maps.MapOptions)[]; | ||
| for (const key of keys) { | ||
| if (!mapOptionKeys.has(key)) continue; | ||
| mapOptions[key] = mapProps[key] as never; | ||
| } | ||
| // update the map options when mapOptions is changed | ||
| // Note: due to the destructuring above, mapOptions will be seen as changed | ||
| // with every re-render, so we're boldly assuming the maps-api will properly | ||
| // with every re-render, so we're assuming the maps-api will properly | ||
| // deal with unchanged option-values passed into setOptions. | ||
| useEffect(() => { | ||
| useDeepCompareEffect(() => { | ||
| if (!map) return; | ||
| // Changing the mapId via setOptions will trigger an error-message. | ||
| // We will re-create the map-instance in that case anyway, so we | ||
| // remove it here to avoid this error-message. | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| const {mapId, ...opts} = mapOptions; | ||
| map.setOptions(opts); | ||
| map.setOptions(mapOptions); | ||
| }, [mapOptions]); | ||
| useLayoutEffect(() => { | ||
| if (!map || !Number.isFinite(lat) || !Number.isFinite(lng)) return; | ||
| if ( | ||
| cameraStateRef.current.center.lat === lat && | ||
| cameraStateRef.current.center.lng === lng | ||
| ) | ||
| return; | ||
| map.moveCamera({center: {lat: lat as number, lng: lng as number}}); | ||
| }, [lat, lng]); | ||
| useLayoutEffect(() => { | ||
| if (!map || !Number.isFinite(zoom)) return; | ||
| if (cameraStateRef.current.zoom === zoom) return; | ||
| map.moveCamera({zoom: zoom as number}); | ||
| }, [zoom]); | ||
| useLayoutEffect(() => { | ||
| if (!map || !Number.isFinite(heading)) return; | ||
| if (cameraStateRef.current.heading === heading) return; | ||
| map.moveCamera({heading: heading as number}); | ||
| }, [heading]); | ||
| useLayoutEffect(() => { | ||
| if (!map || !Number.isFinite(tilt)) return; | ||
| if (cameraStateRef.current.tilt === tilt) return; | ||
| map.moveCamera({tilt: tilt as number}); | ||
| }, [tilt]); | ||
| /* eslint-enable react-hooks/exhaustive-deps */ | ||
| } |
+0
-1
@@ -12,3 +12,2 @@ export * from './components/advanced-marker'; | ||
| export * from './hooks/use-map'; | ||
| export * from './hooks/autocomplete'; | ||
| export * from './hooks/directions-service'; | ||
@@ -15,0 +14,0 @@ export * from './hooks/street-view-panorama'; |
@@ -53,3 +53,2 @@ import {APILoadingStatus} from './api-loading-status'; | ||
| // changed in between calls. | ||
| if (!window.google?.maps?.importLibrary) { | ||
@@ -56,0 +55,0 @@ this.serializedApiParams = serializedParams; |
@@ -24,6 +24,5 @@ export function isLatLngLiteral( | ||
| ): google.maps.LatLngLiteral { | ||
| return { | ||
| lat: typeof obj.lat === 'function' ? obj.lat() : obj.lat, | ||
| lng: typeof obj.lng === 'function' ? obj.lng() : obj.lng | ||
| }; | ||
| if (isLatLngLiteral(obj)) return obj; | ||
| return obj.toJSON(); | ||
| } |
| /// <reference types="google.maps" /> | ||
| import { MutableRefObject } from 'react'; | ||
| import { MapEvent } from './use-map-events'; | ||
| export type InternalCameraState = { | ||
| center: google.maps.LatLngLiteral; | ||
| heading: number; | ||
| tilt: number; | ||
| zoom: number; | ||
| }; | ||
| export type InternalCameraStateRef = MutableRefObject<InternalCameraState>; | ||
| /** | ||
| * Creates a mutable ref object to track the last known state of the map camera. | ||
| * This is updated by `trackDispatchedEvent` and used in `useMapOptions`. | ||
| */ | ||
| export declare function useInternalCameraState(): InternalCameraStateRef; | ||
| /** | ||
| * Records camera data from the last event dispatched to the React application | ||
| * in a mutable `IternalCameraStateRef`. | ||
| * This data can then be used to prevent feeding these values back to the | ||
| * map-instance when a typical "controlled component" setup (state variable is | ||
| * fed into and updated by the map). | ||
| */ | ||
| export declare function trackDispatchedEvent(ev: MapEvent, cameraStateRef: InternalCameraStateRef): void; |
| /// <reference types="google.maps" /> | ||
| export interface AutocompleteProps { | ||
| inputField: HTMLInputElement | null; | ||
| options?: google.maps.places.AutocompleteOptions; | ||
| onPlaceChanged: (place: google.maps.places.PlaceResult) => void; | ||
| } | ||
| /** | ||
| * Hook to get a Google Maps Places Autocomplete instance | ||
| * monitoring an input field | ||
| */ | ||
| export declare const useAutocomplete: (props: AutocompleteProps) => google.maps.places.Autocomplete | null; |
| import {MutableRefObject, useRef} from 'react'; | ||
| import {MapCameraChangedEvent, MapEvent} from './use-map-events'; | ||
| export type InternalCameraState = { | ||
| center: google.maps.LatLngLiteral; | ||
| heading: number; | ||
| tilt: number; | ||
| zoom: number; | ||
| }; | ||
| export type InternalCameraStateRef = MutableRefObject<InternalCameraState>; | ||
| /** | ||
| * Creates a mutable ref object to track the last known state of the map camera. | ||
| * This is updated by `trackDispatchedEvent` and used in `useMapOptions`. | ||
| */ | ||
| export function useInternalCameraState(): InternalCameraStateRef { | ||
| return useRef<InternalCameraState>({ | ||
| center: {lat: 0, lng: 0}, | ||
| heading: 0, | ||
| tilt: 0, | ||
| zoom: 0 | ||
| }); | ||
| } | ||
| /** | ||
| * Records camera data from the last event dispatched to the React application | ||
| * in a mutable `IternalCameraStateRef`. | ||
| * This data can then be used to prevent feeding these values back to the | ||
| * map-instance when a typical "controlled component" setup (state variable is | ||
| * fed into and updated by the map). | ||
| */ | ||
| export function trackDispatchedEvent( | ||
| ev: MapEvent, | ||
| cameraStateRef: InternalCameraStateRef | ||
| ) { | ||
| const cameraEvent = ev as MapCameraChangedEvent; | ||
| // we're only interested in the camera-events here | ||
| if (!cameraEvent.detail.center) return; | ||
| const {center, zoom, heading, tilt} = cameraEvent.detail; | ||
| cameraStateRef.current.center = center; | ||
| cameraStateRef.current.heading = heading; | ||
| cameraStateRef.current.tilt = tilt; | ||
| cameraStateRef.current.zoom = zoom; | ||
| } |
Sorry, the diff of this file is not supported yet
| import React from 'react'; | ||
| import {renderHook, waitFor} from '@testing-library/react'; | ||
| import {Autocomplete, initialize} from '@googlemaps/jest-mocks'; | ||
| import {APIProvider} from '../../components/api-provider'; | ||
| import {useAutocomplete} from '../autocomplete'; | ||
| import {waitForMockInstance} from './__utils__/wait-for-mock-instance'; | ||
| jest.mock('../../libraries/google-maps-api-loader'); | ||
| let wrapper: ({children}: {children: React.ReactNode}) => JSX.Element | null; | ||
| let onPlaceChanged: jest.Mock; | ||
| let inputField: HTMLInputElement; | ||
| beforeEach(() => { | ||
| initialize(); | ||
| onPlaceChanged = jest.fn(); | ||
| inputField = document.createElement('input'); | ||
| wrapper = ({children}: {children: React.ReactNode}) => ( | ||
| <APIProvider apiKey={'apikey'}>{children}</APIProvider> | ||
| ); | ||
| }); | ||
| test('it should initialize an autocomplete instance', async () => { | ||
| const {result} = renderHook( | ||
| () => useAutocomplete({inputField, onPlaceChanged}), | ||
| {wrapper} | ||
| ); | ||
| const service = await waitForMockInstance(Autocomplete); | ||
| expect(result.current).toBe(service); | ||
| expect(service).toBeInstanceOf(google.maps.places.Autocomplete); | ||
| }); | ||
| test('it throws an error if the places library is missing', async () => { | ||
| // pretend the places library wasn't loaded | ||
| // @ts-expect-error - testing error case | ||
| delete google.maps.places; | ||
| const consoleErrorSpy = jest | ||
| .spyOn(console, 'error') | ||
| .mockImplementation(() => {}); | ||
| const {result} = renderHook( | ||
| () => useAutocomplete({inputField, onPlaceChanged}), | ||
| {wrapper} | ||
| ); | ||
| await waitFor(() => expect(consoleErrorSpy).toHaveBeenCalled()); | ||
| expect(result.current).toBe(null); | ||
| expect(consoleErrorSpy.mock.lastCall).toMatchSnapshot(); | ||
| }); | ||
| test('it adds place_changed listener to autocomplete', async () => { | ||
| const {result} = renderHook( | ||
| () => useAutocomplete({inputField, onPlaceChanged}), | ||
| {wrapper} | ||
| ); | ||
| const service = await waitForMockInstance(Autocomplete); | ||
| expect(result.current).toBe(service); | ||
| expect(service.addListener).toHaveBeenCalled(); | ||
| }); |
| import {useState, useRef, useEffect} from 'react'; | ||
| import {useApiIsLoaded} from './use-api-is-loaded'; | ||
| export interface AutocompleteProps { | ||
| inputField: HTMLInputElement | null; | ||
| options?: google.maps.places.AutocompleteOptions; | ||
| onPlaceChanged: (place: google.maps.places.PlaceResult) => void; | ||
| } | ||
| /** | ||
| * Hook to get a Google Maps Places Autocomplete instance | ||
| * monitoring an input field | ||
| */ | ||
| export const useAutocomplete = ( | ||
| props: AutocompleteProps | ||
| ): google.maps.places.Autocomplete | null => { | ||
| const {inputField, options, onPlaceChanged} = props; | ||
| const googleMapsAPIIsLoaded = useApiIsLoaded(); | ||
| const placeChangedHandler = useRef(onPlaceChanged); | ||
| const [autocomplete, setAutocomplete] = | ||
| useState<google.maps.places.Autocomplete | null>(null); | ||
| // Initializes the Google Maps Places Autocomplete | ||
| useEffect(() => { | ||
| // Wait for the Google Maps API and input element to be initialized | ||
| if (!googleMapsAPIIsLoaded || !inputField) return; | ||
| // FIXME: add dynamic loading for required libraries | ||
| if (!google.maps.places) { | ||
| console.error( | ||
| 'Google Maps Places library is missing. ' + | ||
| 'Please add the places library to the props of the <ApiProvider> ' + | ||
| 'component.' | ||
| ); | ||
| return; | ||
| } | ||
| // Create Autocomplete instance | ||
| const autocompleteInstance = new google.maps.places.Autocomplete( | ||
| inputField, | ||
| options | ||
| ); | ||
| setAutocomplete(autocompleteInstance); | ||
| // Add places change listener to Autocomplete | ||
| autocompleteInstance.addListener('place_changed', () => { | ||
| const place = autocompleteInstance.getPlace(); | ||
| if (placeChangedHandler.current) placeChangedHandler.current(place); | ||
| }); | ||
| // Clear listeners on unmount | ||
| return () => { | ||
| if (autocompleteInstance && typeof google.maps === 'object') { | ||
| google.maps.event.clearInstanceListeners(autocompleteInstance); | ||
| } | ||
| }; | ||
| }, [googleMapsAPIIsLoaded, inputField, options]); | ||
| return autocomplete; | ||
| }; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
479391
4.44%89
7.23%6486
3.08%4
33.33%21
5%+ Added
+ Added