MIF_E31221222/sigap-website/app/_components/map/overlay.tsx

156 lines
4.4 KiB
TypeScript

import { IControl, Map } from "mapbox-gl";
import { ControlPosition } from "mapbox-gl";
import { cloneElement, memo, ReactElement, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { useControl } from "react-map-gl/mapbox";
import { v4 as uuidv4 } from 'uuid';
// Updated props type to include addControl in children props
type OverlayProps = {
position: ControlPosition;
children: ReactElement<{
map?: Map;
addControl?: (control: IControl, position?: ControlPosition) => void;
}>;
id?: string;
className?: string;
style?: React.CSSProperties;
};
// Custom control for overlay
class OverlayControl implements IControl {
_map: Map | null = null;
_container: HTMLElement | null = null;
_position: ControlPosition;
_id: string;
_redraw?: () => void;
_className?: string;
_style?: React.CSSProperties;
constructor({
position,
id,
redraw,
className,
style,
}: {
position: ControlPosition;
id: string;
redraw?: () => void;
className?: string;
style?: React.CSSProperties;
}) {
this._position = position;
this._id = id;
this._redraw = redraw;
this._className = className;
this._style = style;
}
onAdd(map: Map) {
this._map = map;
this._container = document.createElement('div');
// Apply base classes but keep it minimal to avoid layout conflicts
this._container.className = `mapboxgl-ctrl ${this._className || ''}`;
this._container.id = this._id;
// Important: These styles make the overlay adapt to content
this._container.style.pointerEvents = 'auto';
this._container.style.display = 'inline-block'; // Allow container to size to content
this._container.style.maxWidth = 'none'; // Remove any max-width constraints
this._container.style.width = 'auto'; // Let width be determined by content
this._container.style.height = 'auto'; // Let height be determined by content
this._container.style.overflow = 'visible'; // Allow content to overflow if needed
// Apply any custom styles passed as props
if (this._style) {
Object.entries(this._style).forEach(([key, value]) => {
// @ts-ignore - dynamically setting style properties
this._container.style[key] = value;
});
}
if (this._redraw) {
map.on('move', this._redraw);
this._redraw();
}
return this._container;
}
onRemove() {
if (!this._map || !this._container) return;
if (this._redraw) {
this._map.off('move', this._redraw);
}
this._container.remove();
this._map = null;
}
getDefaultPosition() {
return this._position;
}
getMap() {
return this._map;
}
getElement() {
return this._container;
}
// Method to add other controls to the map
addControl(control: IControl, position?: ControlPosition) {
if (this._map) {
this._map.addControl(control, position);
}
return this;
}
}
// Enhanced Overlay component
function _Overlay({ position, children, id = `overlay-${uuidv4()}`, className, style }: OverlayProps) {
const [container, setContainer] = useState<HTMLElement | null>(null);
const [map, setMap] = useState<Map | null>(null);
// Use useControl with unique ID to avoid conflicts
const ctrl = useControl<OverlayControl>(
() =>
new OverlayControl({
position,
id,
className,
style,
}),
{ position }
);
// Update container and map instance when control is ready
useEffect(() => {
if (ctrl) {
setContainer(ctrl.getElement());
setMap(ctrl.getMap());
}
}, [ctrl]);
// Only render if container is ready
if (!container || !map) return null;
// Use createPortal to render children to container and pass addControl method
// return createPortal(
// cloneElement(children, { map, addControl: ctrl.addControl.bind(ctrl) }),
// container
// );
return createPortal(
cloneElement(children, { map }),
container
)
}
// Export as memoized component
export const Overlay = memo(_Overlay);