refactor(packages): change package structure

This commit is contained in:
moklick
2023-06-05 15:40:18 +02:00
parent b2e296901e
commit 8354759f57
332 changed files with 980 additions and 3244 deletions
+224
View File
@@ -0,0 +1,224 @@
import { type ZoomTransform, zoom, zoomTransform } from 'd3-zoom';
import { select } from 'd3-selection';
import {
type CoordinateExtent,
type Viewport,
PanZoomTransformOptions,
PanZoomUpdateOptions,
PanZoomParams,
PanZoomInstance,
} from '../types';
import { clamp } from '../utils';
import { getD3Transition, viewportToTransform } from './utils';
import {
createPanOnScrollHandler,
createPanZoomEndHandler,
createPanZoomHandler,
createPanZoomStartHandler,
createZoomOnScrollHandler,
} from './eventhandler';
import { createFilter } from './filter';
export type ZoomPanValues = {
isZoomingOrPanning: boolean;
usedRightMouseButton: boolean;
prevViewport: Viewport;
mouseButton: number;
timerId: ReturnType<typeof setTimeout> | undefined;
};
export function XYPanZoom({
domNode,
minZoom,
maxZoom,
translateExtent,
viewport,
onPanZoom,
onPanZoomStart,
onPanZoomEnd,
onTransformChange,
onDraggingChange,
}: PanZoomParams): PanZoomInstance {
const zoomPanValues: ZoomPanValues = {
isZoomingOrPanning: false,
usedRightMouseButton: false,
prevViewport: { x: 0, y: 0, zoom: 0 },
mouseButton: 0,
timerId: undefined,
};
const bbox = domNode.getBoundingClientRect();
const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent);
const d3Selection = select(domNode).call(d3ZoomInstance);
setViewportConstrained(
{
x: viewport.x,
y: viewport.y,
zoom: clamp(viewport.zoom, minZoom, maxZoom),
},
[
[0, 0],
[bbox.width, bbox.height],
],
translateExtent
);
const d3ZoomHandler = d3Selection.on('wheel.zoom')!;
function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) {
if (d3Selection) {
d3ZoomInstance?.transform(getD3Transition(d3Selection, options?.duration), transform);
}
}
// public functions
function update({
noWheelClassName,
noPanClassName,
onPaneContextMenu,
userSelectionActive,
panOnScroll,
panOnDrag,
panOnScrollMode,
panOnScrollSpeed,
preventScrolling,
zoomOnPinch,
zoomOnScroll,
zoomOnDoubleClick,
zoomActivationKeyPressed,
lib,
}: PanZoomUpdateOptions) {
if (userSelectionActive && !zoomPanValues.isZoomingOrPanning) {
destroy();
}
const isPanOnScroll = panOnScroll && !zoomActivationKeyPressed && !userSelectionActive;
const wheelHandler = isPanOnScroll
? createPanOnScrollHandler({
noWheelClassName,
d3Selection,
d3Zoom: d3ZoomInstance,
panOnScrollMode,
panOnScrollSpeed,
zoomOnPinch,
})
: createZoomOnScrollHandler({
noWheelClassName,
preventScrolling,
d3ZoomHandler,
});
d3Selection.on('wheel.zoom', wheelHandler, { passive: false });
if (!userSelectionActive) {
// pan zoom start
const startHandler = createPanZoomStartHandler({
zoomPanValues,
onDraggingChange,
onPanZoomStart,
});
d3ZoomInstance.on('start', startHandler);
// pan zoom
const panZoomHandler = createPanZoomHandler({
zoomPanValues,
panOnDrag,
onPaneContextMenu: !!onPaneContextMenu,
onPanZoom,
onTransformChange,
});
d3ZoomInstance.on('zoom', panZoomHandler);
// pan zoom end
const panZoomEndHandler = createPanZoomEndHandler({
zoomPanValues,
panOnDrag,
panOnScroll,
onPaneContextMenu,
onPanZoomEnd,
onDraggingChange,
});
d3ZoomInstance.on('end', panZoomEndHandler);
}
const filter = createFilter({
zoomActivationKeyPressed,
panOnDrag,
zoomOnScroll,
panOnScroll,
zoomOnDoubleClick,
zoomOnPinch,
userSelectionActive,
noPanClassName,
noWheelClassName,
lib,
});
d3ZoomInstance.filter(filter);
}
function destroy() {
d3ZoomInstance.on('zoom', null);
}
function setViewportConstrained(
viewport: Viewport,
extent: CoordinateExtent,
translateExtent: CoordinateExtent
): ZoomTransform | undefined {
const nextTransform = viewportToTransform(viewport);
const contrainedTransform = d3ZoomInstance?.constrain()(nextTransform, extent, translateExtent);
if (contrainedTransform) {
setTransform(contrainedTransform);
}
return contrainedTransform;
}
function setViewport(viewport: Viewport, options?: PanZoomTransformOptions) {
const nextTransform = viewportToTransform(viewport);
setTransform(nextTransform, options);
return nextTransform;
}
function getViewport(): Viewport {
const transform = d3Selection ? zoomTransform(d3Selection.node() as Element) : { x: 0, y: 0, k: 1 };
return { x: transform.x, y: transform.y, zoom: transform.k };
}
function scaleTo(zoom: number, options?: PanZoomTransformOptions) {
if (d3Selection) {
d3ZoomInstance?.scaleTo(getD3Transition(d3Selection, options?.duration), zoom);
}
}
function scaleBy(factor: number, options?: PanZoomTransformOptions) {
if (d3Selection) {
d3ZoomInstance?.scaleBy(getD3Transition(d3Selection, options?.duration), factor);
}
}
function setScaleExtent(scaleExtent: [number, number]) {
d3ZoomInstance?.scaleExtent(scaleExtent);
}
function setTranslateExtent(translateExtent: CoordinateExtent) {
d3ZoomInstance?.translateExtent(translateExtent);
}
return {
update,
destroy,
setViewport,
setViewportConstrained,
getViewport,
scaleTo,
scaleBy,
setScaleExtent,
setTranslateExtent,
};
}
@@ -0,0 +1,192 @@
import type { D3ZoomEvent } from 'd3-zoom';
import { pointer } from 'd3-selection';
import {
PanOnScrollMode,
type D3SelectionInstance,
type D3ZoomHandler,
type D3ZoomInstance,
type OnPanZoom,
type Viewport,
type OnDraggingChange,
type OnTransformChange,
} from '../types';
import { isRightClickPan, isWrappedWithClass, transformToViewport, viewChanged } from './utils';
export type ZoomPanValues = {
isZoomingOrPanning: boolean;
usedRightMouseButton: boolean;
prevViewport: Viewport;
mouseButton: number;
timerId: ReturnType<typeof setTimeout> | undefined;
};
export type PanOnScrollParams = {
noWheelClassName: string;
d3Selection: D3SelectionInstance;
d3Zoom: D3ZoomInstance;
panOnScrollMode: PanOnScrollMode;
panOnScrollSpeed: number;
zoomOnPinch: boolean;
};
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({
noWheelClassName,
d3Selection,
d3Zoom,
panOnScrollMode,
panOnScrollSpeed,
zoomOnPinch,
}: PanOnScrollParams) {
return (event: any) => {
if (isWrappedWithClass(event, noWheelClassName)) {
return false;
}
event.preventDefault();
event.stopImmediatePropagation();
const currentZoom = d3Selection.property('__zoom').k || 1;
if (event.ctrlKey && zoomOnPinch) {
const point = pointer(event);
// taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10;
const zoom = currentZoom * Math.pow(2, pinchDelta);
d3Zoom.scaleTo(d3Selection, zoom, point);
return;
}
// increase scroll speed in firefox
// firefox: deltaMode === 1; chrome: deltaMode === 0
const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
const deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
const deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
d3Zoom.translateBy(
d3Selection,
-(deltaX / currentZoom) * panOnScrollSpeed,
-(deltaY / currentZoom) * panOnScrollSpeed
);
};
}
export function createZoomOnScrollHandler({ noWheelClassName, preventScrolling, d3ZoomHandler }: ZoomOnScrollParams) {
return function (this: Element, event: any, d: unknown) {
if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) {
return null;
}
event.preventDefault();
d3ZoomHandler.call(this, event, d);
};
}
export function createPanZoomStartHandler({ zoomPanValues, onDraggingChange, onPanZoomStart }: PanZoomStartParams) {
return (event: D3ZoomEvent<HTMLDivElement, any>) => {
// we need to remember it here, because it's always 0 in the "zoom" event
zoomPanValues.mouseButton = event.sourceEvent?.button || 0;
zoomPanValues.isZoomingOrPanning = true;
if (event.sourceEvent?.type === 'mousedown') {
onDraggingChange(true);
}
if (onPanZoomStart) {
const viewport = transformToViewport(event.transform);
zoomPanValues.prevViewport = viewport;
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)
);
onTransformChange([event.transform.x, event.transform.y, event.transform.k]);
if (onPanZoom) {
onPanZoom?.(event.sourceEvent as MouseEvent | TouchEvent, transformToViewport(event.transform));
}
};
}
export function createPanZoomEndHandler({
zoomPanValues,
panOnDrag,
panOnScroll,
onDraggingChange,
onPanZoomEnd,
onPaneContextMenu,
}: PanZoomEndParams) {
return (event: D3ZoomEvent<HTMLDivElement, any>) => {
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
);
}
};
}
+95
View File
@@ -0,0 +1,95 @@
import { isWrappedWithClass } from './utils';
export type FilterParams = {
zoomActivationKeyPressed: boolean;
zoomOnScroll: boolean;
zoomOnPinch: boolean;
panOnDrag: boolean | number[];
panOnScroll: boolean;
zoomOnDoubleClick: boolean;
userSelectionActive: boolean;
noWheelClassName: string;
noPanClassName: string;
lib: string;
};
export function createFilter({
zoomActivationKeyPressed,
zoomOnScroll,
zoomOnPinch,
panOnDrag,
panOnScroll,
zoomOnDoubleClick,
userSelectionActive,
noWheelClassName,
noPanClassName,
lib,
}: FilterParams) {
return (event: any): boolean => {
const zoomScroll = zoomActivationKeyPressed || zoomOnScroll;
const pinchZoom = zoomOnPinch && event.ctrlKey;
if (
event.button === 1 &&
event.type === 'mousedown' &&
(isWrappedWithClass(event, `${lib}-flow__node`) || isWrappedWithClass(event, `${lib}-flow__edge`))
) {
return true;
}
// if all interactions are disabled, we prevent all zoom events
if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) {
return false;
}
// during a selection we prevent all other interactions
if (userSelectionActive) {
return false;
}
// if zoom on double click is disabled, we prevent the double click event
if (!zoomOnDoubleClick && event.type === 'dblclick') {
return false;
}
// if the target element is inside an element with the nowheel class, we prevent zooming
if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') {
return false;
}
// if the target element is inside an element with the nopan class, we prevent panning
if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') {
return false;
}
if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') {
return false;
}
// when there is no scroll handling enabled, we prevent all wheel events
if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') {
return false;
}
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) {
return false;
}
// if the pane is only movable using allowed clicks
if (
Array.isArray(panOnDrag) &&
!panOnDrag.includes(event.button) &&
(event.type === 'mousedown' || event.type === 'touchstart')
) {
return false;
}
// We only allow right clicks if pan on drag is set to right click
const buttonAllowed =
(Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) || !event.button || event.button <= 1;
// default filter for d3-zoom
return (!event.ctrlKey || event.type === 'wheel') && buttonAllowed;
};
}
+1
View File
@@ -0,0 +1 @@
export * from './XYPanZoom';
+23
View File
@@ -0,0 +1,23 @@
import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
import { type D3SelectionInstance, type Viewport } from '../types';
export const viewChanged = (prevViewport: Viewport, eventViewport: any): boolean =>
prevViewport.x !== eventViewport.x || prevViewport.y !== eventViewport.y || prevViewport.zoom !== eventViewport.k;
export const transformToViewport = (transform: ZoomTransform): Viewport => ({
x: transform.x,
y: transform.y,
zoom: transform.k,
});
export const viewportToTransform = ({ x, y, zoom }: Viewport): ZoomTransform =>
zoomIdentity.translate(x, y).scale(zoom);
export const isWrappedWithClass = (event: any, className: string | undefined) => event.target.closest(`.${className}`);
export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: number) =>
usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2);
export const getD3Transition = (selection: D3SelectionInstance, duration = 0) =>
typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration) : selection;