|
5 | 5 | /* global document */ |
6 | 6 | import {Widget} from '@deck.gl/core'; |
7 | 7 | import type {Deck, PickingInfo, WidgetProps} from '@deck.gl/core'; |
8 | | -import {render} from 'preact'; |
9 | | -import {SimpleMenu} from './lib/components/simple-menu'; |
10 | | - |
11 | | -/** The standard, modern way is to use event.button === 2, where button is the standardized property (0 = left, 1 = middle, 2 = right). */ |
12 | | -const MOUSE_BUTTON_RIGHT = 2; |
13 | | -/** A name for the legacy MouseEvent.which value that corresponds to the right-mouse button. In older browsers, the check is: if (event.which === 3) */ |
14 | | -const MOUSE_WHICH_RIGHT = 3; |
15 | | - |
16 | | -export type ContextWidgetMenuItem = { |
17 | | - label: string; |
18 | | - key: string; |
19 | | -}; |
| 8 | +import {render, type JSX} from 'preact'; |
| 9 | +import {SimpleMenu, SimpleMenuProps} from './lib/components/dropdown-menu'; |
| 10 | +import {Popover, type PopoverProps} from './lib/components/popover'; |
20 | 11 |
|
21 | 12 | export type ContextMenuWidgetProps = WidgetProps & { |
22 | 13 | /** View to attach to and interact with. Required when using multiple views. */ |
23 | 14 | viewId?: string | null; |
24 | | - /** Controls visibility of the context menu */ |
25 | | - visible?: boolean; |
26 | | - /** Screen position at which to place the menu */ |
27 | | - position: {x: number; y: number}; |
28 | | - /** Items to render */ |
29 | | - menuItems: ContextWidgetMenuItem[]; |
30 | 15 | /** Provide menu items for the menu given the picked object */ |
31 | | - getMenuItems: (info: PickingInfo, widget: ContextMenuWidget) => ContextWidgetMenuItem[] | null; |
| 16 | + getMenuItems: ( |
| 17 | + info: PickingInfo, |
| 18 | + widget: ContextMenuWidget |
| 19 | + ) => SimpleMenuProps['menuItems'] | null; |
32 | 20 | /** Callback with the selected item */ |
33 | | - onMenuItemSelected?: (key: string, pickInfo: PickingInfo | null) => void; |
| 21 | + onMenuItemSelected?: (value: string, pickInfo: PickingInfo | null) => void; |
| 22 | + /** Position menu relative to the anchor. |
| 23 | + * @default 'bottom-start' |
| 24 | + */ |
| 25 | + placement?: PopoverProps['placement']; |
| 26 | + /** Pixel offset |
| 27 | + * @default 10 |
| 28 | + */ |
| 29 | + offset?: PopoverProps['offset']; |
| 30 | + /** |
| 31 | + * Show an arrow pointing at the anchor. Optionally accepts a pixel size. |
| 32 | + * @default false |
| 33 | + */ |
| 34 | + arrow?: PopoverProps['arrow']; |
34 | 35 | }; |
35 | 36 |
|
36 | 37 | export class ContextMenuWidget extends Widget<ContextMenuWidgetProps> { |
37 | 38 | static defaultProps: Required<ContextMenuWidgetProps> = { |
38 | 39 | ...Widget.defaultProps, |
39 | 40 | id: 'context', |
40 | 41 | viewId: null, |
41 | | - visible: false, |
42 | | - position: {x: 0, y: 0}, |
43 | 42 | getMenuItems: undefined!, |
44 | | - menuItems: [], |
45 | | - // eslint-disable-next-line no-console |
46 | | - onMenuItemSelected: (key, pickInfo) => console.log('Context menu item selected:', key, pickInfo) |
| 43 | + onMenuItemSelected: () => {}, |
| 44 | + placement: 'bottom-start', |
| 45 | + offset: 10, |
| 46 | + arrow: false |
47 | 47 | }; |
48 | 48 |
|
49 | 49 | className = 'deck-widget-context-menu'; |
50 | 50 | placement = 'fill' as const; |
51 | 51 |
|
52 | | - pickInfo: PickingInfo | null = null; |
| 52 | + menu: { |
| 53 | + items: SimpleMenuProps['menuItems']; |
| 54 | + pickInfo: PickingInfo; |
| 55 | + } | null = null; |
53 | 56 |
|
54 | 57 | constructor(props: ContextMenuWidgetProps) { |
55 | 58 | super(props); |
56 | | - this.pickInfo = null; |
57 | 59 | this.setProps(this.props); |
58 | 60 | } |
59 | 61 |
|
60 | | - onAdd({deck}: {deck: Deck<any>}): HTMLDivElement { |
61 | | - const element = document.createElement('div'); |
62 | | - element.classList.add('deck-widget', 'deck-widget-context-menu'); |
63 | | - const style = { |
64 | | - margin: '0px', |
65 | | - top: '0px', |
66 | | - left: '0px', |
67 | | - position: 'absolute', |
68 | | - pointerEvents: 'auto' |
69 | | - }; |
70 | | - Object.entries(style).forEach(([key, value]) => element.style.setProperty(key, value)); |
71 | | - |
72 | | - deck.getCanvas()?.addEventListener('click', () => this.hide()); |
| 62 | + onAdd({deck}: {deck: Deck<any>}) { |
73 | 63 | deck.getCanvas()?.addEventListener('contextmenu', event => this.handleContextMenu(event)); |
74 | | - return element; |
75 | 64 | } |
76 | 65 |
|
77 | | - onRenderHTML(rootElement: HTMLElement): void { |
78 | | - const {visible, position, menuItems} = this.props; |
| 66 | + handleContextMenu(srcEvent: MouseEvent) { |
| 67 | + const targetRect = (srcEvent.target as HTMLElement).getBoundingClientRect(); |
| 68 | + const x = srcEvent.clientX - targetRect.x; |
| 69 | + const y = srcEvent.clientY - targetRect.y; |
79 | 70 |
|
80 | | - const ui = |
81 | | - visible && menuItems.length ? ( |
82 | | - <SimpleMenu |
83 | | - menuItems={menuItems} |
84 | | - onItemSelected={key => this.props.onMenuItemSelected(key, this.pickInfo)} |
85 | | - position={position} |
86 | | - style={{pointerEvents: 'auto'}} |
87 | | - /> |
88 | | - ) : null; |
89 | | - render(ui, rootElement); |
| 71 | + const pickInfo = this.deck?.pickObject({x, y}) || { |
| 72 | + x, |
| 73 | + y, |
| 74 | + picked: false, |
| 75 | + layer: null, |
| 76 | + color: null, |
| 77 | + index: -1, |
| 78 | + pixelRatio: 1 |
| 79 | + }; |
| 80 | + const menuItems = this.props.getMenuItems(pickInfo, this) || []; |
| 81 | + this.menu = |
| 82 | + menuItems.length > 0 |
| 83 | + ? { |
| 84 | + items: menuItems, |
| 85 | + pickInfo |
| 86 | + } |
| 87 | + : null; |
| 88 | + srcEvent.preventDefault(); |
| 89 | + this.updateHTML(); |
90 | 90 | } |
91 | 91 |
|
92 | | - handleContextMenu(srcEvent: MouseEvent): boolean { |
93 | | - if ( |
94 | | - srcEvent && |
95 | | - (srcEvent.button === MOUSE_BUTTON_RIGHT || srcEvent.which === MOUSE_WHICH_RIGHT) |
96 | | - ) { |
97 | | - this.pickInfo = |
98 | | - this.deck?.pickObject({ |
99 | | - x: srcEvent.clientX, |
100 | | - y: srcEvent.clientY |
101 | | - }) || null; |
102 | | - const menuItems = (this.pickInfo && this.props.getMenuItems?.(this.pickInfo, this)) || []; |
103 | | - const visible = menuItems.length > 0; |
104 | | - this.setProps({ |
105 | | - visible, |
106 | | - position: {x: srcEvent.clientX, y: srcEvent.clientY}, |
107 | | - menuItems |
108 | | - }); |
109 | | - this.updateHTML(); |
110 | | - srcEvent.preventDefault(); |
111 | | - return visible; |
| 92 | + onRenderHTML(rootElement: HTMLElement): void { |
| 93 | + if (!this.menu) { |
| 94 | + render(null, rootElement); |
| 95 | + return; |
112 | 96 | } |
| 97 | + const {items, pickInfo} = this.menu; |
113 | 98 |
|
114 | | - return false; |
| 99 | + const style = { |
| 100 | + pointerEvents: 'auto', |
| 101 | + position: 'static', |
| 102 | + ...this.props.style |
| 103 | + }; |
| 104 | + |
| 105 | + const ui = ( |
| 106 | + <Popover |
| 107 | + x={pickInfo.x} |
| 108 | + y={pickInfo.y} |
| 109 | + placement={this.props.placement} |
| 110 | + arrow={this.props.arrow} |
| 111 | + arrowColor="var(--menu-background, #fff)" |
| 112 | + offset={this.props.offset} |
| 113 | + > |
| 114 | + <SimpleMenu |
| 115 | + menuItems={items} |
| 116 | + onSelect={value => this.props.onMenuItemSelected(value, pickInfo)} |
| 117 | + style={style} |
| 118 | + isOpen |
| 119 | + onClose={() => this.hide()} |
| 120 | + /> |
| 121 | + </Popover> |
| 122 | + ); |
| 123 | + render(ui, rootElement); |
115 | 124 | } |
116 | 125 |
|
117 | | - hide(): void { |
118 | | - this.setProps({visible: false}); |
| 126 | + hide() { |
| 127 | + this.menu = null; |
| 128 | + this.updateHTML(); |
119 | 129 | } |
120 | 130 | } |
0 commit comments