Files
papercrate/frontend/src/desktop/useDocumentDrag.js
T
2025-11-07 23:48:25 +01:00

818 lines
28 KiB
JavaScript

import { useCallback, useEffect, useRef } from 'react';
import { preventAll, safeInvoke } from './events';
import { clamp } from './math';
import usePointerTap from '../ui/usePointerTap';
import { MIN_TIMESTEP, MAX_TIMESTEP, applyDomTransform } from './workspaceEngine';
const DRAG_HYSTERESIS_PX = 4;
const DRAG_HYSTERESIS_SQUARED = DRAG_HYSTERESIS_PX * DRAG_HYSTERESIS_PX;
const EDGE_COLLISION_THRESHOLD = 0.5;
const getEventTargetElement = (event) => {
if (typeof Element === 'undefined' || !event) {
return null;
}
const candidate = event.target || (event.nativeEvent ? event.nativeEvent.target : null);
return candidate instanceof Element ? candidate : null;
};
const useDocumentDrag = (options = {}) => {
const {
engine,
layoutRef,
dragTransformsRef,
itemRefs,
documentLookup,
ensureDocumentSize,
resolveBaseMetrics,
bringToFront,
setDraggingId,
canvasSize,
openOverlayForDoc,
recalcVisibleDocIds,
settings,
containerRef,
onInspectDocument,
onDocumentStackSelect,
selectedDocumentIds,
markLayoutDirty,
} = options;
const {
canvasPadding = 24,
defaultCanvasWidth = 1024,
defaultCanvasHeight = 680,
debugDrag = false,
} = settings || {};
useEffect(
() => () => {
engine?.disposeInertiaAnimations?.();
},
[engine],
);
const tapHandler = usePointerTap({
delay: 220,
onSingle: () => {},
onDouble: ({ data, event }) => {
if (!data || !data.docId) {
return;
}
if (event?.altKey) {
openOverlayForDoc(data.docId, data.originInfo);
return;
}
if (typeof onInspectDocument === 'function') {
onInspectDocument(data.docId, event);
}
},
});
const dragStateRef = useRef(null);
const setDragTransform = useCallback((docKey, transform) => {
if (!docKey) {
return;
}
const map = dragTransformsRef?.current;
if (!map) {
return;
}
map.set(String(docKey), transform);
}, [dragTransformsRef]);
const clearDragTransforms = useCallback(() => {
const map = dragTransformsRef?.current;
if (!map || typeof map.clear !== 'function') {
return;
}
map.clear();
}, [dragTransformsRef]);
const commitActiveDragTransforms = useCallback((docIds = null) => {
const map = dragTransformsRef?.current;
if (!map || !map.size) {
return;
}
const keys = Array.isArray(docIds) && docIds.length
? docIds.map((id) => (id != null ? String(id) : null)).filter(Boolean)
: Array.from(map.keys());
keys.forEach((key) => {
const transform = map.get(key);
if (!transform) {
return;
}
const previous = layoutRef.current.get(key) || {};
layoutRef.current.set(key, {
...previous,
centerX: transform.centerX,
centerY: transform.centerY,
rotation: transform.rotation ?? previous.rotation ?? 0,
});
});
markLayoutDirty?.();
}, [dragTransformsRef, layoutRef, markLayoutDirty]);
const finishDrag = useCallback(
(pointerId, { clearTransforms = true } = {}) => {
const state = dragStateRef.current;
if (state && state.pointerId === pointerId) {
const capturedTarget = state.capturedTarget;
if (capturedTarget && typeof capturedTarget.releasePointerCapture === 'function') {
try {
capturedTarget.releasePointerCapture(pointerId);
} catch (error) {
if (debugDrag) {
void error;
}
}
}
}
dragStateRef.current = null;
setDraggingId(null);
engine?.endDrag?.();
if (clearTransforms) {
clearDragTransforms();
}
},
[clearDragTransforms, debugDrag, engine, setDraggingId],
);
const handlePointerDown = useCallback(
(event, docIdInput, options = {}) => {
const targetElement = getEventTargetElement(event);
if (targetElement && typeof targetElement.closest === 'function' && targetElement.closest('[data-desk-tag-chip="true"]')) {
return;
}
preventAll(event);
const docId = docIdInput != null ? docIdInput : null;
const docKey = docId != null ? String(docId) : null;
if (!docKey) {
return;
}
engine?.cancelInertiaAnimation?.(docKey);
const doc = documentLookup.get(docKey);
if (!doc) {
return;
}
const stackDocIdsOptionRaw = options?.stackDocIds;
const stackDocIdsOption = Array.isArray(stackDocIdsOptionRaw)
? stackDocIdsOptionRaw
.map((value) => (value != null ? String(value) : null))
.filter(Boolean)
: null;
const stackSelectionAppliedInitial = Boolean(options?.stackSelectionApplied);
const wasSelectedAtPointerDown = Boolean(options?.wasSelected);
const pointerModifierActive = typeof options?.modifierActive === 'boolean'
? options.modifierActive
: Boolean(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey);
const stackReplace = Boolean(options?.stackReplace);
let selectionIds = Array.isArray(selectedDocumentIds)
? selectedDocumentIds.map((id) => String(id))
: [];
if (!stackDocIdsOption && !pointerModifierActive && !wasSelectedAtPointerDown) {
selectionIds = [docKey];
}
if (stackDocIdsOption && stackDocIdsOption.length) {
const selectionSet = new Set(selectionIds);
stackDocIdsOption.forEach((value) => {
if (value != null) {
selectionSet.add(String(value));
}
});
selectionIds = Array.from(selectionSet);
}
const metaOrCtrl = event.metaKey || event.ctrlKey;
if (!stackDocIdsOption && metaOrCtrl && !selectionIds.includes(docKey)) {
selectionIds = [...selectionIds, docKey];
}
selectionIds = selectionIds
.map((id) => String(id))
.filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id));
if (!selectionIds.includes(docKey)) {
selectionIds.unshift(docKey);
}
if (!selectionIds.length) {
selectionIds = [docKey];
}
const isGroupDrag = selectionIds.length > 1;
if (isGroupDrag) {
selectionIds.forEach((id) => {
if (id !== docKey) {
engine?.cancelInertiaAnimation?.(id);
}
});
}
const sizeInfo = ensureDocumentSize(doc) || { width: 0, height: 0 };
const docWidth = sizeInfo.width || 320;
const docHeight = sizeInfo.height || 240;
const { baseScale } = resolveBaseMetrics(doc, docWidth, docHeight);
const normalizedBaseScale =
Number.isFinite(baseScale) && baseScale > 0 ? baseScale : 1;
const entry = layoutRef.current.get(docKey) || null;
const defaultCenterX = canvasPadding + docWidth / 2;
const defaultCenterY = canvasPadding + docHeight / 2;
const centerX = typeof entry?.centerX === 'number' ? entry.centerX : defaultCenterX;
const centerY = typeof entry?.centerY === 'number' ? entry.centerY : defaultCenterY;
const modifierPressed = pointerModifierActive;
if (!modifierPressed) {
if (isGroupDrag) {
const layout = layoutRef.current;
const ordered = [...selectionIds]
.filter((id, index, array) => array.indexOf(id) === index)
.sort((a, b) => {
const aZ = layout.get(a)?.z ?? 0;
const bZ = layout.get(b)?.z ?? 0;
return aZ - bZ;
});
ordered.forEach((id) => {
bringToFront(id === docKey ? docId : id);
});
} else {
bringToFront(docId);
}
}
if (entry && (entry.centerX !== centerX || entry.centerY !== centerY)) {
layoutRef.current.set(docKey, { ...entry, centerX, centerY });
}
const capturedTarget = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
if (capturedTarget && typeof capturedTarget.setPointerCapture === 'function') {
try {
capturedTarget.setPointerCapture(event.pointerId);
} catch (error) {
if (debugDrag) {
void error;
}
}
}
const containerRect = containerRef?.current?.getBoundingClientRect?.() || null;
const containerLeft = containerRect?.left || 0;
const containerTop = containerRect?.top || 0;
const pointerCanvasX = event.clientX - containerLeft;
const pointerCanvasY = event.clientY - containerTop;
const pointerOffsetX = pointerCanvasX - centerX;
const pointerOffsetY = pointerCanvasY - centerY;
const initialRotationDeg = entry?.rotation ?? 0;
const initialRotationRad = (initialRotationDeg * Math.PI) / 180;
const cosInitial = Math.cos(-initialRotationRad);
const sinInitial = Math.sin(-initialRotationRad);
const localPointerOffsetX = pointerOffsetX * cosInitial - pointerOffsetY * sinInitial;
const localPointerOffsetY = pointerOffsetX * sinInitial + pointerOffsetY * cosInitial;
const groupItems = selectionIds.map((id) => {
const itemDoc = documentLookup.get(id);
const itemSize = ensureDocumentSize(itemDoc) || sizeInfo;
const itemWidth = itemSize.width || docWidth;
const itemHeight = itemSize.height || docHeight;
const itemEntry = layoutRef.current.get(id) || null;
const itemCenterX =
typeof itemEntry?.centerX === 'number' ? itemEntry.centerX : canvasPadding + itemWidth / 2;
const itemCenterY =
typeof itemEntry?.centerY === 'number' ? itemEntry.centerY : canvasPadding + itemHeight / 2;
const baseOffsetX = itemCenterX - centerX;
const baseOffsetY = itemCenterY - centerY;
const initialRotation = itemEntry?.rotation ?? 0;
const targetRotation = initialRotation;
return {
docId: id,
width: itemWidth,
height: itemHeight,
currentCenterX: itemCenterX,
currentCenterY: itemCenterY,
baseOffsetX,
baseOffsetY,
offsetX: baseOffsetX,
offsetY: baseOffsetY,
initialRotation,
displayRotation: initialRotation,
targetRotation,
};
});
const eventTimestamp =
typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp)
? event.timeStamp
: typeof performance !== 'undefined' && typeof performance.now === 'function'
? performance.now()
: Date.now();
const hasStackSource = Array.isArray(stackDocIdsOption) && stackDocIdsOption.length > 1;
dragStateRef.current = {
docId: docKey,
docKey,
pointerId: event.pointerId,
originCenterX: centerX,
originCenterY: centerY,
currentCenterX: centerX,
currentCenterY: centerY,
startX: event.clientX,
startY: event.clientY,
rotation: entry?.rotation ?? 0,
restRotation: entry?.rotation ?? 0,
dynamicRotation: 0,
angularVelocity: 0,
moved: false,
locked: false,
width: docWidth,
height: docHeight,
dragScale: 1,
baseScale: normalizedBaseScale,
capturedTarget,
lastClientX: event.clientX,
lastClientY: event.clientY,
lastTimestamp: eventTimestamp,
localPointerOffsetX,
localPointerOffsetY,
containerRectLeft: containerLeft,
containerRectTop: containerTop,
isGroup: isGroupDrag,
activeDocIds: selectionIds,
groupItems,
groupElevated: !isGroupDrag,
stackDocIds: hasStackSource ? stackDocIdsOption : null,
stackSelectionApplied: stackSelectionAppliedInitial || !hasStackSource,
stackReplace,
};
const state = dragStateRef.current;
clearDragTransforms();
state.groupItems.forEach((item) => {
if (!item?.docId) {
return;
}
setDragTransform(item.docId, {
centerX: item.currentCenterX,
centerY: item.currentCenterY,
rotation: item.displayRotation ?? item.initialRotation ?? 0,
width: item.width,
height: item.height,
scale: item.docId === state.docKey ? state.dragScale || 1 : 1,
});
});
engine?.beginDrag?.(state.activeDocIds);
setDraggingId(docKey);
if (isGroupDrag) {
groupItems.forEach((item) => {
if (item.docId === docKey) {
return;
}
const node = itemRefs.current.get(item.docId);
if (node) {
item.displayRotation = item.initialRotation;
const itemEntry = layoutRef.current.get(item.docId) || null;
applyDomTransform(node, {
centerX: item.currentCenterX,
centerY: item.currentCenterY,
width: item.width,
height: item.height,
rotation: item.displayRotation ?? 0,
scale: 1,
zIndex: itemEntry?.z,
});
}
});
}
}, [
bringToFront,
canvasPadding,
containerRef,
documentLookup,
engine,
ensureDocumentSize,
layoutRef,
resolveBaseMetrics,
selectedDocumentIds,
setDraggingId,
debugDrag,
itemRefs,
clearDragTransforms,
setDragTransform,
]);
const handlePointerMove = useCallback(
(event) => {
const state = dragStateRef.current;
if (!state) {
return;
}
if (state.pointerId !== event.pointerId) {
return;
}
preventAll(event);
if (state.isGroup) {
const containerRect = containerRef?.current?.getBoundingClientRect?.();
if (containerRect) {
state.containerRectLeft = containerRect.left;
state.containerRectTop = containerRect.top;
}
const pointerCanvasX = event.clientX - state.containerRectLeft;
const pointerCanvasY = event.clientY - state.containerRectTop;
const deltaX = event.clientX - state.startX;
const deltaY = event.clientY - state.startY;
if (!state.moved) {
const distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared < DRAG_HYSTERESIS_SQUARED) {
return;
}
state.moved = true;
if (
!state.stackSelectionApplied
&& Array.isArray(state.stackDocIds)
&& state.stackDocIds.length > 0
) {
safeInvoke(onDocumentStackSelect, state.stackDocIds, event, { replace: state.stackReplace });
state.stackSelectionApplied = true;
}
if (!state.groupElevated) {
const layout = layoutRef.current;
const sortedGroup = state.activeDocIds
.filter((id) => id !== state.docKey)
.sort((a, b) => {
const aZ = layout.get(a)?.z ?? 0;
const bZ = layout.get(b)?.z ?? 0;
return aZ - bZ;
});
sortedGroup.forEach((id) => bringToFront(id));
bringToFront(state.docKey);
state.groupElevated = true;
}
}
const docWidth = state.width;
const docHeight = state.height;
const halfWidth = docWidth / 2;
const halfHeight = docHeight / 2;
const canvasWidth = canvasSize.width || defaultCanvasWidth;
const canvasHeight = canvasSize.height || defaultCanvasHeight;
const minCenterX = canvasPadding + halfWidth;
const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - halfWidth);
const minCenterY = canvasPadding + halfHeight;
const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - halfHeight);
const desiredCenterX = pointerCanvasX - state.localPointerOffsetX;
const desiredCenterY = pointerCanvasY - state.localPointerOffsetY;
const centerX = clamp(desiredCenterX, minCenterX, maxCenterX);
const centerY = clamp(desiredCenterY, minCenterY, maxCenterY);
state.currentCenterX = centerX;
state.currentCenterY = centerY;
state.groupItems.forEach((item) => {
const isPrimary = item.docId === state.docKey;
if (isPrimary) {
item.currentCenterX = centerX;
item.currentCenterY = centerY;
item.offsetX = item.baseOffsetX ?? 0;
item.offsetY = item.baseOffsetY ?? 0;
item.displayRotation = state.rotation ?? item.displayRotation ?? 0;
} else {
const decay = 0.82;
const currentOffsetX = (item.offsetX ?? item.baseOffsetX ?? 0) * decay;
const currentOffsetY = (item.offsetY ?? item.baseOffsetY ?? 0) * decay;
item.offsetX = Math.abs(currentOffsetX) < 0.5 ? 0 : currentOffsetX;
item.offsetY = Math.abs(currentOffsetY) < 0.5 ? 0 : currentOffsetY;
const targetX = centerX + item.offsetX;
const targetY = centerY + item.offsetY;
const smoothing = 0.18;
item.currentCenterX += (targetX - item.currentCenterX) * smoothing;
item.currentCenterY += (targetY - item.currentCenterY) * smoothing;
const halfW = item.width / 2;
const halfH = item.height / 2;
const minX = canvasPadding + halfW;
const maxX = Math.max(minX, canvasWidth - canvasPadding - halfW);
const minY = canvasPadding + halfH;
const maxY = Math.max(minY, canvasHeight - canvasPadding - halfH);
item.currentCenterX = clamp(item.currentCenterX, minX, maxX);
item.currentCenterY = clamp(item.currentCenterY, minY, maxY);
const rotationBlend = 0.16;
item.displayRotation += (item.targetRotation - item.displayRotation) * rotationBlend;
}
const entry = layoutRef.current.get(item.docId) || null;
const payload = {
centerX: item.currentCenterX,
centerY: item.currentCenterY,
rotation: item.displayRotation ?? 0,
width: item.width,
height: item.height,
scale: isPrimary ? state.dragScale || 1 : 1,
zIndex: entry?.z,
};
setDragTransform(item.docId, payload);
const node = itemRefs.current.get(item.docId);
applyDomTransform(node, payload);
});
state.lastClientX = event.clientX;
state.lastClientY = event.clientY;
state.lastTimestamp =
typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp)
? event.timeStamp
: typeof performance !== 'undefined' && typeof performance.now === 'function'
? performance.now()
: Date.now();
return;
}
if (state.locked) {
return;
}
const deltaX = event.clientX - state.startX;
const deltaY = event.clientY - state.startY;
const docWidth = state.width;
const docHeight = state.height;
const halfWidth = docWidth / 2;
const halfHeight = docHeight / 2;
const containerRect = containerRef?.current?.getBoundingClientRect?.();
if (containerRect) {
state.containerRectLeft = containerRect.left;
state.containerRectTop = containerRect.top;
}
const containerLeft = state.containerRectLeft;
const containerTop = state.containerRectTop;
const pointerCanvasX = event.clientX - containerLeft;
const pointerCanvasY = event.clientY - containerTop;
const entry = layoutRef.current.get(state.docKey) || {};
const rotationDeg = state.rotation ?? entry.rotation ?? 0;
const rotationRad = (rotationDeg * Math.PI) / 180;
const cosRot = Math.cos(rotationRad);
const sinRot = Math.sin(rotationRad);
const rotatedOffsetX =
state.localPointerOffsetX * cosRot - state.localPointerOffsetY * sinRot;
const rotatedOffsetY =
state.localPointerOffsetX * sinRot + state.localPointerOffsetY * cosRot;
const previousCenterX = Number.isFinite(state.currentCenterX)
? state.currentCenterX
: state.originCenterX;
const previousCenterY = Number.isFinite(state.currentCenterY)
? state.currentCenterY
: state.originCenterY;
const absCos = Math.abs(cosRot);
const absSin = Math.abs(sinRot);
const rotatedHalfWidth = absCos * halfWidth + absSin * halfHeight;
const rotatedHalfHeight = absSin * halfWidth + absCos * halfHeight;
const canvasWidth = canvasSize.width || defaultCanvasWidth;
const canvasHeight = canvasSize.height || defaultCanvasHeight;
const minCenterX = canvasPadding + rotatedHalfWidth;
const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - rotatedHalfWidth);
const minCenterY = canvasPadding + rotatedHalfHeight;
const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - rotatedHalfHeight);
const pointerRelativePrevX = pointerCanvasX - previousCenterX;
const pointerRelativePrevY = pointerCanvasY - previousCenterY;
const cosInversePrev = Math.cos(-rotationRad);
const sinInversePrev = Math.sin(-rotationRad);
const pointerLocalPrevX = pointerRelativePrevX * cosInversePrev - pointerRelativePrevY * sinInversePrev;
const pointerLocalPrevY = pointerRelativePrevX * sinInversePrev + pointerRelativePrevY * cosInversePrev;
const pointerInsideRelativeToPrev =
Math.abs(pointerLocalPrevX) <= halfWidth && Math.abs(pointerLocalPrevY) <= halfHeight;
const desiredCenterX = pointerCanvasX - rotatedOffsetX;
const desiredCenterY = pointerCanvasY - rotatedOffsetY;
const clampedCenterX = clamp(desiredCenterX, minCenterX, maxCenterX);
const clampedCenterY = clamp(desiredCenterY, minCenterY, maxCenterY);
if (!state.moved) {
const distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared < DRAG_HYSTERESIS_SQUARED) {
return;
}
bringToFront(state.docKey);
state.moved = true;
}
const collidedWithHorizontalEdge =
Math.abs(desiredCenterY - clampedCenterY) > EDGE_COLLISION_THRESHOLD;
const collidedWithVerticalEdge =
Math.abs(desiredCenterX - clampedCenterX) > EDGE_COLLISION_THRESHOLD;
const collidedWithEdge = collidedWithHorizontalEdge || collidedWithVerticalEdge;
let currentCenterX = clampedCenterX;
let currentCenterY = clampedCenterY;
if (collidedWithEdge && !pointerInsideRelativeToPrev) {
currentCenterX = previousCenterX;
currentCenterY = previousCenterY;
}
state.currentCenterX = currentCenterX;
state.currentCenterY = currentCenterY;
const layoutEntry = layoutRef.current.get(state.docKey) || null;
const transformPayload = {
centerX: currentCenterX,
centerY: currentCenterY,
rotation: rotationDeg,
width: state.width,
height: state.height,
scale: state.dragScale || 1,
zIndex: layoutEntry?.z,
};
setDragTransform(state.docKey, transformPayload);
const primaryNode = itemRefs.current.get(state.docKey);
applyDomTransform(primaryNode, transformPayload);
const offsetX = pointerCanvasX - currentCenterX;
const offsetY = pointerCanvasY - currentCenterY;
const cosInverseCurrent = Math.cos(-rotationRad);
const sinInverseCurrent = Math.sin(-rotationRad);
const pointerLocalX = offsetX * cosInverseCurrent - offsetY * sinInverseCurrent;
const pointerLocalY = offsetX * sinInverseCurrent + offsetY * cosInverseCurrent;
const pointerInsideCard =
Math.abs(pointerLocalX) <= halfWidth && Math.abs(pointerLocalY) <= halfHeight;
const currentTimestamp =
typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp)
? event.timeStamp
: typeof performance !== 'undefined' && typeof performance.now === 'function'
? performance.now()
: Date.now();
const previousTimestamp = state.lastTimestamp ?? currentTimestamp;
let dt = (currentTimestamp - previousTimestamp) / 1000;
if (!Number.isFinite(dt) || dt <= 0) {
dt = MIN_TIMESTEP;
}
dt = clamp(dt, MIN_TIMESTEP, MAX_TIMESTEP);
state.lastClientX = event.clientX;
state.lastClientY = event.clientY;
state.lastTimestamp = currentTimestamp;
const rotationForOffsetDeg = state.rotation || 0;
const rotationForOffsetRad = (rotationForOffsetDeg * Math.PI) / 180;
const cosInverse = Math.cos(-rotationForOffsetRad);
const sinInverse = Math.sin(-rotationForOffsetRad);
const pointerRelativeX = pointerCanvasX - currentCenterX;
const pointerRelativeY = pointerCanvasY - currentCenterY;
const updatedLocalOffsetX = pointerRelativeX * cosInverse - pointerRelativeY * sinInverse;
const updatedLocalOffsetY = pointerRelativeX * sinInverse + pointerRelativeY * cosInverse;
if (!collidedWithEdge || pointerInsideCard) {
state.localPointerOffsetX = updatedLocalOffsetX;
state.localPointerOffsetY = updatedLocalOffsetY;
}
void debugDrag;
},
[
bringToFront,
canvasPadding,
canvasSize.height,
canvasSize.width,
defaultCanvasHeight,
defaultCanvasWidth,
containerRef,
layoutRef,
itemRefs,
debugDrag,
onDocumentStackSelect,
setDragTransform,
],
);
const handlePointerUp = useCallback(
(event) => {
const state = dragStateRef.current;
if (!state || state.pointerId !== event.pointerId) {
finishDrag(event.pointerId);
return;
}
if (state.isGroup) {
engine?.finalizeGroupDrag?.(state);
commitActiveDragTransforms(state.activeDocIds);
finishDrag(event.pointerId);
recalcVisibleDocIds();
return;
}
if (state.moved) {
commitActiveDragTransforms([state.docKey]);
const inertiaState = {
restRotation: state.restRotation,
dynamicRotation: state.dynamicRotation,
angularVelocity: state.angularVelocity,
rotation: state.rotation,
width: state.width,
height: state.height,
dragScale: state.dragScale || 1,
};
const docId = state.docKey;
finishDrag(event.pointerId);
engine?.startInertiaAnimation?.(docId, inertiaState);
return;
}
const docId = state.docKey;
const metaPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
if (!metaPressed) {
bringToFront(docId);
}
const originInfo = {
rotation: state.rotation || 0,
scale: state.baseScale || 1,
width: state.width,
height: state.height,
};
const docKey = docId != null ? String(docId) : null;
const doc = docKey ? documentLookup.get(docKey) : null;
tapHandler(event, {
docId,
originInfo,
docTitle: doc?.title || 'document',
});
finishDrag(event.pointerId);
},
[
bringToFront,
commitActiveDragTransforms,
documentLookup,
engine,
finishDrag,
recalcVisibleDocIds,
tapHandler,
],
);
const handlePointerCancel = useCallback(
(event) => {
const state = dragStateRef.current;
if (state && state.pointerId === event.pointerId && state.moved) {
if (state.isGroup) {
engine?.finalizeGroupDrag?.(state);
commitActiveDragTransforms(state.activeDocIds);
finishDrag(event.pointerId);
recalcVisibleDocIds();
return;
}
commitActiveDragTransforms([state.docKey]);
const inertiaState = {
restRotation: state.restRotation,
dynamicRotation: state.dynamicRotation,
angularVelocity: state.angularVelocity,
rotation: state.rotation,
width: state.width,
height: state.height,
dragScale: state.dragScale || 1,
};
const docId = state.docKey;
finishDrag(event.pointerId);
engine?.startInertiaAnimation?.(docId, inertiaState);
return;
}
finishDrag(event.pointerId);
},
[commitActiveDragTransforms, engine, finishDrag, recalcVisibleDocIds],
);
return {
handlePointerDown,
handlePointerMove,
handlePointerUp,
handlePointerCancel,
};
};
export default useDocumentDrag;