244 lines
6.9 KiB
TypeScript
244 lines
6.9 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
import type { D3ZoomEvent } from 'd3-zoom';
|
|
import { pointer } from 'd3-selection';
|
|
|
|
import {
|
|
PanOnScrollMode,
|
|
type D3SelectionInstance,
|
|
type D3ZoomHandler,
|
|
type D3ZoomInstance,
|
|
type OnPanZoom,
|
|
type OnDraggingChange,
|
|
type OnTransformChange,
|
|
} from '../types';
|
|
import { isRightClickPan, isWrappedWithClass, transformToViewport, viewChanged, wheelDelta } from './utils';
|
|
import { isMacOs } from '../utils';
|
|
import { ZoomPanValues } from './XYPanZoom';
|
|
|
|
export type PanOnScrollParams = {
|
|
zoomPanValues: ZoomPanValues;
|
|
noWheelClassName: string;
|
|
d3Selection: D3SelectionInstance;
|
|
d3Zoom: D3ZoomInstance;
|
|
panOnScrollMode: PanOnScrollMode;
|
|
panOnScrollSpeed: number;
|
|
zoomOnPinch: boolean;
|
|
onPanZoomStart?: OnPanZoom;
|
|
onPanZoom?: OnPanZoom;
|
|
onPanZoomEnd?: OnPanZoom;
|
|
};
|
|
|
|
export type ZoomOnScrollParams = {
|
|
noWheelClassName: string;
|
|
preventScrolling: boolean;
|
|
d3ZoomHandler: D3ZoomHandler;
|
|
};
|
|
|
|
export type PanZoomStartParams = {
|
|
zoomPanValues: ZoomPanValues;
|
|
onDraggingChange: OnDraggingChange;
|
|
onPanZoomStart?: OnPanZoom;
|
|
};
|
|
|
|
export type PanZoomParams = {
|
|
zoomPanValues: ZoomPanValues;
|
|
panOnDrag: boolean | number[];
|
|
onPaneContextMenu: boolean;
|
|
onTransformChange: OnTransformChange;
|
|
onPanZoom?: OnPanZoom;
|
|
};
|
|
|
|
export type PanZoomEndParams = {
|
|
zoomPanValues: ZoomPanValues;
|
|
panOnDrag: boolean | number[];
|
|
panOnScroll: boolean;
|
|
onDraggingChange: (isDragging: boolean) => void;
|
|
onPanZoomEnd?: OnPanZoom;
|
|
onPaneContextMenu?: (event: any) => void;
|
|
};
|
|
|
|
export function createPanOnScrollHandler({
|
|
zoomPanValues,
|
|
noWheelClassName,
|
|
d3Selection,
|
|
d3Zoom,
|
|
panOnScrollMode,
|
|
panOnScrollSpeed,
|
|
zoomOnPinch,
|
|
onPanZoomStart,
|
|
onPanZoom,
|
|
onPanZoomEnd,
|
|
}: PanOnScrollParams) {
|
|
return (event: any) => {
|
|
if (isWrappedWithClass(event, noWheelClassName)) {
|
|
return false;
|
|
}
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
|
|
const currentZoom = d3Selection.property('__zoom').k || 1;
|
|
|
|
// macos sets ctrlKey=true for pinch gesture on a trackpad
|
|
if (event.ctrlKey && zoomOnPinch) {
|
|
const point = pointer(event);
|
|
const pinchDelta = wheelDelta(event);
|
|
const zoom = currentZoom * Math.pow(2, pinchDelta);
|
|
// @ts-ignore
|
|
d3Zoom.scaleTo(d3Selection, zoom, point, event);
|
|
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* increase scroll speed in firefox
|
|
* firefox: deltaMode === 1; chrome: deltaMode === 0
|
|
*/
|
|
const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
|
|
let deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
|
|
let deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
|
|
|
|
// this enables vertical scrolling with shift + scroll on windows
|
|
if (!isMacOs() && event.shiftKey && panOnScrollMode !== PanOnScrollMode.Vertical) {
|
|
deltaX = event.deltaY * deltaNormalize;
|
|
deltaY = 0;
|
|
}
|
|
|
|
d3Zoom.translateBy(
|
|
d3Selection,
|
|
-(deltaX / currentZoom) * panOnScrollSpeed,
|
|
-(deltaY / currentZoom) * panOnScrollSpeed,
|
|
// @ts-ignore
|
|
{ internal: true }
|
|
);
|
|
|
|
const nextViewport = transformToViewport(d3Selection.property('__zoom'));
|
|
|
|
clearTimeout(zoomPanValues.panScrollTimeout);
|
|
|
|
/*
|
|
* for pan on scroll we need to handle the event calls on our own
|
|
* we can't use the start, zoom and end events from d3-zoom
|
|
* because start and move gets called on every scroll event and not once at the beginning
|
|
*/
|
|
if (!zoomPanValues.isPanScrolling) {
|
|
zoomPanValues.isPanScrolling = true;
|
|
|
|
onPanZoomStart?.(event, nextViewport);
|
|
}
|
|
|
|
if (zoomPanValues.isPanScrolling) {
|
|
onPanZoom?.(event, nextViewport);
|
|
|
|
zoomPanValues.panScrollTimeout = setTimeout(() => {
|
|
onPanZoomEnd?.(event, nextViewport);
|
|
|
|
zoomPanValues.isPanScrolling = false;
|
|
}, 150);
|
|
}
|
|
};
|
|
}
|
|
|
|
export function createZoomOnScrollHandler({ noWheelClassName, preventScrolling, d3ZoomHandler }: ZoomOnScrollParams) {
|
|
return function (this: Element, event: any, d: unknown) {
|
|
// we still want to enable pinch zooming even if preventScrolling is set to false
|
|
const preventZoom = !preventScrolling && event.type === 'wheel' && !event.ctrlKey;
|
|
|
|
if (preventZoom || isWrappedWithClass(event, noWheelClassName)) {
|
|
return null;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
d3ZoomHandler.call(this, event, d);
|
|
};
|
|
}
|
|
|
|
export function createPanZoomStartHandler({ zoomPanValues, onDraggingChange, onPanZoomStart }: PanZoomStartParams) {
|
|
return (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
|
if (event.sourceEvent?.internal) {
|
|
return;
|
|
}
|
|
|
|
const viewport = transformToViewport(event.transform);
|
|
|
|
// we need to remember it here, because it's always 0 in the "zoom" event
|
|
zoomPanValues.mouseButton = event.sourceEvent?.button || 0;
|
|
zoomPanValues.isZoomingOrPanning = true;
|
|
zoomPanValues.prevViewport = viewport;
|
|
|
|
if (event.sourceEvent?.type === 'mousedown') {
|
|
onDraggingChange(true);
|
|
}
|
|
|
|
if (onPanZoomStart) {
|
|
onPanZoomStart?.(event.sourceEvent as MouseEvent | TouchEvent, viewport);
|
|
}
|
|
};
|
|
}
|
|
|
|
export function createPanZoomHandler({
|
|
zoomPanValues,
|
|
panOnDrag,
|
|
onPaneContextMenu,
|
|
onTransformChange,
|
|
onPanZoom,
|
|
}: PanZoomParams) {
|
|
return (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
|
zoomPanValues.usedRightMouseButton = !!(
|
|
onPaneContextMenu && isRightClickPan(panOnDrag, zoomPanValues.mouseButton ?? 0)
|
|
);
|
|
|
|
if (!event.sourceEvent?.sync) {
|
|
onTransformChange([event.transform.x, event.transform.y, event.transform.k]);
|
|
}
|
|
|
|
if (onPanZoom && !event.sourceEvent?.internal) {
|
|
onPanZoom?.(event.sourceEvent as MouseEvent | TouchEvent, transformToViewport(event.transform));
|
|
}
|
|
};
|
|
}
|
|
|
|
export function createPanZoomEndHandler({
|
|
zoomPanValues,
|
|
panOnDrag,
|
|
panOnScroll,
|
|
onDraggingChange,
|
|
onPanZoomEnd,
|
|
onPaneContextMenu,
|
|
}: PanZoomEndParams) {
|
|
return (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
|
if (event.sourceEvent?.internal) {
|
|
return;
|
|
}
|
|
|
|
zoomPanValues.isZoomingOrPanning = false;
|
|
|
|
if (
|
|
onPaneContextMenu &&
|
|
isRightClickPan(panOnDrag, zoomPanValues.mouseButton ?? 0) &&
|
|
!zoomPanValues.usedRightMouseButton &&
|
|
event.sourceEvent
|
|
) {
|
|
onPaneContextMenu(event.sourceEvent);
|
|
}
|
|
zoomPanValues.usedRightMouseButton = false;
|
|
|
|
onDraggingChange(false);
|
|
|
|
if (onPanZoomEnd && viewChanged(zoomPanValues.prevViewport, event.transform)) {
|
|
const viewport = transformToViewport(event.transform);
|
|
zoomPanValues.prevViewport = viewport;
|
|
|
|
clearTimeout(zoomPanValues.timerId);
|
|
zoomPanValues.timerId = setTimeout(
|
|
() => {
|
|
onPanZoomEnd?.(event.sourceEvent as MouseEvent | TouchEvent, viewport);
|
|
},
|
|
// we need a setTimeout for panOnScroll to supress multiple end events fired during scroll
|
|
panOnScroll ? 150 : 0
|
|
);
|
|
}
|
|
};
|
|
}
|