// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors /* global document */ import {Widget} from '@deck.gl/core'; import type {Deck, PickingInfo, WidgetProps} from '@deck.gl/core'; import {render, type JSX} from 'preact'; import {SimpleMenu, type MenuItem} from './lib/components/dropdown-menu'; import {Popover, type PopoverProps} from './lib/components/popover'; export type ContextMenuWidgetProps = WidgetProps & { /** View to attach to and interact with. Required when using multiple views. */ viewId?: string | null; /** Menu items for the menu. */ menuItems?: MenuItem[]; /** Callback to provide menu items for the menu given the picked object. Overrides `menuItems` */ getMenuItems?: (info: PickingInfo, widget: ContextMenuWidget) => MenuItem[] | null; /** Callback when a menu item is selected */ onMenuItemSelected?: (value: string, pickInfo: PickingInfo | null) => void; /** Position menu relative to the anchor. * @default 'bottom-start' */ placement?: PopoverProps['placement']; /** Pixel offset * @default 10 */ offset?: PopoverProps['offset']; /** * Show an arrow pointing at the anchor. Optionally accepts a pixel size. * @default false */ arrow?: PopoverProps['arrow']; }; export class ContextMenuWidget extends Widget { static defaultProps: Required = { ...Widget.defaultProps, id: 'context', viewId: null, menuItems: [], getMenuItems: undefined!, onMenuItemSelected: () => {}, placement: 'bottom-start', offset: 10, arrow: false }; className = 'deck-widget-context-menu'; placement = 'fill' as const; menu: { items: MenuItem[]; pickInfo: PickingInfo; } | null = null; constructor(props: ContextMenuWidgetProps) { super(props); this.setProps(this.props); } onAdd({deck}: {deck: Deck}) { deck.getCanvas()?.addEventListener('contextmenu', event => this.handleContextMenu(event)); } handleContextMenu(srcEvent: MouseEvent) { const targetRect = (srcEvent.target as HTMLElement).getBoundingClientRect(); const x = srcEvent.clientX - targetRect.x; const y = srcEvent.clientY - targetRect.y; const pickInfo = this.deck?.pickObject({x, y}) || { x, y, picked: false, layer: null, color: null, index: -1, pixelRatio: 1 }; const menuItems = this.props.getMenuItems?.(pickInfo, this) || this.props.menuItems; this.menu = menuItems.length > 0 ? { items: menuItems, pickInfo } : null; srcEvent.preventDefault(); this.updateHTML(); } onRenderHTML(rootElement: HTMLElement): void { if (!this.menu) { render(null, rootElement); return; } const {items, pickInfo} = this.menu; const style = { pointerEvents: 'auto', position: 'static', ...this.props.style }; const ui = ( this.props.onMenuItemSelected(value, pickInfo)} style={style} isOpen onClose={() => this.hide()} /> ); render(ui, rootElement); } hide() { this.menu = null; this.updateHTML(); } }