// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import {Widget} from '@deck.gl/core'; import type {Deck, Viewport, WidgetProps} from '@deck.gl/core'; import {render, JSX} from 'preact'; import {Popover, type PopoverProps} from './lib/components/popover'; import {UserContent} from './lib/components/user-content'; import {IconButton} from './lib/components/icon-button'; export type PopupContent = { text?: string; html?: string; element?: HTMLElement | null; }; export type PopupWidgetProps = WidgetProps & { /** View to attach to and interact with. Required when using multiple views */ viewId?: string | null; /** Content to display at the anchor. Opens the popup if clicked. */ marker?: PopupContent | null; /** Content to display in the popup */ content: string | PopupContent; /** Whether the pop up is open by default * @default true */ defaultIsOpen?: boolean; /** Anchor of the popup in world coordinates, e.g. [longitude, latitude]. */ position: number[]; /** Position popup relative to the anchor. * @default 'right' */ placement?: PopoverProps['placement']; /** Pixel offset * @default 10 */ offset?: PopoverProps['offset']; /** * Show an arrow pointing at the anchor. Optionally accepts a pixel size. * @default 10 */ arrow?: PopoverProps['arrow']; /** Whether to show a close button in the popup * @default true */ closeButton?: boolean; /** Close the popup if clicked outside * @default false */ closeOnClickOutside?: boolean; /** Callback when popup is opened/closed */ onOpenChange?: (isOpen: boolean) => void; }; export class PopupWidget extends Widget { static defaultProps: Required = { ...Widget.defaultProps, id: 'info', viewId: null, position: [0, 0], marker: null, defaultIsOpen: true, content: '', placement: 'right', offset: 10, arrow: 10, closeButton: true, closeOnClickOutside: false, onOpenChange: () => {} }; className = 'deck-widget-popup'; placement = 'fill' as const; viewport?: Viewport; isOpen: boolean; constructor(props: PopupWidgetProps) { super(props); this.setProps(this.props); this.isOpen = this.props.defaultIsOpen; } setProps(props: Partial) { this.viewId = props.viewId ?? this.viewId; super.setProps(props); } onAdd({deck}: {deck: Deck; viewId: string | null}) { this.deck = deck; } onRemove() { // Invoke clean up of preact hooks if (this.rootElement) { render(null, this.rootElement); } } onViewportChange(viewport) { this.viewport = viewport; this.updateHTML(); } onClick() { if (this.props.closeOnClickOutside) { this._setIsOpen(false); } } protected _setIsOpen(isOpen: boolean) { if (this.isOpen === isOpen) return; this.isOpen = isOpen; this.props.onOpenChange?.(isOpen); this.updateHTML(); } onRenderHTML(rootElement: HTMLElement): void { if (!this.viewport) { render(null, rootElement); return; } const {marker, content, style} = this.props; // Project the clicked geographic coordinate to canvas (x, y) const [x, y] = this.viewport.project(this.props.position); // Render the popup container with a content box and a placeholder for the arrow. // The container is positioned absolutely (initially at 0,0) and will be repositioned after measuring. const ui = (
{marker && (
this._setIsOpen(true)} />
)} {this.isOpen && (
{this.props.closeButton && (
this._setIsOpen(false)} />
)}
)}
); render(ui, rootElement); } }