UNPKG

@vis.gl/react-google-maps

Version:

React components and hooks for the Google Maps JavaScript API

220 lines (191 loc) 6.49 kB
import {useEffect} from 'react'; /** * Handlers for all events that could be emitted by map-instances. */ export type MapEventProps = Partial<{ // map view state events onBoundsChanged: (event: MapCameraChangedEvent) => void; onCenterChanged: (event: MapCameraChangedEvent) => void; onHeadingChanged: (event: MapCameraChangedEvent) => void; onTiltChanged: (event: MapCameraChangedEvent) => void; onZoomChanged: (event: MapCameraChangedEvent) => void; onCameraChanged: (event: MapCameraChangedEvent) => void; // mouse / touch / pointer events onClick: (event: MapMouseEvent) => void; onDblclick: (event: MapMouseEvent) => void; onContextmenu: (event: MapMouseEvent) => void; onMousemove: (event: MapMouseEvent) => void; onMouseover: (event: MapMouseEvent) => void; onMouseout: (event: MapMouseEvent) => void; onDrag: (event: MapEvent) => void; onDragend: (event: MapEvent) => void; onDragstart: (event: MapEvent) => void; // loading events onTilesLoaded: (event: MapEvent) => void; onIdle: (event: MapEvent) => void; // configuration events onProjectionChanged: (event: MapEvent) => void; onIsFractionalZoomEnabledChanged: (event: MapEvent) => void; onMapCapabilitiesChanged: (event: MapEvent) => void; onMapTypeIdChanged: (event: MapEvent) => void; onRenderingTypeChanged: (event: MapEvent) => void; }>; /** * Sets up effects to bind event-handlers for all event-props in MapEventProps. * @internal */ export function useMapEvents( map: google.maps.Map | null, props: MapEventProps ) { // note: calling a useEffect hook from within a loop is prohibited by the // rules of hooks, but it's ok here since it's unconditional and the number // and order of iterations is always strictly the same. // (see https://legacy.reactjs.org/docs/hooks-rules.html) for (const propName of eventPropNames) { // fixme: this cast is essentially a 'trust me, bro' for typescript, but // a proper solution seems way too complicated right now const handler = props[propName] as (ev: MapEvent) => void; const eventType = propNameToEventType[propName]; // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { if (!map) return; if (!handler) return; const listener = google.maps.event.addListener( map, eventType, (ev?: google.maps.MapMouseEvent | google.maps.IconMouseEvent) => { handler(createMapEvent(eventType, map, ev)); } ); return () => listener.remove(); }, [map, eventType, handler]); } } /** * Create the wrapped map-events used for the event-props. * @param type the event type as it is specified to the maps api * @param map the map instance the event originates from * @param srcEvent the source-event if there is one. */ function createMapEvent( type: string, map: google.maps.Map, srcEvent?: google.maps.MapMouseEvent | google.maps.IconMouseEvent ): MapEvent { const ev: MapEvent = { type, map, detail: {}, stoppable: false, stop: () => {} }; if (cameraEventTypes.includes(type)) { const camEvent = ev as MapCameraChangedEvent; 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( '[createEvent] 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' ); } camEvent.detail = { center: center?.toJSON() || {lat: 0, lng: 0}, zoom: (zoom as number) || 0, heading: heading as number, tilt: tilt as number, bounds: bounds?.toJSON() || { north: 90, east: 180, south: -90, west: -180 } }; return camEvent; } else if (mouseEventTypes.includes(type)) { if (!srcEvent) throw new Error('[createEvent] mouse events must provide a srcEvent'); const mouseEvent = ev as MapMouseEvent; mouseEvent.domEvent = srcEvent.domEvent; mouseEvent.stoppable = true; mouseEvent.stop = () => srcEvent.stop(); mouseEvent.detail = { latLng: srcEvent.latLng?.toJSON() || null, placeId: (srcEvent as google.maps.IconMouseEvent).placeId }; return mouseEvent; } return ev; } /** * maps the camelCased names of event-props to the corresponding event-types * used in the maps API. */ const propNameToEventType: {[prop in keyof Required<MapEventProps>]: string} = { onBoundsChanged: 'bounds_changed', onCenterChanged: 'center_changed', onClick: 'click', onContextmenu: 'contextmenu', onDblclick: 'dblclick', onDrag: 'drag', onDragend: 'dragend', onDragstart: 'dragstart', onHeadingChanged: 'heading_changed', onIdle: 'idle', onIsFractionalZoomEnabledChanged: 'isfractionalzoomenabled_changed', onMapCapabilitiesChanged: 'mapcapabilities_changed', onMapTypeIdChanged: 'maptypeid_changed', onMousemove: 'mousemove', onMouseout: 'mouseout', onMouseover: 'mouseover', onProjectionChanged: 'projection_changed', onRenderingTypeChanged: 'renderingtype_changed', onTilesLoaded: 'tilesloaded', onTiltChanged: 'tilt_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; const cameraEventTypes = [ 'bounds_changed', 'center_changed', 'heading_changed', 'tilt_changed', 'zoom_changed' ]; const mouseEventTypes = [ 'click', 'contextmenu', 'dblclick', 'mousemove', 'mouseout', 'mouseover' ]; type MapEventPropName = keyof MapEventProps; const eventPropNames = Object.keys(propNameToEventType) as MapEventPropName[]; export type MapEvent<T = unknown> = { type: string; map: google.maps.Map; detail: T; stoppable: boolean; stop: () => void; domEvent?: MouseEvent | TouchEvent | PointerEvent | KeyboardEvent | Event; }; export type MapMouseEvent = MapEvent<{ latLng: google.maps.LatLngLiteral | null; placeId: string | null; }>; export type MapCameraChangedEvent = MapEvent<{ center: google.maps.LatLngLiteral; bounds: google.maps.LatLngBoundsLiteral; zoom: number; heading: number; tilt: number; }>;