@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.
Files changed (65) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +43 -0
  3. package/dist/_deprecate/long-press-button.d.ts +13 -0
  4. package/dist/_deprecate/long-press-button.d.ts.map +1 -0
  5. package/dist/_deprecate/long-press-button.js +32 -0
  6. package/dist/_deprecate/long-press-button.js.map +1 -0
  7. package/dist/_deprecate/view-control-widget.d.ts +78 -0
  8. package/dist/_deprecate/view-control-widget.d.ts.map +1 -0
  9. package/dist/_deprecate/view-control-widget.js +198 -0
  10. package/dist/_deprecate/view-control-widget.js.map +1 -0
  11. package/dist/index.cjs +708 -0
  12. package/dist/index.cjs.map +7 -0
  13. package/dist/index.d.ts +13 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +10 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/widgets/html-cluster-widget.d.ts +25 -0
  18. package/dist/widgets/html-cluster-widget.d.ts.map +1 -0
  19. package/dist/widgets/html-cluster-widget.js +39 -0
  20. package/dist/widgets/html-cluster-widget.js.map +1 -0
  21. package/dist/widgets/html-overlay-item.d.ts +13 -0
  22. package/dist/widgets/html-overlay-item.d.ts.map +1 -0
  23. package/dist/widgets/html-overlay-item.js +10 -0
  24. package/dist/widgets/html-overlay-item.js.map +1 -0
  25. package/dist/widgets/html-overlay-widget.d.ts +45 -0
  26. package/dist/widgets/html-overlay-widget.d.ts.map +1 -0
  27. package/dist/widgets/html-overlay-widget.js +112 -0
  28. package/dist/widgets/html-overlay-widget.js.map +1 -0
  29. package/dist/widgets/html-tooltip-widget.d.ts +30 -0
  30. package/dist/widgets/html-tooltip-widget.d.ts.map +1 -0
  31. package/dist/widgets/html-tooltip-widget.js +67 -0
  32. package/dist/widgets/html-tooltip-widget.js.map +1 -0
  33. package/dist/widgets/long-press-button.d.ts +22 -0
  34. package/dist/widgets/long-press-button.d.ts.map +1 -0
  35. package/dist/widgets/long-press-button.js +84 -0
  36. package/dist/widgets/long-press-button.js.map +1 -0
  37. package/dist/widgets/long-press-controller.d.ts +27 -0
  38. package/dist/widgets/long-press-controller.d.ts.map +1 -0
  39. package/dist/widgets/long-press-controller.js +144 -0
  40. package/dist/widgets/long-press-controller.js.map +1 -0
  41. package/dist/widgets/pan-widget.d.ts +33 -0
  42. package/dist/widgets/pan-widget.d.ts.map +1 -0
  43. package/dist/widgets/pan-widget.js +141 -0
  44. package/dist/widgets/pan-widget.js.map +1 -0
  45. package/dist/widgets/view-manager-utils.d.ts +11 -0
  46. package/dist/widgets/view-manager-utils.d.ts.map +1 -0
  47. package/dist/widgets/view-manager-utils.js +13 -0
  48. package/dist/widgets/view-manager-utils.js.map +1 -0
  49. package/dist/widgets/zoom-range-widget.d.ts +43 -0
  50. package/dist/widgets/zoom-range-widget.d.ts.map +1 -0
  51. package/dist/widgets/zoom-range-widget.js +190 -0
  52. package/dist/widgets/zoom-range-widget.js.map +1 -0
  53. package/package.json +41 -0
  54. package/src/_deprecate/long-press-button.tsx +50 -0
  55. package/src/_deprecate/view-control-widget.tsx +339 -0
  56. package/src/index.ts +18 -0
  57. package/src/widgets/html-cluster-widget.ts +84 -0
  58. package/src/widgets/html-overlay-item.tsx +32 -0
  59. package/src/widgets/html-overlay-widget.tsx +147 -0
  60. package/src/widgets/html-tooltip-widget.tsx +93 -0
  61. package/src/widgets/long-press-button.tsx +125 -0
  62. package/src/widgets/long-press-controller.ts +159 -0
  63. package/src/widgets/pan-widget.tsx +182 -0
  64. package/src/widgets/view-manager-utils.ts +24 -0
  65. 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
+ }