88 lines
2.9 KiB
TypeScript
88 lines
2.9 KiB
TypeScript
import type { Transform, XYPosition, SnapGrid, Dimensions, Position, Handle } from '../types';
|
|
import { snapPosition, pointToRendererPoint } from './general';
|
|
|
|
export type GetPointerPositionParams = {
|
|
transform: Transform;
|
|
snapGrid?: SnapGrid;
|
|
snapToGrid?: boolean;
|
|
};
|
|
|
|
export function getPointerPosition(
|
|
event: MouseEvent | TouchEvent,
|
|
{ snapGrid = [0, 0], snapToGrid = false, transform }: GetPointerPositionParams
|
|
): XYPosition & { xSnapped: number; ySnapped: number } {
|
|
const { x, y } = getEventPosition(event);
|
|
const pointerPos = pointToRendererPoint({ x, y }, transform);
|
|
const { x: xSnapped, y: ySnapped } = snapToGrid ? snapPosition(pointerPos, snapGrid) : pointerPos;
|
|
|
|
// we need the snapped position in order to be able to skip unnecessary drag events
|
|
return {
|
|
xSnapped,
|
|
ySnapped,
|
|
...pointerPos,
|
|
};
|
|
}
|
|
|
|
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
|
|
width: node.offsetWidth,
|
|
height: node.offsetHeight,
|
|
});
|
|
|
|
export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
|
|
(element.getRootNode?.() as Document | ShadowRoot) || window?.document;
|
|
|
|
const inputTags = ['INPUT', 'SELECT', 'TEXTAREA'];
|
|
|
|
export function isInputDOMNode(event: KeyboardEvent): boolean {
|
|
// using composed path for handling shadow dom
|
|
const target = (event.composedPath?.()?.[0] || event.target) as HTMLElement;
|
|
const isInput = inputTags.includes(target?.nodeName) || target?.hasAttribute('contenteditable');
|
|
|
|
// when an input field is focused we don't want to trigger deletion or movement of nodes
|
|
return isInput || !!target?.closest('.nokey');
|
|
}
|
|
|
|
export const isMouseEvent = (event: MouseEvent | TouchEvent): event is MouseEvent => 'clientX' in event;
|
|
|
|
export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRect) => {
|
|
const isMouse = isMouseEvent(event);
|
|
const evtX = isMouse ? event.clientX : event.touches?.[0].clientX;
|
|
const evtY = isMouse ? event.clientY : event.touches?.[0].clientY;
|
|
|
|
return {
|
|
x: evtX - (bounds?.left ?? 0),
|
|
y: evtY - (bounds?.top ?? 0),
|
|
};
|
|
};
|
|
|
|
// The handle bounds are calculated relative to the node element.
|
|
// We store them in the internals object of the node in order to avoid
|
|
// unnecessary recalculations.
|
|
export const getHandleBounds = (
|
|
type: 'source' | 'target',
|
|
nodeElement: HTMLDivElement,
|
|
nodeBounds: DOMRect,
|
|
zoom: number,
|
|
nodeId: string
|
|
): Handle[] | null => {
|
|
const handles = nodeElement.querySelectorAll(`.${type}`);
|
|
|
|
if (!handles || !handles.length) {
|
|
return null;
|
|
}
|
|
|
|
return Array.from(handles).map((handle): Handle => {
|
|
const handleBounds = handle.getBoundingClientRect();
|
|
|
|
return {
|
|
id: handle.getAttribute('data-handleid'),
|
|
type,
|
|
nodeId,
|
|
position: handle.getAttribute('data-handlepos') as unknown as Position,
|
|
x: (handleBounds.left - nodeBounds.left) / zoom,
|
|
y: (handleBounds.top - nodeBounds.top) / zoom,
|
|
...getDimensions(handle as HTMLDivElement),
|
|
};
|
|
});
|
|
};
|