@deck.gl/widgets 9.2.11 → 9.3.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compass-widget.js +5 -5
- package/dist/compass-widget.js.map +1 -1
- package/dist/context-menu-widget.d.ts +27 -20
- package/dist/context-menu-widget.d.ts.map +1 -1
- package/dist/context-menu-widget.js +45 -48
- package/dist/context-menu-widget.js.map +1 -1
- package/dist/dist.dev.js +3114 -869
- package/dist/fullscreen-widget.js +1 -1
- package/dist/fullscreen-widget.js.map +1 -1
- package/dist/geocoder-widget.d.ts +2 -4
- package/dist/geocoder-widget.d.ts.map +1 -1
- package/dist/geocoder-widget.js +16 -20
- package/dist/geocoder-widget.js.map +1 -1
- package/dist/gimbal-widget.d.ts +0 -3
- package/dist/gimbal-widget.d.ts.map +1 -1
- package/dist/gimbal-widget.js +4 -22
- package/dist/gimbal-widget.js.map +1 -1
- package/dist/icon-widget.d.ts +28 -0
- package/dist/icon-widget.d.ts.map +1 -0
- package/dist/icon-widget.js +35 -0
- package/dist/icon-widget.js.map +1 -0
- package/dist/index.cjs +1271 -561
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +16 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -10
- package/dist/index.js.map +1 -1
- package/dist/info-widget.d.ts +39 -17
- package/dist/info-widget.d.ts.map +1 -1
- package/dist/info-widget.js +46 -130
- package/dist/info-widget.js.map +1 -1
- package/dist/lib/components/dropdown-menu.d.ts +12 -5
- package/dist/lib/components/dropdown-menu.d.ts.map +1 -1
- package/dist/lib/components/dropdown-menu.js +20 -11
- package/dist/lib/components/dropdown-menu.js.map +1 -1
- package/dist/lib/components/icon-button.d.ts +3 -0
- package/dist/lib/components/icon-button.d.ts.map +1 -1
- package/dist/lib/components/icon-button.js +10 -2
- package/dist/lib/components/icon-button.js.map +1 -1
- package/dist/lib/components/popover.d.ts +30 -0
- package/dist/lib/components/popover.d.ts.map +1 -0
- package/dist/lib/components/popover.js +93 -0
- package/dist/lib/components/popover.js.map +1 -0
- package/dist/lib/components/range-input.d.ts +26 -0
- package/dist/lib/components/range-input.d.ts.map +1 -0
- package/dist/lib/components/range-input.js +261 -0
- package/dist/lib/components/range-input.js.map +1 -0
- package/dist/lib/components/user-content.d.ts +8 -0
- package/dist/lib/components/user-content.d.ts.map +1 -0
- package/dist/lib/components/user-content.js +15 -0
- package/dist/lib/components/user-content.js.map +1 -0
- package/dist/lib/data-url.d.ts +5 -0
- package/dist/lib/data-url.d.ts.map +1 -0
- package/dist/lib/data-url.js +7 -0
- package/dist/lib/data-url.js.map +1 -0
- package/dist/popup-widget.d.ts +64 -0
- package/dist/popup-widget.d.ts.map +1 -0
- package/dist/popup-widget.js +76 -0
- package/dist/popup-widget.js.map +1 -0
- package/dist/reset-view-widget.d.ts +1 -1
- package/dist/reset-view-widget.d.ts.map +1 -1
- package/dist/reset-view-widget.js +12 -11
- package/dist/reset-view-widget.js.map +1 -1
- package/dist/scrollbar-widget.d.ts +68 -0
- package/dist/scrollbar-widget.d.ts.map +1 -0
- package/dist/scrollbar-widget.js +181 -0
- package/dist/scrollbar-widget.js.map +1 -0
- package/dist/selector-widget.d.ts +44 -0
- package/dist/selector-widget.d.ts.map +1 -0
- package/dist/selector-widget.js +64 -0
- package/dist/selector-widget.js.map +1 -0
- package/dist/splitter-widget.d.ts +52 -11
- package/dist/splitter-widget.d.ts.map +1 -1
- package/dist/splitter-widget.js +177 -76
- package/dist/splitter-widget.js.map +1 -1
- package/dist/stats-widget.d.ts +6 -1
- package/dist/stats-widget.d.ts.map +1 -1
- package/dist/stats-widget.js +39 -10
- package/dist/stats-widget.js.map +1 -1
- package/dist/stylesheet.css +291 -39
- package/dist/theme-widget.d.ts.map +1 -1
- package/dist/theme-widget.js +7 -5
- package/dist/theme-widget.js.map +1 -1
- package/dist/themes.d.ts +30 -0
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +28 -4
- package/dist/themes.js.map +1 -1
- package/dist/timeline-widget.d.ts +25 -6
- package/dist/timeline-widget.d.ts.map +1 -1
- package/dist/timeline-widget.js +51 -18
- package/dist/timeline-widget.js.map +1 -1
- package/dist/toggle-widget.d.ts +38 -0
- package/dist/toggle-widget.d.ts.map +1 -0
- package/dist/toggle-widget.js +47 -0
- package/dist/toggle-widget.js.map +1 -0
- package/dist/zoom-widget.d.ts +2 -8
- package/dist/zoom-widget.d.ts.map +1 -1
- package/dist/zoom-widget.js +23 -18
- package/dist/zoom-widget.js.map +1 -1
- package/dist.min.js +2 -2
- package/package.json +4 -3
- package/src/compass-widget.tsx +5 -5
- package/src/context-menu-widget.tsx +88 -78
- package/src/fullscreen-widget.tsx +1 -1
- package/src/geocoder-widget.tsx +17 -24
- package/src/gimbal-widget.tsx +4 -26
- package/src/icon-widget.tsx +69 -0
- package/src/index.ts +22 -15
- package/src/info-widget.tsx +100 -157
- package/src/lib/components/dropdown-menu.tsx +58 -29
- package/src/lib/components/icon-button.tsx +15 -3
- package/src/lib/components/popover.tsx +167 -0
- package/src/lib/components/range-input.tsx +432 -0
- package/src/lib/components/user-content.tsx +30 -0
- package/src/lib/data-url.ts +5 -0
- package/src/popup-widget.tsx +170 -0
- package/src/reset-view-widget.tsx +12 -11
- package/src/scrollbar-widget.tsx +288 -0
- package/src/selector-widget.tsx +111 -0
- package/src/splitter-widget.tsx +281 -96
- package/src/stats-widget.tsx +59 -12
- package/src/stylesheet.css +291 -39
- package/src/theme-widget.tsx +7 -5
- package/src/themes.ts +35 -4
- package/src/timeline-widget.tsx +103 -33
- package/src/toggle-widget.tsx +101 -0
- package/src/zoom-widget.tsx +25 -21
- package/dist/fps-widget.d.ts +0 -28
- package/dist/fps-widget.d.ts.map +0 -1
- package/dist/fps-widget.js +0 -51
- package/dist/fps-widget.js.map +0 -1
- package/dist/lib/components/grouped-icon-button.d.ts +0 -10
- package/dist/lib/components/grouped-icon-button.d.ts.map +0 -1
- package/dist/lib/components/grouped-icon-button.js +0 -7
- package/dist/lib/components/grouped-icon-button.js.map +0 -1
- package/dist/lib/components/icon-menu.d.ts +0 -16
- package/dist/lib/components/icon-menu.d.ts.map +0 -1
- package/dist/lib/components/icon-menu.js +0 -34
- package/dist/lib/components/icon-menu.js.map +0 -1
- package/dist/lib/components/simple-menu.d.ts +0 -16
- package/dist/lib/components/simple-menu.d.ts.map +0 -1
- package/dist/lib/components/simple-menu.js +0 -30
- package/dist/lib/components/simple-menu.js.map +0 -1
- package/dist/view-selector-widget.d.ts +0 -35
- package/dist/view-selector-widget.d.ts.map +0 -1
- package/dist/view-selector-widget.js +0 -63
- package/dist/view-selector-widget.js.map +0 -1
- package/src/fps-widget.tsx +0 -77
- package/src/lib/components/grouped-icon-button.tsx +0 -27
- package/src/lib/components/icon-menu.tsx +0 -71
- package/src/lib/components/simple-menu.tsx +0 -52
- package/src/view-selector-widget.tsx +0 -151
package/src/info-widget.tsx
CHANGED
|
@@ -5,43 +5,67 @@
|
|
|
5
5
|
import {Widget} from '@deck.gl/core';
|
|
6
6
|
import type {Deck, PickingInfo, Viewport, WidgetProps} from '@deck.gl/core';
|
|
7
7
|
import {render, JSX} from 'preact';
|
|
8
|
+
import {Popover, type PopoverProps} from './lib/components/popover';
|
|
9
|
+
import {UserContent} from './lib/components/user-content';
|
|
10
|
+
|
|
11
|
+
export type TooltipContent = {
|
|
12
|
+
/** Anchor of the popup in world coordinates, e.g. [longitude, latitude].
|
|
13
|
+
* If not supplied, default to the mouse position where the popup was triggered.
|
|
14
|
+
*/
|
|
15
|
+
position?: number[];
|
|
16
|
+
/** Text content to display in the popup */
|
|
17
|
+
text?: string;
|
|
18
|
+
/** HTML content to display in the popup. If supplied, `text` is ignored. */
|
|
19
|
+
html?: string;
|
|
20
|
+
/** HTML element to attach inside the popup. */
|
|
21
|
+
element?: HTMLElement | null;
|
|
22
|
+
/** Custom class name to add to the popup */
|
|
23
|
+
className?: string;
|
|
24
|
+
/** CSS style overrides of the popup */
|
|
25
|
+
style?: Partial<CSSStyleDeclaration>;
|
|
26
|
+
};
|
|
8
27
|
|
|
9
28
|
export type InfoWidgetProps = WidgetProps & {
|
|
10
29
|
/** View to attach to and interact with. Required when using multiple views */
|
|
11
30
|
viewId?: string | null;
|
|
12
31
|
/** Determines the interaction mode of the widget */
|
|
13
|
-
mode: 'click' | 'hover'
|
|
32
|
+
mode: 'click' | 'hover';
|
|
14
33
|
/** Function to generate the popup contents from the selected element */
|
|
15
|
-
getTooltip?: (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
getTooltip?: (
|
|
35
|
+
info: PickingInfo,
|
|
36
|
+
widget: InfoWidget
|
|
37
|
+
) => TooltipContent | string | null | undefined;
|
|
38
|
+
/** Position popup relative to the anchor.
|
|
39
|
+
* @default 'right'
|
|
40
|
+
*/
|
|
41
|
+
placement?: PopoverProps['placement'];
|
|
42
|
+
/** Pixel offset
|
|
43
|
+
* @default 10
|
|
44
|
+
*/
|
|
45
|
+
offset?: PopoverProps['offset'];
|
|
46
|
+
/**
|
|
47
|
+
* Show an arrow pointing at the anchor. Optionally accepts a pixel size.
|
|
48
|
+
* @default 10
|
|
49
|
+
*/
|
|
50
|
+
arrow?: PopoverProps['arrow'];
|
|
26
51
|
};
|
|
27
52
|
|
|
28
53
|
export class InfoWidget extends Widget<InfoWidgetProps> {
|
|
29
54
|
static defaultProps: Required<InfoWidgetProps> = {
|
|
30
55
|
...Widget.defaultProps,
|
|
31
56
|
id: 'info',
|
|
32
|
-
position: [0, 0],
|
|
33
|
-
text: '',
|
|
34
|
-
visible: false,
|
|
35
|
-
minOffset: 0,
|
|
36
57
|
viewId: null,
|
|
37
58
|
mode: 'hover',
|
|
38
59
|
getTooltip: undefined!,
|
|
39
|
-
|
|
60
|
+
placement: 'right',
|
|
61
|
+
offset: 10,
|
|
62
|
+
arrow: 10
|
|
40
63
|
};
|
|
41
64
|
|
|
42
65
|
className = 'deck-widget-info';
|
|
43
66
|
placement = 'fill' as const;
|
|
44
67
|
viewport?: Viewport;
|
|
68
|
+
tooltip: Required<TooltipContent> | null = null;
|
|
45
69
|
|
|
46
70
|
constructor(props: InfoWidgetProps) {
|
|
47
71
|
super(props);
|
|
@@ -53,11 +77,15 @@ export class InfoWidget extends Widget<InfoWidgetProps> {
|
|
|
53
77
|
super.setProps(props);
|
|
54
78
|
}
|
|
55
79
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
onAdd({deck}: {deck: Deck<any>; viewId: string | null}) {
|
|
81
|
+
this.deck = deck;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onRemove() {
|
|
85
|
+
// Invoke clean up of preact hooks
|
|
86
|
+
if (this.rootElement) {
|
|
87
|
+
render(null, this.rootElement);
|
|
88
|
+
}
|
|
61
89
|
}
|
|
62
90
|
|
|
63
91
|
onViewportChange(viewport) {
|
|
@@ -66,156 +94,71 @@ export class InfoWidget extends Widget<InfoWidgetProps> {
|
|
|
66
94
|
}
|
|
67
95
|
|
|
68
96
|
onHover(info: PickingInfo): void {
|
|
69
|
-
if (this.props.mode === 'hover'
|
|
70
|
-
|
|
71
|
-
// hover tooltips should show over static and click infos
|
|
72
|
-
this.setProps({
|
|
73
|
-
visible: tooltip !== null,
|
|
74
|
-
...tooltip,
|
|
75
|
-
style: {zIndex: '1', ...tooltip?.style}
|
|
76
|
-
});
|
|
97
|
+
if (this.props.mode === 'hover') {
|
|
98
|
+
this.tooltip = this._getTooltip(info);
|
|
77
99
|
}
|
|
78
100
|
}
|
|
79
101
|
|
|
80
|
-
onClick(info: PickingInfo):
|
|
81
|
-
if (this.props.mode === 'click'
|
|
82
|
-
|
|
83
|
-
this.setProps({
|
|
84
|
-
visible: tooltip !== null,
|
|
85
|
-
...tooltip
|
|
86
|
-
});
|
|
87
|
-
return tooltip !== null;
|
|
102
|
+
onClick(info: PickingInfo): void {
|
|
103
|
+
if (this.props.mode === 'click') {
|
|
104
|
+
this.tooltip = this._getTooltip(info);
|
|
88
105
|
}
|
|
89
|
-
|
|
90
|
-
// Original behavior
|
|
91
|
-
return this.props.onClick?.(this, info) || false;
|
|
92
106
|
}
|
|
93
107
|
|
|
94
|
-
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
protected _getTooltip(info: PickingInfo): Required<TooltipContent> | null {
|
|
109
|
+
if (!this.props.getTooltip) return null;
|
|
110
|
+
|
|
111
|
+
const content = this.props.getTooltip(info, this) ?? null;
|
|
112
|
+
if (content === null) return null;
|
|
113
|
+
const normalizedTooltip: TooltipContent =
|
|
114
|
+
typeof content === 'string' ? {text: content} : content;
|
|
115
|
+
|
|
116
|
+
const position = normalizedTooltip.position || info.coordinate;
|
|
117
|
+
if (!position) return null;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
position,
|
|
121
|
+
text: '',
|
|
122
|
+
html: '',
|
|
123
|
+
element: null,
|
|
124
|
+
className: '',
|
|
125
|
+
style: {},
|
|
126
|
+
...normalizedTooltip
|
|
127
|
+
};
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
onRenderHTML(rootElement: HTMLElement): void {
|
|
104
|
-
if (!this.viewport) {
|
|
131
|
+
if (!this.viewport || this.tooltip === null) {
|
|
132
|
+
render(null, rootElement);
|
|
105
133
|
return;
|
|
106
134
|
}
|
|
107
|
-
const
|
|
135
|
+
const style: Partial<CSSStyleDeclaration> = {
|
|
136
|
+
...this.props.style,
|
|
137
|
+
...this.tooltip.style
|
|
138
|
+
};
|
|
108
139
|
// Project the clicked geographic coordinate to canvas (x, y)
|
|
109
|
-
const [x, y] = this.viewport.project(
|
|
110
|
-
|
|
111
|
-
const minOffset = this.props.minOffset || 0;
|
|
112
|
-
const gap = 10; // gap between clicked point and popup box
|
|
113
|
-
const arrowHeight = 8; // height of the triangle arrow
|
|
114
|
-
const arrowWidth = 16; // full width of the arrow
|
|
115
|
-
|
|
116
|
-
// Decide vertical orientation.
|
|
117
|
-
// If the clicked point is in the bottom half, place the popup above the point.
|
|
118
|
-
const isAbove = y > this.viewport.height / 2;
|
|
119
|
-
const background = (this.props.style && this.props.style.background) || 'rgba(255,255,255,0.9)';
|
|
140
|
+
const [x, y] = this.viewport.project(this.tooltip.position);
|
|
120
141
|
|
|
121
142
|
// Render the popup container with a content box and a placeholder for the arrow.
|
|
122
143
|
// The container is positioned absolutely (initially at 0,0) and will be repositioned after measuring.
|
|
123
|
-
const ui =
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}}
|
|
134
|
-
|
|
135
|
-
{this.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
const ui = (
|
|
145
|
+
<Popover
|
|
146
|
+
x={x}
|
|
147
|
+
y={y}
|
|
148
|
+
placement={this.props.placement}
|
|
149
|
+
arrow={this.props.arrow}
|
|
150
|
+
arrowColor="var(--menu-background, #fff)"
|
|
151
|
+
offset={this.props.offset}
|
|
152
|
+
>
|
|
153
|
+
<UserContent
|
|
154
|
+
className={`deck-widget-popup-content ${this.tooltip.className} ${this.props.className}`}
|
|
155
|
+
style={style as JSX.CSSProperties}
|
|
156
|
+
html={this.tooltip.html}
|
|
157
|
+
text={this.tooltip.text}
|
|
158
|
+
element={this.tooltip.element}
|
|
159
|
+
/>
|
|
160
|
+
</Popover>
|
|
161
|
+
);
|
|
140
162
|
render(ui, rootElement);
|
|
141
|
-
|
|
142
|
-
// After rendering, measure the content and adjust positioning so that:
|
|
143
|
-
// - The popup (with its arrow) does not cover the clicked point.
|
|
144
|
-
// - The arrow's tip points to the clicked point.
|
|
145
|
-
// - The popup remains within the canvas bounds, with minOffset.
|
|
146
|
-
// eslint-disable-next-line max-statements, complexity
|
|
147
|
-
requestAnimationFrame(() => {
|
|
148
|
-
if (!this.props.visible || !rootElement.firstChild || !this.viewport) return;
|
|
149
|
-
|
|
150
|
-
const container = rootElement.firstChild as HTMLElement;
|
|
151
|
-
const contentEl = container.querySelector('.popup-content') as HTMLElement;
|
|
152
|
-
const arrowEl = container.querySelector('.popup-arrow') as HTMLElement;
|
|
153
|
-
if (!contentEl || !arrowEl) return;
|
|
154
|
-
const contentRect = contentEl.getBoundingClientRect();
|
|
155
|
-
const popupWidth = contentRect.width;
|
|
156
|
-
const popupHeight = contentRect.height;
|
|
157
|
-
|
|
158
|
-
// Compute ideal horizontal position: center the popup on the clicked x
|
|
159
|
-
let computedLeft = x - popupWidth / 2;
|
|
160
|
-
// Compute vertical position based on orientation:
|
|
161
|
-
// If the popup is above, position its bottom edge (minus arrow & gap) at the clicked y.
|
|
162
|
-
// Otherwise, position its top edge (plus arrow & gap) at the clicked y.
|
|
163
|
-
let computedTop;
|
|
164
|
-
if (isAbove) {
|
|
165
|
-
computedTop = y - gap - arrowHeight - popupHeight;
|
|
166
|
-
} else {
|
|
167
|
-
computedTop = y + gap + arrowHeight;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Ensure the popup (content) stays within horizontal bounds of the canvas
|
|
171
|
-
if (computedLeft < minOffset) {
|
|
172
|
-
computedLeft = minOffset;
|
|
173
|
-
}
|
|
174
|
-
if (computedLeft + popupWidth > this.viewport.width - minOffset) {
|
|
175
|
-
computedLeft = this.viewport.width - minOffset - popupWidth;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Ensure the vertical position (including the arrow) stays within canvas bounds.
|
|
179
|
-
if (isAbove) {
|
|
180
|
-
if (computedTop < minOffset) {
|
|
181
|
-
computedTop = minOffset;
|
|
182
|
-
}
|
|
183
|
-
} else if (computedTop + popupHeight + arrowHeight > this.viewport.height - minOffset) {
|
|
184
|
-
computedTop = this.viewport.height - minOffset - popupHeight - arrowHeight;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Update container position (remove any transform and set left/top explicitly)
|
|
188
|
-
container.style.left = `${computedLeft}px`;
|
|
189
|
-
container.style.top = `${computedTop}px`;
|
|
190
|
-
container.style.transform = '';
|
|
191
|
-
|
|
192
|
-
// Compute arrow's horizontal offset relative to the container.
|
|
193
|
-
// We want the arrow's tip to align with the clicked point.
|
|
194
|
-
let arrowLeft = x - computedLeft - arrowWidth / 2;
|
|
195
|
-
// Optionally constrain arrowLeft so it stays within the popup's width.
|
|
196
|
-
arrowLeft = Math.max(arrowLeft, 0);
|
|
197
|
-
arrowLeft = Math.min(arrowLeft, popupWidth - arrowWidth);
|
|
198
|
-
|
|
199
|
-
// Update arrow element's style based on orientation.
|
|
200
|
-
if (isAbove) {
|
|
201
|
-
// Popup is above the clicked point so arrow appears at the bottom of the popup.
|
|
202
|
-
arrowEl.style.left = `${arrowLeft}px`;
|
|
203
|
-
arrowEl.style.bottom = `-${arrowHeight}px`;
|
|
204
|
-
arrowEl.style.top = '';
|
|
205
|
-
arrowEl.style.borderLeft = `${arrowWidth / 2}px solid transparent`;
|
|
206
|
-
arrowEl.style.borderRight = `${arrowWidth / 2}px solid transparent`;
|
|
207
|
-
arrowEl.style.borderTop = `${arrowHeight}px solid ${background}`;
|
|
208
|
-
arrowEl.style.borderBottom = '';
|
|
209
|
-
} else {
|
|
210
|
-
// Popup is below the clicked point so arrow appears at the top.
|
|
211
|
-
arrowEl.style.left = `${arrowLeft}px`;
|
|
212
|
-
arrowEl.style.top = `-${arrowHeight}px`;
|
|
213
|
-
arrowEl.style.bottom = '';
|
|
214
|
-
arrowEl.style.borderLeft = `${arrowWidth / 2}px solid transparent`;
|
|
215
|
-
arrowEl.style.borderRight = `${arrowWidth / 2}px solid transparent`;
|
|
216
|
-
arrowEl.style.borderBottom = `${arrowHeight}px solid ${background}`;
|
|
217
|
-
arrowEl.style.borderTop = '';
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
163
|
}
|
|
221
164
|
}
|
|
@@ -2,18 +2,26 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
|
-
import {type JSX} from 'preact';
|
|
5
|
+
import {type JSX, type ComponentChild} from 'preact';
|
|
6
6
|
import {useState, useRef, useEffect} from 'preact/hooks';
|
|
7
|
+
import {getCSSMask} from '../data-url';
|
|
7
8
|
|
|
8
|
-
export type MenuItem =
|
|
9
|
+
export type MenuItem =
|
|
10
|
+
| string
|
|
11
|
+
| {
|
|
12
|
+
value?: string;
|
|
13
|
+
label: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
onSelect?: () => void;
|
|
16
|
+
};
|
|
9
17
|
|
|
10
18
|
export type DropdownMenuProps = {
|
|
11
19
|
menuItems: MenuItem[];
|
|
12
|
-
onSelect
|
|
13
|
-
style?:
|
|
20
|
+
onSelect?: (value: string) => void;
|
|
21
|
+
style?: Partial<CSSStyleDeclaration>;
|
|
14
22
|
};
|
|
15
23
|
|
|
16
|
-
function getMenuItemValue(item: MenuItem): string {
|
|
24
|
+
function getMenuItemValue(item: MenuItem): string | undefined {
|
|
17
25
|
return typeof item === 'string' ? item : item.value;
|
|
18
26
|
}
|
|
19
27
|
|
|
@@ -27,26 +35,51 @@ function getMenuItemIcon(item: MenuItem): string | undefined {
|
|
|
27
35
|
|
|
28
36
|
export const DropdownMenu = (props: DropdownMenuProps) => {
|
|
29
37
|
const [isOpen, setIsOpen] = useState(false);
|
|
30
|
-
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
return (
|
|
40
|
+
<SimpleMenu
|
|
41
|
+
{...props}
|
|
42
|
+
style={{...props.style, position: 'absolute'}}
|
|
43
|
+
isOpen={isOpen}
|
|
44
|
+
onClose={() => setIsOpen(false)}
|
|
45
|
+
trigger={
|
|
46
|
+
<button className="deck-widget-dropdown-button" onClick={() => setIsOpen(!isOpen)}>
|
|
47
|
+
<span className={`deck-widget-dropdown-icon ${isOpen ? 'open' : ''}`} />
|
|
48
|
+
</button>
|
|
49
|
+
}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
33
53
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
export type SimpleMenuProps = DropdownMenuProps & {
|
|
55
|
+
trigger?: ComponentChild;
|
|
56
|
+
isOpen: boolean;
|
|
57
|
+
onClose: () => void;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const SimpleMenu = (props: SimpleMenuProps) => {
|
|
61
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
39
62
|
|
|
40
63
|
useEffect(() => {
|
|
64
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
65
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
66
|
+
props.onClose();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
41
69
|
document.addEventListener('mousedown', handleClickOutside);
|
|
42
70
|
return () => {
|
|
43
71
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
44
72
|
};
|
|
45
73
|
}, []);
|
|
46
74
|
|
|
47
|
-
const handleSelect = (item: MenuItem) => {
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
const handleSelect = (value: string | undefined, item: MenuItem) => {
|
|
76
|
+
if (value) {
|
|
77
|
+
if (typeof item === 'object') {
|
|
78
|
+
item.onSelect?.();
|
|
79
|
+
}
|
|
80
|
+
props.onSelect?.(value);
|
|
81
|
+
props.onClose();
|
|
82
|
+
}
|
|
50
83
|
};
|
|
51
84
|
|
|
52
85
|
// Don't render anything if there are no menu items
|
|
@@ -55,25 +88,21 @@ export const DropdownMenu = (props: DropdownMenuProps) => {
|
|
|
55
88
|
}
|
|
56
89
|
|
|
57
90
|
return (
|
|
58
|
-
<div className="deck-widget-dropdown-container" ref={dropdownRef}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
{props.menuItems.map(item => {
|
|
91
|
+
<div className="deck-widget-dropdown-container" ref={dropdownRef}>
|
|
92
|
+
{props.trigger}
|
|
93
|
+
{props.isOpen && (
|
|
94
|
+
<ul className="deck-widget-dropdown-menu" style={props.style as JSX.CSSProperties}>
|
|
95
|
+
{props.menuItems.map((item, i) => {
|
|
96
|
+
const value = getMenuItemValue(item);
|
|
65
97
|
const icon = getMenuItemIcon(item);
|
|
66
98
|
return (
|
|
67
99
|
<li
|
|
68
|
-
className=
|
|
69
|
-
key={
|
|
70
|
-
onClick={() => handleSelect(item)}
|
|
100
|
+
className={`deck-widget-dropdown-item ${value ? '' : 'disabled'}`}
|
|
101
|
+
key={i}
|
|
102
|
+
onClick={() => handleSelect(value, item)}
|
|
71
103
|
>
|
|
72
104
|
{icon && (
|
|
73
|
-
<span
|
|
74
|
-
className="deck-widget-dropdown-item-icon"
|
|
75
|
-
style={{maskImage: `url("${icon}")`, WebkitMaskImage: `url("${icon}")`}}
|
|
76
|
-
/>
|
|
105
|
+
<span className="deck-widget-dropdown-item-icon" style={getCSSMask(icon)} />
|
|
77
106
|
)}
|
|
78
107
|
{getMenuItemLabel(item)}
|
|
79
108
|
</li>
|
|
@@ -3,26 +3,38 @@
|
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
5
|
import type {ComponentChildren, JSX} from 'preact';
|
|
6
|
+
import {useMemo} from 'preact/hooks';
|
|
7
|
+
import {getCSSMask} from '../data-url';
|
|
6
8
|
|
|
7
9
|
export type IconButtonProps = {
|
|
8
10
|
className?: string;
|
|
11
|
+
icon?: string;
|
|
9
12
|
label?: string;
|
|
13
|
+
color?: string;
|
|
14
|
+
style?: JSX.CSSProperties;
|
|
10
15
|
onClick?: JSX.MouseEventHandler<HTMLButtonElement>;
|
|
11
16
|
children?: ComponentChildren;
|
|
12
17
|
};
|
|
13
18
|
|
|
14
19
|
/** Renders a button component with widget CSS */
|
|
15
20
|
export const IconButton = (props: IconButtonProps) => {
|
|
16
|
-
const {className = '', label, onClick, children} = props;
|
|
21
|
+
const {className = '', style, color, icon, label, onClick, children} = props;
|
|
22
|
+
|
|
23
|
+
const iconStyle = useMemo(() => {
|
|
24
|
+
const css: JSX.CSSProperties | undefined = getCSSMask(icon);
|
|
25
|
+
if (!color) return css;
|
|
26
|
+
return {...css, backgroundColor: color};
|
|
27
|
+
}, [color, icon]);
|
|
28
|
+
|
|
17
29
|
return (
|
|
18
|
-
<div className="deck-widget-button">
|
|
30
|
+
<div className="deck-widget-button" style={style}>
|
|
19
31
|
<button
|
|
20
32
|
className={`deck-widget-icon-button ${className}`}
|
|
21
33
|
type="button"
|
|
22
34
|
onClick={onClick}
|
|
23
35
|
title={label}
|
|
24
36
|
>
|
|
25
|
-
{children ? children : <div className="deck-widget-icon" />}
|
|
37
|
+
{children ? children : <div className="deck-widget-icon" style={iconStyle} />}
|
|
26
38
|
</button>
|
|
27
39
|
</div>
|
|
28
40
|
);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// deck.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
import type {JSX, ComponentChildren} from 'preact';
|
|
5
|
+
import {useRef, useEffect, useMemo} from 'preact/hooks';
|
|
6
|
+
import {
|
|
7
|
+
computePosition,
|
|
8
|
+
flip,
|
|
9
|
+
shift,
|
|
10
|
+
offset,
|
|
11
|
+
arrow,
|
|
12
|
+
autoUpdate,
|
|
13
|
+
type Placement,
|
|
14
|
+
type ComputePositionConfig
|
|
15
|
+
} from '@floating-ui/dom';
|
|
16
|
+
|
|
17
|
+
export type PopoverProps = {
|
|
18
|
+
/** Anchor x */
|
|
19
|
+
x: number;
|
|
20
|
+
/** Anchor y */
|
|
21
|
+
y: number;
|
|
22
|
+
/** Position content relative to the anchor.
|
|
23
|
+
* @default 'right'
|
|
24
|
+
*/
|
|
25
|
+
placement?: Placement;
|
|
26
|
+
/** Pixel offset
|
|
27
|
+
* @default 0
|
|
28
|
+
*/
|
|
29
|
+
offset?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Show an arrow pointing at the anchor. Optionally accepts a pixel size.
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
arrow?: false | number | [width: number, height: number];
|
|
35
|
+
/**
|
|
36
|
+
* CSS color of the arrow
|
|
37
|
+
* @default 'white'
|
|
38
|
+
*/
|
|
39
|
+
arrowColor?: string;
|
|
40
|
+
/** Content of the popover */
|
|
41
|
+
children: ComponentChildren;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Popover = ({
|
|
45
|
+
x,
|
|
46
|
+
y,
|
|
47
|
+
placement = 'right',
|
|
48
|
+
offset: pixelOffset = 0,
|
|
49
|
+
arrow: arrowSize = false,
|
|
50
|
+
arrowColor = 'white',
|
|
51
|
+
children
|
|
52
|
+
}: PopoverProps) => {
|
|
53
|
+
const anchorRef = useRef<HTMLDivElement>(null);
|
|
54
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
55
|
+
const arrowRef = useRef<HTMLDivElement>(null);
|
|
56
|
+
const updaterRef = useRef<() => void>();
|
|
57
|
+
|
|
58
|
+
updaterRef.current = () => {
|
|
59
|
+
if (!anchorRef.current || !contentRef.current) return;
|
|
60
|
+
|
|
61
|
+
const arrowWidth = Array.isArray(arrowSize) ? arrowSize[0] : arrowSize || 0;
|
|
62
|
+
const arrowHeight = Array.isArray(arrowSize) ? arrowSize[1] : arrowSize || 0;
|
|
63
|
+
const padding = pixelOffset + Math.max(arrowHeight, arrowWidth);
|
|
64
|
+
|
|
65
|
+
const middleware: ComputePositionConfig['middleware'] = placement.includes('-')
|
|
66
|
+
? [offset(padding), flip(), shift()]
|
|
67
|
+
: [offset(padding), shift(), flip()];
|
|
68
|
+
if (arrowRef.current) middleware.push(arrow({element: arrowRef.current}));
|
|
69
|
+
computePosition(anchorRef.current, contentRef.current, {
|
|
70
|
+
placement,
|
|
71
|
+
strategy: 'fixed',
|
|
72
|
+
middleware
|
|
73
|
+
}).then(popoverPos => {
|
|
74
|
+
if (contentRef.current) {
|
|
75
|
+
Object.assign(contentRef.current.style, {
|
|
76
|
+
left: `${popoverPos.x}px`,
|
|
77
|
+
top: `${popoverPos.y}px`
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const arrowData = popoverPos.middlewareData.arrow;
|
|
82
|
+
if (arrowData && arrowRef.current) {
|
|
83
|
+
const arrowStyle = createArrow(arrowWidth, arrowHeight, arrowColor, popoverPos.placement);
|
|
84
|
+
arrowStyle.transform = `translate(${arrowData.x || 0}px, ${arrowData.y || 0}px)`;
|
|
85
|
+
Object.assign(arrowRef.current.style, arrowStyle);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
useMemo(() => {
|
|
91
|
+
updaterRef.current?.();
|
|
92
|
+
}, [x, y, placement, arrowSize, pixelOffset]);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
// initial mount
|
|
96
|
+
if (contentRef.current) {
|
|
97
|
+
contentRef.current.style.visibility = 'visible';
|
|
98
|
+
}
|
|
99
|
+
const cleanup = autoUpdate(anchorRef.current!, contentRef.current!, () =>
|
|
100
|
+
updaterRef.current?.()
|
|
101
|
+
);
|
|
102
|
+
return () => {
|
|
103
|
+
cleanup();
|
|
104
|
+
};
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div style={{position: 'absolute', left: x, top: y}} ref={anchorRef}>
|
|
109
|
+
<div
|
|
110
|
+
className="deck-widget deck-widget-popover"
|
|
111
|
+
style={{position: 'fixed', visibility: 'hidden', pointerEvents: 'none'}}
|
|
112
|
+
ref={contentRef}
|
|
113
|
+
>
|
|
114
|
+
{Boolean(arrowSize) && (
|
|
115
|
+
<div
|
|
116
|
+
className="deck-widget-popover-arrow"
|
|
117
|
+
style={{position: 'absolute'}}
|
|
118
|
+
ref={arrowRef}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
{children}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function createArrow(
|
|
128
|
+
width: number,
|
|
129
|
+
height: number,
|
|
130
|
+
color: string,
|
|
131
|
+
placement: Placement
|
|
132
|
+
): JSX.CSSProperties {
|
|
133
|
+
const result: JSX.CSSProperties = {
|
|
134
|
+
width: 0,
|
|
135
|
+
height: 0,
|
|
136
|
+
top: '',
|
|
137
|
+
bottom: '',
|
|
138
|
+
left: '',
|
|
139
|
+
right: ''
|
|
140
|
+
};
|
|
141
|
+
if (placement.startsWith('bottom')) {
|
|
142
|
+
result.borderLeft = `${width / 2}px solid transparent`;
|
|
143
|
+
result.borderRight = `${width / 2}px solid transparent`;
|
|
144
|
+
result.borderBottom = `${height}px solid ${color}`;
|
|
145
|
+
result.borderTop = '';
|
|
146
|
+
result.top = `${-height}px`;
|
|
147
|
+
} else if (placement.startsWith('top')) {
|
|
148
|
+
result.borderLeft = `${width / 2}px solid transparent`;
|
|
149
|
+
result.borderRight = `${width / 2}px solid transparent`;
|
|
150
|
+
result.borderTop = `${height}px solid ${color}`;
|
|
151
|
+
result.borderBottom = '';
|
|
152
|
+
result.bottom = `${-height}px`;
|
|
153
|
+
} else if (placement.startsWith('right')) {
|
|
154
|
+
result.borderTop = `${width / 2}px solid transparent`;
|
|
155
|
+
result.borderBottom = `${width / 2}px solid transparent`;
|
|
156
|
+
result.borderRight = `${height}px solid ${color}`;
|
|
157
|
+
result.borderLeft = '';
|
|
158
|
+
result.left = `${-height}px`;
|
|
159
|
+
} else if (placement.startsWith('left')) {
|
|
160
|
+
result.borderTop = `${width / 2}px solid transparent`;
|
|
161
|
+
result.borderBottom = `${width / 2}px solid transparent`;
|
|
162
|
+
result.borderLeft = `${height}px solid ${color}`;
|
|
163
|
+
result.borderRight = '';
|
|
164
|
+
result.right = `${-height}px`;
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|