/* eslint-disable complexity */ import React, { Children, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'; import {createPortal} from 'react-dom'; import {useMap} from '../hooks/use-map'; import {useMapsLibrary} from '../hooks/use-maps-library'; import type {Ref, PropsWithChildren} from 'react'; export interface AdvancedMarkerContextValue { marker: google.maps.marker.AdvancedMarkerElement; } export const AdvancedMarkerContext = React.createContext(null); type AdvancedMarkerEventProps = { onClick?: (e: google.maps.MapMouseEvent) => void; onDrag?: (e: google.maps.MapMouseEvent) => void; onDragStart?: (e: google.maps.MapMouseEvent) => void; onDragEnd?: (e: google.maps.MapMouseEvent) => void; }; export type AdvancedMarkerProps = PropsWithChildren< Omit< google.maps.marker.AdvancedMarkerElementOptions, 'gmpDraggable' | 'map' > & AdvancedMarkerEventProps & { /** * className to add a class to the advanced marker element * Can only be used with HTML Marker content */ className?: string; draggable?: boolean; } >; export type AdvancedMarkerRef = google.maps.marker.AdvancedMarkerElement | null; function useAdvancedMarker(props: AdvancedMarkerProps) { const [marker, setMarker] = useState(null); const [contentContainer, setContentContainer] = useState(null); const map = useMap(); const markerLibrary = useMapsLibrary('marker'); const { children, className, onClick, onDrag, onDragStart, onDragEnd, collisionBehavior, draggable, position, title, zIndex } = props; const numChilds = Children.count(children); // create marker instance and add it to the map when map becomes available useEffect(() => { if (!map || !markerLibrary) return; const newMarker = new markerLibrary.AdvancedMarkerElement(); newMarker.map = map; setMarker(newMarker); // create container for marker content if there are children if (numChilds > 0) { const el = document.createElement('div'); if (className) el.className = className; newMarker.content = el; setContentContainer(el); } return () => { newMarker.map = null; setMarker(null); setContentContainer(null); }; // We do not want to re-render the whole marker when the className changes // because that causes a short flickering of the marker. // The className update is handled in the useEffect below. // Excluding the className from the dependency array onm purpose here // eslint-disable-next-line react-hooks/exhaustive-deps }, [map, markerLibrary, numChilds]); // update className of advanced marker element useEffect(() => { if (!contentContainer) return; contentContainer.className = className ?? ''; }, [contentContainer, className]); // bind all marker events useEffect(() => { if (!marker) return; const gme = google.maps.event; if (onClick) gme.addListener(marker, 'click', onClick); if (onDrag) gme.addListener(marker, 'drag', onDrag); if (onDragStart) gme.addListener(marker, 'dragstart', onDragStart); if (onDragEnd) gme.addListener(marker, 'dragend', onDragEnd); if ((onDrag || onDragStart || onDragEnd) && !draggable) { console.warn( 'You need to set the marker to draggable to listen to drag-events.' ); } const m = marker; return () => { gme.clearInstanceListeners(m); }; }, [marker, draggable, onClick, onDragStart, onDrag, onDragEnd]); // update other marker props when changed useEffect(() => { if (!marker) return; if (position !== undefined) marker.position = position; if (draggable !== undefined) marker.gmpDraggable = draggable; if (collisionBehavior !== undefined) marker.collisionBehavior = collisionBehavior; if (zIndex !== undefined) marker.zIndex = zIndex; if (typeof title === 'string') marker.title = title; }, [marker, position, draggable, collisionBehavior, zIndex, title]); return [marker, contentContainer] as const; } export const AdvancedMarker = forwardRef( (props: AdvancedMarkerProps, ref: Ref) => { const {children} = props; const [marker, contentContainer] = useAdvancedMarker(props); const advancedMarkerContextValue: AdvancedMarkerContextValue | null = useMemo(() => (marker ? {marker} : null), [marker]); useImperativeHandle(ref, () => marker, [marker]); if (!marker) { return null; } return ( {contentContainer !== null && createPortal(children, contentContainer)} ); } ); export function useAdvancedMarkerRef() { const [marker, setMarker] = useState(null); const refCallback = useCallback((m: AdvancedMarkerRef | null) => { setMarker(m); }, []); return [refCallback, marker] as const; }