169 lines
4.1 KiB
TypeScript
169 lines
4.1 KiB
TypeScript
export type RuntimeInputDeviceKind = 'pointer' | 'mocap' | 'keyboard' | 'unknown';
|
|
|
|
export type RuntimeInputPoint = {
|
|
clientX: number;
|
|
clientY: number;
|
|
normalizedX?: number;
|
|
normalizedY?: number;
|
|
};
|
|
|
|
export type RuntimeDragInputSession<TTargetId extends string = string> = {
|
|
targetId: TTargetId;
|
|
inputId: string;
|
|
deviceKind: RuntimeInputDeviceKind;
|
|
startPoint: RuntimeInputPoint;
|
|
currentPoint: RuntimeInputPoint;
|
|
dragging: boolean;
|
|
forceDrop: boolean;
|
|
};
|
|
|
|
export type RuntimeDragInputPress<TTargetId extends string = string> = {
|
|
targetId: TTargetId;
|
|
inputId: string;
|
|
deviceKind: RuntimeInputDeviceKind;
|
|
point: RuntimeInputPoint;
|
|
};
|
|
|
|
export type RuntimeDragInputMove = {
|
|
inputId: string;
|
|
point: RuntimeInputPoint;
|
|
forceDragging?: boolean;
|
|
};
|
|
|
|
export type RuntimeDragInputRelease = {
|
|
inputId: string;
|
|
point: RuntimeInputPoint;
|
|
forceDrop?: boolean;
|
|
};
|
|
|
|
export type RuntimeDragInputControllerOptions<
|
|
TTargetId extends string = string,
|
|
> = {
|
|
dragThresholdPx?: number;
|
|
onPress?: (session: RuntimeDragInputSession<TTargetId>) => void;
|
|
onDragStart?: (session: RuntimeDragInputSession<TTargetId>) => void;
|
|
onDragMove?: (session: RuntimeDragInputSession<TTargetId>) => void;
|
|
onDrop?: (session: RuntimeDragInputSession<TTargetId>) => void;
|
|
onTap?: (session: RuntimeDragInputSession<TTargetId>) => void;
|
|
onCancel?: (session: RuntimeDragInputSession<TTargetId>) => void;
|
|
};
|
|
|
|
const DEFAULT_DRAG_THRESHOLD_PX = 8;
|
|
|
|
function clonePoint(point: RuntimeInputPoint): RuntimeInputPoint {
|
|
return { ...point };
|
|
}
|
|
|
|
function shouldStartDragging(
|
|
session: RuntimeDragInputSession,
|
|
point: RuntimeInputPoint,
|
|
thresholdPx: number,
|
|
forceDragging = false,
|
|
) {
|
|
if (session.dragging || forceDragging) {
|
|
return true;
|
|
}
|
|
|
|
return (
|
|
Math.hypot(
|
|
point.clientX - session.startPoint.clientX,
|
|
point.clientY - session.startPoint.clientY,
|
|
) >= thresholdPx
|
|
);
|
|
}
|
|
|
|
export function createRuntimeDragInputController<
|
|
TTargetId extends string = string,
|
|
>(initialOptions: RuntimeDragInputControllerOptions<TTargetId> = {}) {
|
|
let options = initialOptions;
|
|
let session: RuntimeDragInputSession<TTargetId> | null = null;
|
|
|
|
const setOptions = (
|
|
nextOptions: RuntimeDragInputControllerOptions<TTargetId>,
|
|
) => {
|
|
options = nextOptions;
|
|
};
|
|
|
|
const cancel = (inputId?: string) => {
|
|
if (inputId && session?.inputId !== inputId) {
|
|
return;
|
|
}
|
|
|
|
const activeSession = session;
|
|
session = null;
|
|
if (activeSession) {
|
|
options.onCancel?.(activeSession);
|
|
}
|
|
};
|
|
|
|
const press = (input: RuntimeDragInputPress<TTargetId>) => {
|
|
cancel();
|
|
session = {
|
|
targetId: input.targetId,
|
|
inputId: input.inputId,
|
|
deviceKind: input.deviceKind,
|
|
startPoint: clonePoint(input.point),
|
|
currentPoint: clonePoint(input.point),
|
|
dragging: false,
|
|
forceDrop: false,
|
|
};
|
|
options.onPress?.(session);
|
|
return session;
|
|
};
|
|
|
|
const move = (input: RuntimeDragInputMove) => {
|
|
if (!session || session.inputId !== input.inputId) {
|
|
return null;
|
|
}
|
|
|
|
const wasDragging = session.dragging;
|
|
session = {
|
|
...session,
|
|
currentPoint: clonePoint(input.point),
|
|
dragging: shouldStartDragging(
|
|
session,
|
|
input.point,
|
|
options.dragThresholdPx ?? DEFAULT_DRAG_THRESHOLD_PX,
|
|
input.forceDragging,
|
|
),
|
|
};
|
|
|
|
if (!wasDragging && session.dragging) {
|
|
options.onDragStart?.(session);
|
|
}
|
|
if (session.dragging) {
|
|
options.onDragMove?.(session);
|
|
}
|
|
|
|
return session;
|
|
};
|
|
|
|
const release = (input: RuntimeDragInputRelease) => {
|
|
if (!session || session.inputId !== input.inputId) {
|
|
return null;
|
|
}
|
|
|
|
const completedSession = {
|
|
...session,
|
|
currentPoint: clonePoint(input.point),
|
|
forceDrop: input.forceDrop === true,
|
|
};
|
|
session = null;
|
|
if (completedSession.dragging || completedSession.forceDrop) {
|
|
options.onDrop?.(completedSession);
|
|
} else {
|
|
options.onTap?.(completedSession);
|
|
}
|
|
return completedSession;
|
|
};
|
|
|
|
return {
|
|
cancel,
|
|
getSession: () => session,
|
|
move,
|
|
press,
|
|
release,
|
|
setOptions,
|
|
};
|
|
}
|