@deck.gl-community/widgets 9.2.0-beta.5
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/LICENSE +20 -0
- package/README.md +43 -0
- package/dist/_deprecate/long-press-button.d.ts +13 -0
- package/dist/_deprecate/long-press-button.d.ts.map +1 -0
- package/dist/_deprecate/long-press-button.js +32 -0
- package/dist/_deprecate/long-press-button.js.map +1 -0
- package/dist/_deprecate/view-control-widget.d.ts +78 -0
- package/dist/_deprecate/view-control-widget.d.ts.map +1 -0
- package/dist/_deprecate/view-control-widget.js +198 -0
- package/dist/_deprecate/view-control-widget.js.map +1 -0
- package/dist/index.cjs +708 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/widgets/html-cluster-widget.d.ts +25 -0
- package/dist/widgets/html-cluster-widget.d.ts.map +1 -0
- package/dist/widgets/html-cluster-widget.js +39 -0
- package/dist/widgets/html-cluster-widget.js.map +1 -0
- package/dist/widgets/html-overlay-item.d.ts +13 -0
- package/dist/widgets/html-overlay-item.d.ts.map +1 -0
- package/dist/widgets/html-overlay-item.js +10 -0
- package/dist/widgets/html-overlay-item.js.map +1 -0
- package/dist/widgets/html-overlay-widget.d.ts +45 -0
- package/dist/widgets/html-overlay-widget.d.ts.map +1 -0
- package/dist/widgets/html-overlay-widget.js +112 -0
- package/dist/widgets/html-overlay-widget.js.map +1 -0
- package/dist/widgets/html-tooltip-widget.d.ts +30 -0
- package/dist/widgets/html-tooltip-widget.d.ts.map +1 -0
- package/dist/widgets/html-tooltip-widget.js +67 -0
- package/dist/widgets/html-tooltip-widget.js.map +1 -0
- package/dist/widgets/long-press-button.d.ts +22 -0
- package/dist/widgets/long-press-button.d.ts.map +1 -0
- package/dist/widgets/long-press-button.js +84 -0
- package/dist/widgets/long-press-button.js.map +1 -0
- package/dist/widgets/long-press-controller.d.ts +27 -0
- package/dist/widgets/long-press-controller.d.ts.map +1 -0
- package/dist/widgets/long-press-controller.js +144 -0
- package/dist/widgets/long-press-controller.js.map +1 -0
- package/dist/widgets/pan-widget.d.ts +33 -0
- package/dist/widgets/pan-widget.d.ts.map +1 -0
- package/dist/widgets/pan-widget.js +141 -0
- package/dist/widgets/pan-widget.js.map +1 -0
- package/dist/widgets/view-manager-utils.d.ts +11 -0
- package/dist/widgets/view-manager-utils.d.ts.map +1 -0
- package/dist/widgets/view-manager-utils.js +13 -0
- package/dist/widgets/view-manager-utils.js.map +1 -0
- package/dist/widgets/zoom-range-widget.d.ts +43 -0
- package/dist/widgets/zoom-range-widget.d.ts.map +1 -0
- package/dist/widgets/zoom-range-widget.js +190 -0
- package/dist/widgets/zoom-range-widget.js.map +1 -0
- package/package.json +41 -0
- package/src/_deprecate/long-press-button.tsx +50 -0
- package/src/_deprecate/view-control-widget.tsx +339 -0
- package/src/index.ts +18 -0
- package/src/widgets/html-cluster-widget.ts +84 -0
- package/src/widgets/html-overlay-item.tsx +32 -0
- package/src/widgets/html-overlay-widget.tsx +147 -0
- package/src/widgets/html-tooltip-widget.tsx +93 -0
- package/src/widgets/long-press-button.tsx +125 -0
- package/src/widgets/long-press-controller.ts +159 -0
- package/src/widgets/pan-widget.tsx +182 -0
- package/src/widgets/view-manager-utils.ts +24 -0
- package/src/widgets/zoom-range-widget.tsx +284 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {point} from '@turf/helpers';
|
|
6
|
+
import Supercluster from 'supercluster';
|
|
7
|
+
import type {VNode} from 'preact';
|
|
8
|
+
|
|
9
|
+
import type {WidgetProps, Viewport} from '@deck.gl/core';
|
|
10
|
+
import {HtmlOverlayWidget, type HtmlOverlayWidgetProps} from './html-overlay-widget';
|
|
11
|
+
|
|
12
|
+
export type HtmlClusterWidgetProps = HtmlOverlayWidgetProps & WidgetProps;
|
|
13
|
+
|
|
14
|
+
export abstract class HtmlClusterWidget<ObjType> extends HtmlOverlayWidget<HtmlClusterWidgetProps> {
|
|
15
|
+
static override defaultProps = {
|
|
16
|
+
...HtmlOverlayWidget.defaultProps,
|
|
17
|
+
id: 'html-cluster-overlay'
|
|
18
|
+
} satisfies Required<WidgetProps> & HtmlClusterWidgetProps;
|
|
19
|
+
|
|
20
|
+
protected superCluster: Supercluster | null = null;
|
|
21
|
+
protected lastObjects: ObjType[] | null = null;
|
|
22
|
+
|
|
23
|
+
protected override getOverlayItems(viewport: Viewport): VNode[] {
|
|
24
|
+
const newObjects = this.getAllObjects();
|
|
25
|
+
if (newObjects !== this.lastObjects) {
|
|
26
|
+
this.superCluster = new Supercluster(this.getClusterOptions());
|
|
27
|
+
this.superCluster.load(
|
|
28
|
+
newObjects.map((object) => point(this.getObjectCoordinates(object), {object}))
|
|
29
|
+
);
|
|
30
|
+
this.lastObjects = newObjects;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const clusters = this.superCluster?.getClusters([-180, -90, 180, 90], Math.round(this.getZoom())) ?? [];
|
|
34
|
+
|
|
35
|
+
const overlayItems = clusters.map(
|
|
36
|
+
({
|
|
37
|
+
geometry: {coordinates},
|
|
38
|
+
properties: {cluster, point_count: pointCount, cluster_id: clusterId, object}
|
|
39
|
+
}) =>
|
|
40
|
+
cluster
|
|
41
|
+
? this.renderCluster(coordinates, clusterId, pointCount)
|
|
42
|
+
: this.renderObject(coordinates, object)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return overlayItems.filter(Boolean) as VNode[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getClusterObjects(clusterId: number): ObjType[] {
|
|
49
|
+
return (
|
|
50
|
+
this.superCluster
|
|
51
|
+
?.getLeaves(clusterId, Infinity)
|
|
52
|
+
.map((leaf) => leaf.properties.object as ObjType) ?? []
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Override to provide items that need clustering.
|
|
57
|
+
// If the items have not changed please provide the same array to avoid
|
|
58
|
+
// regeneration of the cluster which causes performance issues.
|
|
59
|
+
abstract getAllObjects(): ObjType[];
|
|
60
|
+
|
|
61
|
+
// Override to provide coordinates for each object of getAllObjects()
|
|
62
|
+
abstract getObjectCoordinates(obj: ObjType): [number, number];
|
|
63
|
+
|
|
64
|
+
// Get options object used when instantiating supercluster
|
|
65
|
+
getClusterOptions(): Record<string, any> {
|
|
66
|
+
return {
|
|
67
|
+
maxZoom: 20
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Override to return an HtmlOverlayItem
|
|
72
|
+
abstract renderObject(
|
|
73
|
+
coordinates: number[],
|
|
74
|
+
obj: ObjType
|
|
75
|
+
): VNode<Record<string, any>> | null | undefined;
|
|
76
|
+
|
|
77
|
+
// Override to return an HtmlOverlayItem
|
|
78
|
+
// use getClusterObjects() to get cluster contents
|
|
79
|
+
abstract renderCluster(
|
|
80
|
+
coordinates: number[],
|
|
81
|
+
clusterId: number,
|
|
82
|
+
pointCount: number
|
|
83
|
+
): VNode<Record<string, any>> | null | undefined;
|
|
84
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {ComponentChildren, JSX} from 'preact';
|
|
6
|
+
|
|
7
|
+
export type HtmlOverlayItemProps = {
|
|
8
|
+
/** Injected by HtmlOverlayWidget */
|
|
9
|
+
x?: number;
|
|
10
|
+
/** Injected by HtmlOverlayWidget */
|
|
11
|
+
y?: number;
|
|
12
|
+
|
|
13
|
+
/** Coordinates of this overlay in [lng, lat] (and optional z). */
|
|
14
|
+
coordinates: number[];
|
|
15
|
+
children?: ComponentChildren;
|
|
16
|
+
style?: JSX.CSSProperties;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function HtmlOverlayItem({x = 0, y = 0, children, style, ...props}: HtmlOverlayItemProps) {
|
|
20
|
+
const {zIndex = 'auto', ...remainingStyle} = style || {};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
// Using transform translate to position overlay items will result in a smooth zooming
|
|
24
|
+
// effect, whereas using the top/left css properties will cause overlay items to
|
|
25
|
+
// jiggle when zooming
|
|
26
|
+
<div style={{transform: `translate(${x}px, ${y}px)`, position: 'absolute', zIndex: `${zIndex}`}}>
|
|
27
|
+
<div style={{userSelect: 'none', ...remainingStyle}} {...props}>
|
|
28
|
+
{children}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {cloneElement, render, toChildArray, Fragment, type ComponentChildren, type VNode} from 'preact';
|
|
6
|
+
import type {Deck, Viewport, WidgetPlacement, WidgetProps} from '@deck.gl/core';
|
|
7
|
+
import {Widget} from '@deck.gl/core';
|
|
8
|
+
|
|
9
|
+
export type HtmlOverlayWidgetProps = WidgetProps & {
|
|
10
|
+
/** View id to attach the overlay to. Defaults to the containing view. */
|
|
11
|
+
viewId?: string | null;
|
|
12
|
+
/** Margin beyond the viewport before hiding overlay items. */
|
|
13
|
+
overflowMargin?: number;
|
|
14
|
+
/** z-index for the overlay container. */
|
|
15
|
+
zIndex?: number;
|
|
16
|
+
/** Items to render; defaults to the supplied children. */
|
|
17
|
+
items?: ComponentChildren;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const ROOT_STYLE: Partial<CSSStyleDeclaration> = {
|
|
21
|
+
width: '100%',
|
|
22
|
+
height: '100%',
|
|
23
|
+
position: 'absolute',
|
|
24
|
+
pointerEvents: 'none',
|
|
25
|
+
overflow: 'hidden'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export class HtmlOverlayWidget<PropsT extends HtmlOverlayWidgetProps = HtmlOverlayWidgetProps> extends Widget<PropsT> {
|
|
29
|
+
static override defaultProps = {
|
|
30
|
+
id: 'html-overlay',
|
|
31
|
+
viewId: null,
|
|
32
|
+
overflowMargin: 0,
|
|
33
|
+
zIndex: 1,
|
|
34
|
+
style: {},
|
|
35
|
+
className: ''
|
|
36
|
+
} satisfies Required<WidgetProps> &
|
|
37
|
+
Required<Pick<HtmlOverlayWidgetProps, 'overflowMargin' | 'zIndex'>> &
|
|
38
|
+
HtmlOverlayWidgetProps;
|
|
39
|
+
|
|
40
|
+
placement: WidgetPlacement = 'fill';
|
|
41
|
+
className = 'deck-widget-html-overlay';
|
|
42
|
+
deck?: Deck | null = null;
|
|
43
|
+
protected viewport: Viewport | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(props: PropsT = {} as PropsT) {
|
|
46
|
+
super({...HtmlOverlayWidget.defaultProps, ...props});
|
|
47
|
+
this.viewId = props.viewId ?? null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override setProps(props: Partial<PropsT>): void {
|
|
51
|
+
if (props.viewId !== undefined) {
|
|
52
|
+
this.viewId = props.viewId;
|
|
53
|
+
}
|
|
54
|
+
super.setProps(props);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override onAdd({deck, viewId}: {deck: Deck; viewId: string | null}): void {
|
|
58
|
+
this.deck = deck;
|
|
59
|
+
if (this.viewId === undefined) {
|
|
60
|
+
this.viewId = viewId;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override onRemove(): void {
|
|
65
|
+
this.deck = null;
|
|
66
|
+
this.viewport = null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override onViewportChange(viewport: Viewport): void {
|
|
70
|
+
if (!this.viewId || this.viewId === viewport.id) {
|
|
71
|
+
this.viewport = viewport;
|
|
72
|
+
this.updateHTML();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
protected getViewport(): Viewport | null {
|
|
77
|
+
return this.viewport;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
protected getZoom(): number {
|
|
81
|
+
return this.viewport?.zoom ?? 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
protected scaleWithZoom(n: number): number {
|
|
85
|
+
return n / Math.pow(2, 20 - this.getZoom());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected breakpointWithZoom<T>(threshold: number, a: T, b: T): T {
|
|
89
|
+
return this.getZoom() > threshold ? a : b;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected getCoords(viewport: Viewport, coordinates: number[]): [number, number] {
|
|
93
|
+
const pos = viewport.project(coordinates);
|
|
94
|
+
if (!pos) return [-1, -1];
|
|
95
|
+
return pos as [number, number];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
protected inView(viewport: Viewport, [x, y]: number[]): boolean {
|
|
99
|
+
const overflowMargin = this.props.overflowMargin ?? 0;
|
|
100
|
+
const {width, height} = viewport;
|
|
101
|
+
return !(
|
|
102
|
+
x < -overflowMargin ||
|
|
103
|
+
y < -overflowMargin ||
|
|
104
|
+
x > width + overflowMargin ||
|
|
105
|
+
y > height + overflowMargin
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
protected getOverlayItems(viewport: Viewport): VNode[] {
|
|
110
|
+
const {items} = this.props;
|
|
111
|
+
return (items ? toChildArray(items) : []) as VNode[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected projectItems(items: VNode[], viewport: Viewport): VNode[] {
|
|
115
|
+
const renderItems: VNode[] = [];
|
|
116
|
+
items
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.forEach((item, index) => {
|
|
119
|
+
const coordinates = (item.props as any)?.coordinates;
|
|
120
|
+
if (!coordinates) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const [x, y] = this.getCoords(viewport, coordinates);
|
|
124
|
+
if (this.inView(viewport, [x, y])) {
|
|
125
|
+
const key = item.key === null || item.key === undefined ? index : item.key;
|
|
126
|
+
renderItems.push(cloneElement(item, {x, y, key}));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return renderItems;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
override onRenderHTML(rootElement: HTMLElement): void {
|
|
134
|
+
Object.assign(rootElement.style, ROOT_STYLE, {zIndex: `${this.props.zIndex ?? 1}`});
|
|
135
|
+
|
|
136
|
+
const viewport = this.getViewport();
|
|
137
|
+
if (!viewport) {
|
|
138
|
+
render(null, rootElement);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const overlayItems = this.getOverlayItems(viewport);
|
|
143
|
+
const renderedItems = this.projectItems(overlayItems, viewport);
|
|
144
|
+
|
|
145
|
+
render(<Fragment>{renderedItems}</Fragment>, rootElement);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {ComponentChildren, VNode} from 'preact';
|
|
6
|
+
import type {PickingInfo, WidgetProps, Viewport} from '@deck.gl/core';
|
|
7
|
+
import {HtmlOverlayItem} from './html-overlay-item';
|
|
8
|
+
import {HtmlOverlayWidget, type HtmlOverlayWidgetProps} from './html-overlay-widget';
|
|
9
|
+
|
|
10
|
+
export type HtmlTooltipWidgetProps = HtmlOverlayWidgetProps & {
|
|
11
|
+
/** Delay before showing the tooltip (ms). */
|
|
12
|
+
showDelay?: number;
|
|
13
|
+
/** Extract a tooltip string or node from picking info. */
|
|
14
|
+
getTooltip?: (pickingInfo: PickingInfo) => ComponentChildren;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const TOOLTIP_STYLE = {
|
|
18
|
+
transform: 'translate(-50%,-100%)',
|
|
19
|
+
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
20
|
+
padding: '4px 8px',
|
|
21
|
+
borderRadius: 8,
|
|
22
|
+
color: 'white'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const SHOW_TOOLTIP_TIMEOUT = 250;
|
|
26
|
+
|
|
27
|
+
function defaultGetTooltip(pickingInfo: PickingInfo): ComponentChildren {
|
|
28
|
+
return pickingInfo.object?.style?.tooltip;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class HtmlTooltipWidget extends HtmlOverlayWidget<HtmlTooltipWidgetProps> {
|
|
32
|
+
static override defaultProps = {
|
|
33
|
+
...HtmlOverlayWidget.defaultProps,
|
|
34
|
+
id: 'html-tooltip-overlay',
|
|
35
|
+
showDelay: SHOW_TOOLTIP_TIMEOUT,
|
|
36
|
+
getTooltip: defaultGetTooltip
|
|
37
|
+
} satisfies Required<WidgetProps> & Required<Pick<HtmlTooltipWidgetProps, 'showDelay' | 'getTooltip'>> &
|
|
38
|
+
HtmlTooltipWidgetProps;
|
|
39
|
+
|
|
40
|
+
private timeoutID: ReturnType<typeof globalThis.setTimeout> | null = null;
|
|
41
|
+
private pickingInfo: PickingInfo | null = null;
|
|
42
|
+
private visible = false;
|
|
43
|
+
|
|
44
|
+
override onRemove(): void {
|
|
45
|
+
if (this.timeoutID !== null) {
|
|
46
|
+
globalThis.clearTimeout(this.timeoutID);
|
|
47
|
+
this.timeoutID = null;
|
|
48
|
+
}
|
|
49
|
+
this.visible = false;
|
|
50
|
+
this.pickingInfo = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override onHover(pickingInfo: PickingInfo): void {
|
|
54
|
+
if (this.timeoutID !== null) {
|
|
55
|
+
globalThis.clearTimeout(this.timeoutID);
|
|
56
|
+
this.timeoutID = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tooltipContent = this.props.getTooltip?.(pickingInfo);
|
|
60
|
+
|
|
61
|
+
if (pickingInfo && tooltipContent) {
|
|
62
|
+
const delay = this.props.showDelay ?? SHOW_TOOLTIP_TIMEOUT;
|
|
63
|
+
this.timeoutID = globalThis.setTimeout(() => {
|
|
64
|
+
this.visible = true;
|
|
65
|
+
this.pickingInfo = pickingInfo;
|
|
66
|
+
this.updateHTML();
|
|
67
|
+
}, delay);
|
|
68
|
+
} else {
|
|
69
|
+
this.visible = false;
|
|
70
|
+
this.pickingInfo = null;
|
|
71
|
+
this.updateHTML();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected override getOverlayItems(viewport: Viewport): VNode[] {
|
|
76
|
+
if (!this.visible || !this.pickingInfo) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const tooltipContent = this.props.getTooltip?.(this.pickingInfo);
|
|
81
|
+
const coordinates =
|
|
82
|
+
this.pickingInfo.coordinate ?? (this.pickingInfo as Partial<{lngLat: number[]}>).lngLat ?? null;
|
|
83
|
+
if (!tooltipContent || !coordinates) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
<HtmlOverlayItem key="tooltip" coordinates={coordinates} style={TOOLTIP_STYLE}>
|
|
89
|
+
{tooltipContent}
|
|
90
|
+
</HtmlOverlayItem>
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {Component, type ComponentChildren} from 'preact';
|
|
6
|
+
|
|
7
|
+
const REPEAT_DELAY_MS = 300;
|
|
8
|
+
const REPEAT_INTERVAL_MS = 100;
|
|
9
|
+
|
|
10
|
+
export type LongPressButtonProps = {
|
|
11
|
+
onClick: () => void;
|
|
12
|
+
children: ComponentChildren;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class LongPressButton extends Component<LongPressButtonProps> {
|
|
16
|
+
buttonPressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
17
|
+
usingPointerEvents = false;
|
|
18
|
+
|
|
19
|
+
private stopEvent(event: Event) {
|
|
20
|
+
event.stopPropagation();
|
|
21
|
+
if (typeof (event as any).stopImmediatePropagation === 'function') {
|
|
22
|
+
(event as any).stopImmediatePropagation();
|
|
23
|
+
}
|
|
24
|
+
if (typeof (event as any).preventDefault === 'function') {
|
|
25
|
+
(event as any).preventDefault();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private repeat = () => {
|
|
30
|
+
if (this.buttonPressTimer) {
|
|
31
|
+
this.props.onClick();
|
|
32
|
+
this.buttonPressTimer = setTimeout(this.repeat, REPEAT_INTERVAL_MS);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
private startPress(event: Event) {
|
|
37
|
+
this.stopEvent(event);
|
|
38
|
+
this.props.onClick();
|
|
39
|
+
this.buttonPressTimer = setTimeout(this.repeat, REPEAT_DELAY_MS);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private endPress(event?: Event) {
|
|
43
|
+
if (event) {
|
|
44
|
+
this.stopEvent(event);
|
|
45
|
+
}
|
|
46
|
+
if (this.buttonPressTimer) {
|
|
47
|
+
clearTimeout(this.buttonPressTimer);
|
|
48
|
+
}
|
|
49
|
+
this.buttonPressTimer = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private handlePointerDown = (event: PointerEvent) => {
|
|
53
|
+
this.usingPointerEvents = true;
|
|
54
|
+
(event.currentTarget as HTMLElement | null)?.setPointerCapture?.(event.pointerId);
|
|
55
|
+
this.startPress(event);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
private handlePointerUp = (event: PointerEvent) => {
|
|
59
|
+
(event.currentTarget as HTMLElement | null)?.releasePointerCapture?.(event.pointerId);
|
|
60
|
+
this.endPress(event);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
private handlePointerCancel = (event: PointerEvent) => {
|
|
64
|
+
(event.currentTarget as HTMLElement | null)?.releasePointerCapture?.(event.pointerId);
|
|
65
|
+
this.endPress(event);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
private handleMouseDown = (event: MouseEvent) => {
|
|
69
|
+
if (this.usingPointerEvents) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.startPress(event);
|
|
73
|
+
document.addEventListener('mouseup', this.handleMouseUp, {once: true});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
private handleMouseUp = (event: MouseEvent) => {
|
|
77
|
+
if (this.usingPointerEvents) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.endPress(event);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
private handleTouchStart = (event: TouchEvent) => {
|
|
84
|
+
if (this.usingPointerEvents) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.startPress(event);
|
|
88
|
+
document.addEventListener('touchend', this.handleTouchEnd, {once: true});
|
|
89
|
+
document.addEventListener('touchcancel', this.handleTouchEnd, {once: true});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
private handleTouchEnd = (event: TouchEvent) => {
|
|
93
|
+
if (this.usingPointerEvents) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.endPress(event);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
render() {
|
|
100
|
+
return (
|
|
101
|
+
<div className="deck-widget-button">
|
|
102
|
+
<div
|
|
103
|
+
style={{pointerEvents: 'auto'}}
|
|
104
|
+
onPointerDown={this.handlePointerDown}
|
|
105
|
+
onPointerUp={this.handlePointerUp}
|
|
106
|
+
onPointerCancel={this.handlePointerCancel}
|
|
107
|
+
onPointerMove={(event) => this.stopEvent(event)}
|
|
108
|
+
onPointerLeave={this.handlePointerCancel}
|
|
109
|
+
onPointerOut={this.handlePointerCancel}
|
|
110
|
+
onMouseDown={this.handleMouseDown}
|
|
111
|
+
onMouseUp={this.handleMouseUp}
|
|
112
|
+
onMouseMove={(event) => this.stopEvent(event)}
|
|
113
|
+
onTouchStart={this.handleTouchStart}
|
|
114
|
+
onTouchEnd={this.handleTouchEnd}
|
|
115
|
+
onTouchMove={(event) => this.stopEvent(event)}
|
|
116
|
+
onContextMenu={(event) => event.preventDefault()}
|
|
117
|
+
onWheel={(event) => this.stopEvent(event)}
|
|
118
|
+
onClick={(event) => this.stopEvent(event)}
|
|
119
|
+
>
|
|
120
|
+
{this.props.children}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
6
|
+
|
|
7
|
+
const REPEAT_DELAY_MS = 300;
|
|
8
|
+
const REPEAT_INTERVAL_MS = 100;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Utility that attaches pointer/mouse/touch handlers to an element and
|
|
12
|
+
* invokes a callback immediately plus while the interaction is held.
|
|
13
|
+
*/
|
|
14
|
+
export class LongPressController {
|
|
15
|
+
private buttonPressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
16
|
+
private usingPointerEvents = false;
|
|
17
|
+
|
|
18
|
+
constructor(private element: HTMLElement, private onActivate: () => void) {
|
|
19
|
+
this.handlePointerDown = this.handlePointerDown.bind(this);
|
|
20
|
+
this.handlePointerUp = this.handlePointerUp.bind(this);
|
|
21
|
+
this.handlePointerCancel = this.handlePointerCancel.bind(this);
|
|
22
|
+
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
23
|
+
this.handleMouseUp = this.handleMouseUp.bind(this);
|
|
24
|
+
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
25
|
+
this.handleTouchStart = this.handleTouchStart.bind(this);
|
|
26
|
+
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
|
27
|
+
this.handleTouchMove = this.handleTouchMove.bind(this);
|
|
28
|
+
|
|
29
|
+
element.addEventListener('pointerdown', this.handlePointerDown);
|
|
30
|
+
element.addEventListener('pointerup', this.handlePointerUp);
|
|
31
|
+
element.addEventListener('pointercancel', this.handlePointerCancel);
|
|
32
|
+
element.addEventListener('pointerleave', this.handlePointerCancel);
|
|
33
|
+
element.addEventListener('pointerout', this.handlePointerCancel);
|
|
34
|
+
element.addEventListener('pointermove', this.handlePointerMove);
|
|
35
|
+
element.addEventListener('mousedown', this.handleMouseDown);
|
|
36
|
+
element.addEventListener('mousemove', this.handleMouseMove);
|
|
37
|
+
element.addEventListener('touchstart', this.handleTouchStart, {passive: false});
|
|
38
|
+
element.addEventListener('touchend', this.handleTouchEnd, {passive: false});
|
|
39
|
+
element.addEventListener('touchcancel', this.handleTouchEnd, {passive: false});
|
|
40
|
+
element.addEventListener('touchmove', this.handleTouchMove, {passive: false});
|
|
41
|
+
element.addEventListener('contextmenu', (event) => event.preventDefault());
|
|
42
|
+
element.addEventListener('wheel', (event) => this.stopEvent(event));
|
|
43
|
+
element.addEventListener('click', (event) => this.stopEvent(event));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
destroy(): void {
|
|
47
|
+
this.endPress();
|
|
48
|
+
this.element.removeEventListener('pointerdown', this.handlePointerDown);
|
|
49
|
+
this.element.removeEventListener('pointerup', this.handlePointerUp);
|
|
50
|
+
this.element.removeEventListener('pointercancel', this.handlePointerCancel);
|
|
51
|
+
this.element.removeEventListener('pointerleave', this.handlePointerCancel);
|
|
52
|
+
this.element.removeEventListener('pointerout', this.handlePointerCancel);
|
|
53
|
+
this.element.removeEventListener('pointermove', this.handlePointerMove);
|
|
54
|
+
this.element.removeEventListener('mousedown', this.handleMouseDown);
|
|
55
|
+
this.element.removeEventListener('mousemove', this.handleMouseMove);
|
|
56
|
+
this.element.removeEventListener('touchstart', this.handleTouchStart);
|
|
57
|
+
this.element.removeEventListener('touchend', this.handleTouchEnd);
|
|
58
|
+
this.element.removeEventListener('touchcancel', this.handleTouchEnd);
|
|
59
|
+
this.element.removeEventListener('touchmove', this.handleTouchMove);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private repeat = () => {
|
|
63
|
+
if (this.buttonPressTimer) {
|
|
64
|
+
this.onActivate();
|
|
65
|
+
this.buttonPressTimer = setTimeout(this.repeat, REPEAT_INTERVAL_MS);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
private startPress(event: Event) {
|
|
70
|
+
this.stopEvent(event);
|
|
71
|
+
this.onActivate();
|
|
72
|
+
this.buttonPressTimer = setTimeout(this.repeat, REPEAT_DELAY_MS);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private endPress(event?: Event) {
|
|
76
|
+
if (event) {
|
|
77
|
+
this.stopEvent(event);
|
|
78
|
+
}
|
|
79
|
+
if (this.buttonPressTimer) {
|
|
80
|
+
clearTimeout(this.buttonPressTimer);
|
|
81
|
+
this.buttonPressTimer = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private handlePointerDown(event: PointerEvent) {
|
|
86
|
+
this.usingPointerEvents = true;
|
|
87
|
+
(event.currentTarget as HTMLElement | null)?.setPointerCapture?.(event.pointerId);
|
|
88
|
+
this.startPress(event);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private handlePointerUp(event: PointerEvent) {
|
|
92
|
+
(event.currentTarget as HTMLElement | null)?.releasePointerCapture?.(event.pointerId);
|
|
93
|
+
this.endPress(event);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private handlePointerCancel(event: PointerEvent) {
|
|
97
|
+
(event.currentTarget as HTMLElement | null)?.releasePointerCapture?.(event.pointerId);
|
|
98
|
+
this.endPress(event);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private handlePointerMove = (event: Event) => {
|
|
102
|
+
this.stopEvent(event);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
private handleMouseDown(event: MouseEvent) {
|
|
106
|
+
if (this.usingPointerEvents) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.startPress(event);
|
|
110
|
+
document.addEventListener('mouseup', this.handleMouseUp, {once: true});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private handleMouseUp(event: MouseEvent) {
|
|
114
|
+
if (this.usingPointerEvents) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.endPress(event);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private handleMouseMove(event: MouseEvent) {
|
|
121
|
+
if (this.usingPointerEvents) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.stopEvent(event);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private handleTouchStart(event: TouchEvent) {
|
|
128
|
+
if (this.usingPointerEvents) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.startPress(event);
|
|
132
|
+
document.addEventListener('touchend', this.handleTouchEnd, {once: true});
|
|
133
|
+
document.addEventListener('touchcancel', this.handleTouchEnd, {once: true});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private handleTouchEnd(event: TouchEvent) {
|
|
137
|
+
if (this.usingPointerEvents) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.endPress(event);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private handleTouchMove(event: TouchEvent) {
|
|
144
|
+
if (this.usingPointerEvents) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.stopEvent(event);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private stopEvent(event: Event) {
|
|
151
|
+
event.stopPropagation();
|
|
152
|
+
if (typeof (event as any).stopImmediatePropagation === 'function') {
|
|
153
|
+
(event as any).stopImmediatePropagation();
|
|
154
|
+
}
|
|
155
|
+
if (typeof (event as any).preventDefault === 'function') {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|