ui
This commit is contained in:
@@ -38,6 +38,7 @@ const CARD_MIN = 240;
|
|||||||
const CARD_MAX = 340;
|
const CARD_MAX = 340;
|
||||||
const TAG_REMOVE_DISTANCE = 160;
|
const TAG_REMOVE_DISTANCE = 160;
|
||||||
const STACK_HIT_EPSILON = 4;
|
const STACK_HIT_EPSILON = 4;
|
||||||
|
const POINTER_DRAG_THRESHOLD_SQUARED = 16;
|
||||||
|
|
||||||
const DEBUG_DRAG = false;
|
const DEBUG_DRAG = false;
|
||||||
const DEBUG_FOCUS = true;
|
const DEBUG_FOCUS = true;
|
||||||
@@ -572,8 +573,9 @@ const DesktopWorkspace = ({
|
|||||||
searchResults = null,
|
searchResults = null,
|
||||||
onDocumentOpen,
|
onDocumentOpen,
|
||||||
onInspectDocument = null,
|
onInspectDocument = null,
|
||||||
onDocumentPointerSelect = null,
|
onEntryPointer = null,
|
||||||
onDocumentStackSelect = null,
|
onDocumentStackSelect = null,
|
||||||
|
onPromoteSelection = null,
|
||||||
onAssignTagToDocument = null,
|
onAssignTagToDocument = null,
|
||||||
onRemoveTagFromDocument = null,
|
onRemoveTagFromDocument = null,
|
||||||
ensureAssetUrl = null,
|
ensureAssetUrl = null,
|
||||||
@@ -1832,8 +1834,9 @@ const recalcVisibleDocIds = useCallback(() => {
|
|||||||
pendingRemovalTag,
|
pendingRemovalTag,
|
||||||
onDocumentOpen,
|
onDocumentOpen,
|
||||||
onInspectDocument,
|
onInspectDocument,
|
||||||
onDocumentPointerSelect,
|
onEntryPointer,
|
||||||
onDocumentStackSelect,
|
onDocumentStackSelect,
|
||||||
|
onPromoteSelection,
|
||||||
ensureAssetUrl,
|
ensureAssetUrl,
|
||||||
getDocumentAsset,
|
getDocumentAsset,
|
||||||
handleNavigatorSnapshot,
|
handleNavigatorSnapshot,
|
||||||
@@ -1885,7 +1888,7 @@ const recalcVisibleDocIds = useCallback(() => {
|
|||||||
docSizeVersion,
|
docSizeVersion,
|
||||||
onDocumentOpen,
|
onDocumentOpen,
|
||||||
onInspectDocument,
|
onInspectDocument,
|
||||||
onDocumentPointerSelect,
|
onEntryPointer,
|
||||||
openOverlayForDoc,
|
openOverlayForDoc,
|
||||||
overlayDisplay,
|
overlayDisplay,
|
||||||
overlayOriginRect,
|
overlayOriginRect,
|
||||||
@@ -1902,6 +1905,7 @@ const recalcVisibleDocIds = useCallback(() => {
|
|||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
onClearSelection,
|
onClearSelection,
|
||||||
onDocumentStackSelect,
|
onDocumentStackSelect,
|
||||||
|
onPromoteSelection,
|
||||||
markLayoutDirty,
|
markLayoutDirty,
|
||||||
detailPanelOpen,
|
detailPanelOpen,
|
||||||
onCloseDetailPanel,
|
onCloseDetailPanel,
|
||||||
@@ -1949,8 +1953,9 @@ const DesktopWorkspaceView = () => {
|
|||||||
closeOverlay,
|
closeOverlay,
|
||||||
overlayOriginRect,
|
overlayOriginRect,
|
||||||
overlayOriginTransform,
|
overlayOriginTransform,
|
||||||
onDocumentPointerSelect,
|
onEntryPointer,
|
||||||
onDocumentStackSelect,
|
onDocumentStackSelect,
|
||||||
|
onPromoteSelection,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
onClearSelection,
|
onClearSelection,
|
||||||
detailPanelOpen,
|
detailPanelOpen,
|
||||||
@@ -1960,6 +1965,10 @@ const DesktopWorkspaceView = () => {
|
|||||||
const { handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel } =
|
const { handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel } =
|
||||||
useDocumentDrag();
|
useDocumentDrag();
|
||||||
|
|
||||||
|
const deferredSelectionRef = useRef(null);
|
||||||
|
const pointerStartRef = useRef({ x: 0, y: 0 });
|
||||||
|
const pointerMovedRef = useRef(false);
|
||||||
|
|
||||||
const resolveStackDocIds = useCallback(
|
const resolveStackDocIds = useCallback(
|
||||||
(event, targetDocId = null) => {
|
(event, targetDocId = null) => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
@@ -2245,6 +2254,11 @@ const DesktopWorkspaceView = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
|
pointerStartRef.current = {
|
||||||
|
x: Number.isFinite(event.clientX) ? event.clientX : 0,
|
||||||
|
y: Number.isFinite(event.clientY) ? event.clientY : 0,
|
||||||
|
};
|
||||||
|
pointerMovedRef.current = false;
|
||||||
const alreadySelected = selectedDocumentIds.includes(doc.id);
|
const alreadySelected = selectedDocumentIds.includes(doc.id);
|
||||||
const metaOrCtrlOnly =
|
const metaOrCtrlOnly =
|
||||||
(event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey;
|
(event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey;
|
||||||
@@ -2260,23 +2274,46 @@ const DesktopWorkspaceView = () => {
|
|||||||
if (hasStack && alreadySelected && typeof onDocumentStackSelect === 'function') {
|
if (hasStack && alreadySelected && typeof onDocumentStackSelect === 'function') {
|
||||||
onDocumentStackSelect(hits, event);
|
onDocumentStackSelect(hits, event);
|
||||||
appliedStackSelection = true;
|
appliedStackSelection = true;
|
||||||
|
deferredSelectionRef.current = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldInvokePointerSelect =
|
if (alreadySelected && typeof onPromoteSelection === 'function') {
|
||||||
typeof onDocumentPointerSelect === 'function'
|
onPromoteSelection(doc.id, event);
|
||||||
&& (
|
}
|
||||||
!metaOrCtrlOnly
|
|
||||||
|| !alreadySelected
|
|
||||||
|| event.shiftKey
|
|
||||||
|| event.altKey
|
|
||||||
|| !stackDocIds
|
|
||||||
|| stackDocIds.length <= 1
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldInvokePointerSelect) {
|
const modifierActive =
|
||||||
onDocumentPointerSelect(doc.id, event);
|
Boolean(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey);
|
||||||
|
const deferSelection =
|
||||||
|
!modifierActive
|
||||||
|
&& alreadySelected
|
||||||
|
&& Array.isArray(selectedDocumentIds)
|
||||||
|
&& selectedDocumentIds.length > 1;
|
||||||
|
|
||||||
|
const skipPointerSelection =
|
||||||
|
appliedStackSelection
|
||||||
|
|| (metaOrCtrlOnly
|
||||||
|
&& alreadySelected
|
||||||
|
&& Array.isArray(stackDocIds)
|
||||||
|
&& stackDocIds.length > 1);
|
||||||
|
|
||||||
|
if (skipPointerSelection) {
|
||||||
|
deferredSelectionRef.current = null;
|
||||||
|
} else if (deferSelection) {
|
||||||
|
deferredSelectionRef.current = {
|
||||||
|
type: 'document',
|
||||||
|
id: doc.id,
|
||||||
|
key: `document:${doc.id}`,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
deferredSelectionRef.current = null;
|
||||||
|
if (typeof onEntryPointer === 'function') {
|
||||||
|
onEntryPointer(
|
||||||
|
{ type: 'document', id: doc.id, key: `document:${doc.id}` },
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePointerDown(event, doc.id, {
|
handlePointerDown(event, doc.id, {
|
||||||
@@ -2284,9 +2321,33 @@ const DesktopWorkspaceView = () => {
|
|||||||
stackSelectionApplied: appliedStackSelection,
|
stackSelectionApplied: appliedStackSelection,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={(event) => {
|
||||||
onPointerUp={handlePointerUp}
|
const start = pointerStartRef.current;
|
||||||
onPointerCancel={handlePointerCancel}
|
const dx = Number.isFinite(event.clientX) ? event.clientX - start.x : 0;
|
||||||
|
const dy = Number.isFinite(event.clientY) ? event.clientY - start.y : 0;
|
||||||
|
if (dx * dx + dy * dy > POINTER_DRAG_THRESHOLD_SQUARED) {
|
||||||
|
pointerMovedRef.current = true;
|
||||||
|
}
|
||||||
|
handlePointerMove(event);
|
||||||
|
}}
|
||||||
|
onPointerUp={(event) => {
|
||||||
|
const deferredEntry = deferredSelectionRef.current;
|
||||||
|
const pointerMoved = pointerMovedRef.current;
|
||||||
|
|
||||||
|
handlePointerUp(event);
|
||||||
|
|
||||||
|
if (!pointerMoved && deferredEntry && typeof onEntryPointer === 'function') {
|
||||||
|
onEntryPointer(deferredEntry, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
deferredSelectionRef.current = null;
|
||||||
|
pointerMovedRef.current = false;
|
||||||
|
}}
|
||||||
|
onPointerCancel={(event) => {
|
||||||
|
deferredSelectionRef.current = null;
|
||||||
|
pointerMovedRef.current = false;
|
||||||
|
handlePointerCancel(event);
|
||||||
|
}}
|
||||||
onDragEnter={(event) => handleTagDragEnterDoc(event, doc.id)}
|
onDragEnter={(event) => handleTagDragEnterDoc(event, doc.id)}
|
||||||
onDragOver={(event) => handleTagDragOverDoc(event, doc.id)}
|
onDragOver={(event) => handleTagDragOverDoc(event, doc.id)}
|
||||||
onDragLeave={(event) => handleTagDragLeaveDoc(event, doc.id)}
|
onDragLeave={(event) => handleTagDragLeaveDoc(event, doc.id)}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { useManagementModals } from './useManagementModals';
|
|||||||
import { api, useAppDispatch, useAppState } from './appState';
|
import { api, useAppDispatch, useAppState } from './appState';
|
||||||
import { useDetailPanel } from './useDetailPanel';
|
import { useDetailPanel } from './useDetailPanel';
|
||||||
import { useDocumentSelection } from './useDocumentSelection';
|
import { useDocumentSelection } from './useDocumentSelection';
|
||||||
|
import { useEntryPointerHandler } from '../documents/useEntryPointer';
|
||||||
import { isTagTransferEvent } from '../documents/tagTransfer';
|
import { isTagTransferEvent } from '../documents/tagTransfer';
|
||||||
|
|
||||||
const ASSET_PRESIGN_TTL_MS = 240 * 1000; // backend issues 5 min tokens; refresh slightly early
|
const ASSET_PRESIGN_TTL_MS = 240 * 1000; // backend issues 5 min tokens; refresh slightly early
|
||||||
@@ -4383,6 +4384,29 @@ const AppLayout = () => {
|
|||||||
close: closeDetailPanel,
|
close: closeDetailPanel,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEntryPointer = useEntryPointerHandler({
|
||||||
|
resolveDocumentRowKey,
|
||||||
|
resolveFolderRowKey,
|
||||||
|
onSelectDocument: (documentId, event, { modifierClick, primaryClick, rowKey }) => {
|
||||||
|
const key = rowKey || resolveDocumentRowKey(documentId);
|
||||||
|
if (key) {
|
||||||
|
handleEntrySelection(key, event);
|
||||||
|
}
|
||||||
|
if (!modifierClick && primaryClick) {
|
||||||
|
openDetailPanel({ documentIds: [documentId] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSelectFolder: (folderId, event, { modifierClick, primaryClick, rowKey }) => {
|
||||||
|
const key = rowKey || resolveFolderRowKey(folderId);
|
||||||
|
if (key) {
|
||||||
|
handleEntrySelection(key, event);
|
||||||
|
}
|
||||||
|
if (!modifierClick && primaryClick) {
|
||||||
|
selectFolder(folderId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { breadcrumbs, missingBreadcrumbAncestors } = useMemo(() => {
|
const { breadcrumbs, missingBreadcrumbAncestors } = useMemo(() => {
|
||||||
const chain = [];
|
const chain = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -4715,9 +4739,8 @@ const AppLayout = () => {
|
|||||||
viewMode: documentsViewMode,
|
viewMode: documentsViewMode,
|
||||||
onViewModeChange: handleDocumentsViewModeChange,
|
onViewModeChange: handleDocumentsViewModeChange,
|
||||||
onClearSelection: clearDocumentSelection,
|
onClearSelection: clearDocumentSelection,
|
||||||
onEntrySelection: handleEntrySelection,
|
|
||||||
onDeleteSelection: handleDeleteSelection,
|
onDeleteSelection: handleDeleteSelection,
|
||||||
onOpenDetailPanel: openDetailPanel,
|
onEntryPointer: handleEntryPointer,
|
||||||
tags,
|
tags,
|
||||||
correspondents,
|
correspondents,
|
||||||
documentLookup,
|
documentLookup,
|
||||||
@@ -4749,10 +4772,8 @@ const AppLayout = () => {
|
|||||||
handleFolderDragEnd,
|
handleFolderDragEnd,
|
||||||
handleFolderDragStart,
|
handleFolderDragStart,
|
||||||
handleFolderRename,
|
handleFolderRename,
|
||||||
handleEntrySelection,
|
|
||||||
handleDeleteSelection,
|
handleDeleteSelection,
|
||||||
isFilterActive,
|
isFilterActive,
|
||||||
openDetailPanel,
|
|
||||||
openDocumentPreview,
|
openDocumentPreview,
|
||||||
refreshCurrentFolder,
|
refreshCurrentFolder,
|
||||||
searchLoading,
|
searchLoading,
|
||||||
@@ -4764,6 +4785,7 @@ const AppLayout = () => {
|
|||||||
tagLookupById,
|
tagLookupById,
|
||||||
toggleCorrespondentFilter,
|
toggleCorrespondentFilter,
|
||||||
toggleTagFilter,
|
toggleTagFilter,
|
||||||
|
handleEntryPointer,
|
||||||
ensureAssetUrl,
|
ensureAssetUrl,
|
||||||
getDocumentAsset,
|
getDocumentAsset,
|
||||||
tags,
|
tags,
|
||||||
@@ -4928,20 +4950,6 @@ const AppLayout = () => {
|
|||||||
[applySelection, openDetailPanel],
|
[applySelection, openDetailPanel],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeskDocumentPointerSelect = useCallback(
|
|
||||||
(docId, event) => {
|
|
||||||
if (!docId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rowKey = resolveDocumentRowKey(docId);
|
|
||||||
if (!rowKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleEntrySelection(rowKey, event);
|
|
||||||
},
|
|
||||||
[handleEntrySelection],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDeskDocumentStackSelect = useCallback(
|
const handleDeskDocumentStackSelect = useCallback(
|
||||||
(docIds) => {
|
(docIds) => {
|
||||||
if (!Array.isArray(docIds) || docIds.length === 0) {
|
if (!Array.isArray(docIds) || docIds.length === 0) {
|
||||||
@@ -4956,12 +4964,21 @@ const AppLayout = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
applySelection(rowKeys, {
|
const nextKeys = [...selectedEntries];
|
||||||
anchor: rowKeys[0],
|
rowKeys.forEach((key) => {
|
||||||
|
if (!nextKeys.includes(key)) {
|
||||||
|
nextKeys.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const anchor = rowKeys[0] || selectionAnchorRef.current || nextKeys[nextKeys.length - 1];
|
||||||
|
|
||||||
|
applySelection(nextKeys, {
|
||||||
|
anchor,
|
||||||
interactedKeys: rowKeys,
|
interactedKeys: rowKeys,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[applySelection],
|
[applySelection, selectedEntries, selectionAnchorRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeskHelpOpen = useCallback(() => {
|
const handleDeskHelpOpen = useCallback(() => {
|
||||||
@@ -5001,8 +5018,9 @@ const AppLayout = () => {
|
|||||||
onRefresh: refreshCurrentFolder,
|
onRefresh: refreshCurrentFolder,
|
||||||
onDocumentOpen: openDocumentPreview,
|
onDocumentOpen: openDocumentPreview,
|
||||||
onInspectDocument: handleDeskInspectDocument,
|
onInspectDocument: handleDeskInspectDocument,
|
||||||
onDocumentPointerSelect: handleDeskDocumentPointerSelect,
|
onEntryPointer: handleEntryPointer,
|
||||||
onDocumentStackSelect: handleDeskDocumentStackSelect,
|
onDocumentStackSelect: handleDeskDocumentStackSelect,
|
||||||
|
onPromoteSelection: promoteSelectionOrder,
|
||||||
onOpenHelp: handleDeskHelpOpen,
|
onOpenHelp: handleDeskHelpOpen,
|
||||||
helpOpen: deskHelpOpen,
|
helpOpen: deskHelpOpen,
|
||||||
onHelpClose: handleDeskHelpClose,
|
onHelpClose: handleDeskHelpClose,
|
||||||
@@ -5041,8 +5059,9 @@ const AppLayout = () => {
|
|||||||
refreshCurrentFolder,
|
refreshCurrentFolder,
|
||||||
openDocumentPreview,
|
openDocumentPreview,
|
||||||
handleDeskInspectDocument,
|
handleDeskInspectDocument,
|
||||||
handleDeskDocumentPointerSelect,
|
|
||||||
handleDeskDocumentStackSelect,
|
handleDeskDocumentStackSelect,
|
||||||
|
handleEntryPointer,
|
||||||
|
promoteSelectionOrder,
|
||||||
handleDeskHelpOpen,
|
handleDeskHelpOpen,
|
||||||
handleDeskHelpClose,
|
handleDeskHelpClose,
|
||||||
currentTenantId,
|
currentTenantId,
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { clamp, formatTransform } from './math';
|
||||||
|
|
||||||
|
export const MIN_TIMESTEP = 1 / 120;
|
||||||
|
export const MAX_TIMESTEP = 1 / 20;
|
||||||
|
export const MAX_DYNAMIC_ROTATION = 4;
|
||||||
|
export const MAX_ANGULAR_VELOCITY = 180;
|
||||||
|
export const ANGULAR_DAMPING = 11;
|
||||||
|
export const TORQUE_TO_ACCELERATION = 0.006;
|
||||||
|
export const SETTLE_ANGULAR_VELOCITY = 1.2;
|
||||||
|
|
||||||
|
const callRef = (ref) => {
|
||||||
|
const handler = ref?.current;
|
||||||
|
if (typeof handler === 'function') {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDragPhysics = ({
|
||||||
|
layoutRef,
|
||||||
|
itemRefs,
|
||||||
|
markLayoutDirtyRef,
|
||||||
|
syncLayoutSnapshotRef,
|
||||||
|
}) => {
|
||||||
|
const inertiaAnimations = new Map();
|
||||||
|
|
||||||
|
const applyTransform = (docId, centerX, centerY, width, height, rotation, scale = 1) => {
|
||||||
|
const node = itemRefs.current.get(docId);
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.style.transform = formatTransform(
|
||||||
|
centerX - width / 2,
|
||||||
|
centerY - height / 2,
|
||||||
|
rotation,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalizeGroupDrag = (dragState) => {
|
||||||
|
if (!dragState?.groupItems) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragState.groupItems.forEach((item) => {
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryItem = layoutRef.current.get(item.docId) || {};
|
||||||
|
const centerX = item.currentCenterX ?? entryItem.centerX ?? dragState.originCenterX;
|
||||||
|
const centerY = item.currentCenterY ?? entryItem.centerY ?? dragState.originCenterY;
|
||||||
|
const rotation = item.displayRotation ?? entryItem.rotation ?? 0;
|
||||||
|
|
||||||
|
layoutRef.current.set(item.docId, {
|
||||||
|
...entryItem,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
rotation,
|
||||||
|
});
|
||||||
|
|
||||||
|
applyTransform(
|
||||||
|
item.docId,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
item.width,
|
||||||
|
item.height,
|
||||||
|
rotation,
|
||||||
|
item.docId === dragState.docKey ? dragState.dragScale || 1 : 1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
callRef(markLayoutDirtyRef);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelInertiaAnimation = (docId) => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
inertiaAnimations.delete(docId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const existing = inertiaAnimations.get(docId);
|
||||||
|
if (existing && typeof window.cancelAnimationFrame === 'function') {
|
||||||
|
window.cancelAnimationFrame(existing.frameId);
|
||||||
|
}
|
||||||
|
inertiaAnimations.delete(docId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const integrateRotation = (simulationState, dt, torque = 0, dampingOverride = null) => {
|
||||||
|
const { docId } = simulationState;
|
||||||
|
const entry = layoutRef.current.get(docId);
|
||||||
|
if (!entry) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerX = Number(entry.centerX);
|
||||||
|
const centerY = Number(entry.centerY);
|
||||||
|
if (!Number.isFinite(centerX) || !Number.isFinite(centerY)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const torqueAcceleration = torque * TORQUE_TO_ACCELERATION;
|
||||||
|
let angularVelocity = simulationState.angularVelocity + torqueAcceleration * dt;
|
||||||
|
angularVelocity = clamp(angularVelocity, -MAX_ANGULAR_VELOCITY, MAX_ANGULAR_VELOCITY);
|
||||||
|
|
||||||
|
const dampingConstant = typeof dampingOverride === 'number' ? dampingOverride : ANGULAR_DAMPING;
|
||||||
|
const dampingFactor = Math.exp(-dampingConstant * dt);
|
||||||
|
angularVelocity *= dampingFactor;
|
||||||
|
|
||||||
|
let dynamicRotation = simulationState.dynamicRotation + angularVelocity * dt;
|
||||||
|
if (dynamicRotation > MAX_DYNAMIC_ROTATION) {
|
||||||
|
dynamicRotation = MAX_DYNAMIC_ROTATION;
|
||||||
|
angularVelocity = Math.min(angularVelocity, 0);
|
||||||
|
} else if (dynamicRotation < -MAX_DYNAMIC_ROTATION) {
|
||||||
|
dynamicRotation = -MAX_DYNAMIC_ROTATION;
|
||||||
|
angularVelocity = Math.max(angularVelocity, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
simulationState.angularVelocity = angularVelocity;
|
||||||
|
simulationState.dynamicRotation = dynamicRotation;
|
||||||
|
simulationState.rotation = simulationState.restRotation + dynamicRotation;
|
||||||
|
|
||||||
|
const rotation = simulationState.rotation;
|
||||||
|
layoutRef.current.set(docId, { ...entry, rotation });
|
||||||
|
callRef(markLayoutDirtyRef);
|
||||||
|
|
||||||
|
const node = itemRefs.current.get(docId);
|
||||||
|
if (node) {
|
||||||
|
node.style.transform = formatTransform(
|
||||||
|
centerX - simulationState.width / 2,
|
||||||
|
centerY - simulationState.height / 2,
|
||||||
|
rotation,
|
||||||
|
simulationState.dragScale || 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSettled = Math.abs(angularVelocity) < SETTLE_ANGULAR_VELOCITY;
|
||||||
|
return isSettled;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startInertiaAnimation = (docId, baseState) => {
|
||||||
|
if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelInertiaAnimation(docId);
|
||||||
|
|
||||||
|
const now =
|
||||||
|
typeof performance !== 'undefined' && typeof performance.now === 'function'
|
||||||
|
? performance.now()
|
||||||
|
: Date.now();
|
||||||
|
|
||||||
|
const simulationState = {
|
||||||
|
...baseState,
|
||||||
|
docId,
|
||||||
|
dragScale: baseState.dragScale || 1,
|
||||||
|
lastTimestamp: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
const step = (timestamp) => {
|
||||||
|
const safeTimestamp = Number.isFinite(timestamp) ? timestamp : now + 16;
|
||||||
|
const previous = simulationState.lastTimestamp;
|
||||||
|
let dt = (safeTimestamp - previous) / 1000;
|
||||||
|
if (!Number.isFinite(dt) || dt <= 0) {
|
||||||
|
dt = MIN_TIMESTEP;
|
||||||
|
}
|
||||||
|
dt = clamp(dt, MIN_TIMESTEP, MAX_TIMESTEP);
|
||||||
|
simulationState.lastTimestamp = safeTimestamp;
|
||||||
|
|
||||||
|
const settled = integrateRotation(simulationState, dt, 0);
|
||||||
|
if (settled) {
|
||||||
|
inertiaAnimations.delete(docId);
|
||||||
|
callRef(syncLayoutSnapshotRef);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
simulationState.frameId = window.requestAnimationFrame(step);
|
||||||
|
};
|
||||||
|
|
||||||
|
simulationState.frameId = window.requestAnimationFrame(step);
|
||||||
|
inertiaAnimations.set(docId, simulationState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispose = () => {
|
||||||
|
if (typeof window !== 'undefined' && typeof window.cancelAnimationFrame === 'function') {
|
||||||
|
inertiaAnimations.forEach((animation) => {
|
||||||
|
if (animation?.frameId != null) {
|
||||||
|
window.cancelAnimationFrame(animation.frameId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
inertiaAnimations.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
applyTransform,
|
||||||
|
finalizeGroupDrag,
|
||||||
|
cancelInertiaAnimation,
|
||||||
|
integrateRotation,
|
||||||
|
startInertiaAnimation,
|
||||||
|
dispose,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createDragPhysics;
|
||||||
@@ -1,18 +1,12 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { useDesktopContext } from './context';
|
import { useDesktopContext } from './context';
|
||||||
import { preventAll } from './events';
|
import { preventAll } from './events';
|
||||||
import { clamp, formatTransform } from './math';
|
import { clamp, formatTransform } from './math';
|
||||||
import usePointerTap from '../ui/usePointerTap';
|
import usePointerTap from '../ui/usePointerTap';
|
||||||
|
import createDragPhysics, { MIN_TIMESTEP, MAX_TIMESTEP } from './dragPhysics';
|
||||||
|
|
||||||
const DRAG_HYSTERESIS_PX = 4;
|
const DRAG_HYSTERESIS_PX = 4;
|
||||||
const DRAG_HYSTERESIS_SQUARED = DRAG_HYSTERESIS_PX * DRAG_HYSTERESIS_PX;
|
const DRAG_HYSTERESIS_SQUARED = DRAG_HYSTERESIS_PX * DRAG_HYSTERESIS_PX;
|
||||||
const MIN_TIMESTEP = 1 / 120;
|
|
||||||
const MAX_TIMESTEP = 1 / 20;
|
|
||||||
const MAX_DYNAMIC_ROTATION = 4;
|
|
||||||
const MAX_ANGULAR_VELOCITY = 180;
|
|
||||||
const ANGULAR_DAMPING = 11;
|
|
||||||
const TORQUE_TO_ACCELERATION = 0.006;
|
|
||||||
const SETTLE_ANGULAR_VELOCITY = 1.2;
|
|
||||||
const EDGE_COLLISION_THRESHOLD = 0.5;
|
const EDGE_COLLISION_THRESHOLD = 0.5;
|
||||||
|
|
||||||
const useDocumentDrag = () => {
|
const useDocumentDrag = () => {
|
||||||
@@ -37,61 +31,40 @@ const useDocumentDrag = () => {
|
|||||||
markLayoutDirty,
|
markLayoutDirty,
|
||||||
} = useDesktopContext();
|
} = useDesktopContext();
|
||||||
|
|
||||||
const applyTransform = useCallback(
|
const markLayoutDirtyRef = useRef(markLayoutDirty);
|
||||||
(docId, centerX, centerY, width, height, rotation, scale = 1) => {
|
useEffect(() => {
|
||||||
const node = itemRefs.current.get(docId);
|
markLayoutDirtyRef.current = markLayoutDirty;
|
||||||
if (!node) {
|
}, [markLayoutDirty]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
node.style.transform = formatTransform(
|
|
||||||
centerX - width / 2,
|
|
||||||
centerY - height / 2,
|
|
||||||
rotation,
|
|
||||||
scale,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[itemRefs],
|
|
||||||
);
|
|
||||||
|
|
||||||
const finalizeGroupDrag = useCallback(
|
const syncLayoutSnapshotRef = useRef(syncLayoutSnapshot);
|
||||||
(dragState) => {
|
useEffect(() => {
|
||||||
if (!dragState?.groupItems) {
|
syncLayoutSnapshotRef.current = syncLayoutSnapshot;
|
||||||
return;
|
}, [syncLayoutSnapshot]);
|
||||||
}
|
|
||||||
|
|
||||||
dragState.groupItems.forEach((item) => {
|
const physicsRef = useRef(null);
|
||||||
if (!item) {
|
if (!physicsRef.current) {
|
||||||
return;
|
physicsRef.current = createDragPhysics({
|
||||||
}
|
layoutRef,
|
||||||
|
itemRefs,
|
||||||
const entryItem = layoutRef.current.get(item.docId) || {};
|
markLayoutDirtyRef,
|
||||||
const centerX = item.currentCenterX ?? entryItem.centerX ?? dragState.originCenterX;
|
syncLayoutSnapshotRef,
|
||||||
const centerY = item.currentCenterY ?? entryItem.centerY ?? dragState.originCenterY;
|
|
||||||
const rotation = item.displayRotation ?? entryItem.rotation ?? 0;
|
|
||||||
|
|
||||||
layoutRef.current.set(item.docId, {
|
|
||||||
...entryItem,
|
|
||||||
centerX,
|
|
||||||
centerY,
|
|
||||||
rotation,
|
|
||||||
});
|
|
||||||
|
|
||||||
applyTransform(
|
|
||||||
item.docId,
|
|
||||||
centerX,
|
|
||||||
centerY,
|
|
||||||
item.width,
|
|
||||||
item.height,
|
|
||||||
rotation,
|
|
||||||
item.docId === dragState.docKey ? dragState.dragScale || 1 : 1,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
markLayoutDirty?.();
|
useEffect(
|
||||||
},
|
() => () => {
|
||||||
[applyTransform, layoutRef, markLayoutDirty],
|
physicsRef.current?.dispose?.();
|
||||||
|
},
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
applyTransform,
|
||||||
|
finalizeGroupDrag,
|
||||||
|
cancelInertiaAnimation,
|
||||||
|
startInertiaAnimation,
|
||||||
|
} = physicsRef.current;
|
||||||
|
|
||||||
const tapHandler = usePointerTap({
|
const tapHandler = usePointerTap({
|
||||||
delay: 220,
|
delay: 220,
|
||||||
onSingle: ({ data, event }) => {
|
onSingle: ({ data, event }) => {
|
||||||
@@ -119,118 +92,8 @@ const useDocumentDrag = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const dragStateRef = useRef(null);
|
const dragStateRef = useRef(null);
|
||||||
const inertiaAnimationsRef = useRef(new Map());
|
|
||||||
const { canvasPadding, defaultCanvasWidth, defaultCanvasHeight, debugDrag } = settings;
|
const { canvasPadding, defaultCanvasWidth, defaultCanvasHeight, debugDrag } = settings;
|
||||||
|
|
||||||
const cancelInertiaAnimation = useCallback((docId) => {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
inertiaAnimationsRef.current.delete(docId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const existing = inertiaAnimationsRef.current.get(docId);
|
|
||||||
if (existing && typeof window.cancelAnimationFrame === 'function') {
|
|
||||||
window.cancelAnimationFrame(existing.frameId);
|
|
||||||
}
|
|
||||||
inertiaAnimationsRef.current.delete(docId);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const integrateRotation = useCallback(
|
|
||||||
(simulationState, dt, torque = 0, dampingOverride = null) => {
|
|
||||||
const { docId } = simulationState;
|
|
||||||
const entry = layoutRef.current.get(docId);
|
|
||||||
if (!entry) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const centerX = Number(entry.centerX);
|
|
||||||
const centerY = Number(entry.centerY);
|
|
||||||
if (!Number.isFinite(centerX) || !Number.isFinite(centerY)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const torqueAcceleration = torque * TORQUE_TO_ACCELERATION;
|
|
||||||
let angularVelocity = simulationState.angularVelocity + torqueAcceleration * dt;
|
|
||||||
angularVelocity = clamp(angularVelocity, -MAX_ANGULAR_VELOCITY, MAX_ANGULAR_VELOCITY);
|
|
||||||
|
|
||||||
const dampingConstant = typeof dampingOverride === 'number' ? dampingOverride : ANGULAR_DAMPING;
|
|
||||||
const dampingFactor = Math.exp(-dampingConstant * dt);
|
|
||||||
angularVelocity *= dampingFactor;
|
|
||||||
|
|
||||||
let dynamicRotation = simulationState.dynamicRotation + angularVelocity * dt;
|
|
||||||
if (dynamicRotation > MAX_DYNAMIC_ROTATION) {
|
|
||||||
dynamicRotation = MAX_DYNAMIC_ROTATION;
|
|
||||||
angularVelocity = Math.min(angularVelocity, 0);
|
|
||||||
} else if (dynamicRotation < -MAX_DYNAMIC_ROTATION) {
|
|
||||||
dynamicRotation = -MAX_DYNAMIC_ROTATION;
|
|
||||||
angularVelocity = Math.max(angularVelocity, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
simulationState.angularVelocity = angularVelocity;
|
|
||||||
simulationState.dynamicRotation = dynamicRotation;
|
|
||||||
simulationState.rotation = simulationState.restRotation + dynamicRotation;
|
|
||||||
|
|
||||||
const rotation = simulationState.rotation;
|
|
||||||
layoutRef.current.set(docId, { ...entry, rotation });
|
|
||||||
markLayoutDirty?.();
|
|
||||||
|
|
||||||
const node = itemRefs.current.get(docId);
|
|
||||||
if (node) {
|
|
||||||
node.style.transform = formatTransform(
|
|
||||||
centerX - simulationState.width / 2,
|
|
||||||
centerY - simulationState.height / 2,
|
|
||||||
rotation,
|
|
||||||
simulationState.dragScale || 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSettled = Math.abs(angularVelocity) < SETTLE_ANGULAR_VELOCITY;
|
|
||||||
return isSettled;
|
|
||||||
},
|
|
||||||
[itemRefs, layoutRef, markLayoutDirty],
|
|
||||||
);
|
|
||||||
|
|
||||||
const startInertiaAnimation = useCallback(
|
|
||||||
(docId, baseState) => {
|
|
||||||
if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cancelInertiaAnimation(docId);
|
|
||||||
const now =
|
|
||||||
typeof performance !== 'undefined' && typeof performance.now === 'function'
|
|
||||||
? performance.now()
|
|
||||||
: Date.now();
|
|
||||||
const simulationState = {
|
|
||||||
...baseState,
|
|
||||||
docId,
|
|
||||||
dragScale: baseState.dragScale || 1,
|
|
||||||
lastTimestamp: now,
|
|
||||||
};
|
|
||||||
|
|
||||||
const step = (timestamp) => {
|
|
||||||
const safeTimestamp = Number.isFinite(timestamp) ? timestamp : now + 16;
|
|
||||||
const previous = simulationState.lastTimestamp;
|
|
||||||
let dt = (safeTimestamp - previous) / 1000;
|
|
||||||
if (!Number.isFinite(dt) || dt <= 0) {
|
|
||||||
dt = MIN_TIMESTEP;
|
|
||||||
}
|
|
||||||
dt = clamp(dt, MIN_TIMESTEP, MAX_TIMESTEP);
|
|
||||||
simulationState.lastTimestamp = safeTimestamp;
|
|
||||||
|
|
||||||
const settled = integrateRotation(simulationState, dt, 0);
|
|
||||||
if (settled) {
|
|
||||||
inertiaAnimationsRef.current.delete(docId);
|
|
||||||
syncLayoutSnapshot();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
simulationState.frameId = window.requestAnimationFrame(step);
|
|
||||||
};
|
|
||||||
|
|
||||||
simulationState.frameId = window.requestAnimationFrame(step);
|
|
||||||
inertiaAnimationsRef.current.set(docId, simulationState);
|
|
||||||
},
|
|
||||||
[cancelInertiaAnimation, integrateRotation, syncLayoutSnapshot],
|
|
||||||
);
|
|
||||||
|
|
||||||
const finishDrag = useCallback(
|
const finishDrag = useCallback(
|
||||||
(pointerId) => {
|
(pointerId) => {
|
||||||
const state = dragStateRef.current;
|
const state = dragStateRef.current;
|
||||||
@@ -346,7 +209,18 @@ const useDocumentDrag = () => {
|
|||||||
const modifierPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
const modifierPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
||||||
if (!modifierPressed) {
|
if (!modifierPressed) {
|
||||||
if (isGroupDrag) {
|
if (isGroupDrag) {
|
||||||
groupDocIds.forEach((id) => bringToFront(id));
|
const layout = layoutRef.current;
|
||||||
|
const ordered = [...groupDocIds]
|
||||||
|
.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 {
|
} else {
|
||||||
bringToFront(docId);
|
bringToFront(docId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const DocumentsGrid = ({
|
|||||||
onFolderDragStart,
|
onFolderDragStart,
|
||||||
onFolderDragEnd,
|
onFolderDragEnd,
|
||||||
onDocumentClick,
|
onDocumentClick,
|
||||||
onDocumentOpen,
|
onDocumentActivate,
|
||||||
onDocumentDragStart,
|
onDocumentDragStart,
|
||||||
onDocumentDragEnd,
|
onDocumentDragEnd,
|
||||||
onDocumentTagDragOver,
|
onDocumentTagDragOver,
|
||||||
@@ -244,7 +244,7 @@ const DocumentsGrid = ({
|
|||||||
id={`document-card-${doc.id}`}
|
id={`document-card-${doc.id}`}
|
||||||
data-doc-id={doc.id}
|
data-doc-id={doc.id}
|
||||||
onClick={(event) => onDocumentClick?.(doc, event)}
|
onClick={(event) => onDocumentClick?.(doc, event)}
|
||||||
onDoubleClick={() => onDocumentOpen?.(doc.id)}
|
onDoubleClick={(event) => onDocumentActivate?.(doc, event)}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(event) => onDocumentDragStart?.(event, doc)}
|
onDragStart={(event) => onDocumentDragStart?.(event, doc)}
|
||||||
onDragEnd={(event) => onDocumentDragEnd?.(event)}
|
onDragEnd={(event) => onDocumentDragEnd?.(event)}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const DocumentsList = ({
|
|||||||
onFolderDragEnd,
|
onFolderDragEnd,
|
||||||
onFolderRename,
|
onFolderRename,
|
||||||
onDocumentClick,
|
onDocumentClick,
|
||||||
onDocumentOpen,
|
onDocumentActivate,
|
||||||
onDocumentDragStart,
|
onDocumentDragStart,
|
||||||
onDocumentDragEnd,
|
onDocumentDragEnd,
|
||||||
onDocumentTagDragOver,
|
onDocumentTagDragOver,
|
||||||
@@ -266,7 +266,7 @@ const DocumentsList = ({
|
|||||||
id={`document-row-${doc.id}`}
|
id={`document-row-${doc.id}`}
|
||||||
data-doc-id={doc.id}
|
data-doc-id={doc.id}
|
||||||
onClick={(event) => onDocumentClick?.(doc, event)}
|
onClick={(event) => onDocumentClick?.(doc, event)}
|
||||||
onDoubleClick={() => onDocumentOpen?.(doc.id)}
|
onDoubleClick={(event) => onDocumentActivate?.(doc, event)}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(event) => onDocumentDragStart?.(event, doc)}
|
onDragStart={(event) => onDocumentDragStart?.(event, doc)}
|
||||||
onDragEnd={(event) => onDocumentDragEnd?.(event)}
|
onDragEnd={(event) => onDocumentDragEnd?.(event)}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import DocumentsGrid from './DocumentsGrid';
|
|||||||
import DocumentsList from './DocumentsList';
|
import DocumentsList from './DocumentsList';
|
||||||
import { isTagTransferEvent } from './tagTransfer';
|
import { isTagTransferEvent } from './tagTransfer';
|
||||||
import SelectionFloatingActions from './SelectionFloatingActions';
|
import SelectionFloatingActions from './SelectionFloatingActions';
|
||||||
|
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
|
||||||
|
import { useAssetNavigator } from '../hooks/useAssetNavigator';
|
||||||
|
import { isPointerModifierEvent, isPrimaryPointerEvent } from './useEntryPointer';
|
||||||
|
|
||||||
const DEFAULT_GRID_ICON_SIZE = 144;
|
const DEFAULT_GRID_ICON_SIZE = 144;
|
||||||
|
|
||||||
@@ -39,15 +42,13 @@ const DocumentsPanel = ({
|
|||||||
draggedFolderId,
|
draggedFolderId,
|
||||||
onFolderRename,
|
onFolderRename,
|
||||||
selectedFolderIds = [],
|
selectedFolderIds = [],
|
||||||
onDocumentOpen,
|
|
||||||
selectedDocumentIds = [],
|
selectedDocumentIds = [],
|
||||||
focusedRowKey,
|
focusedRowKey,
|
||||||
draggingDocumentIds = [],
|
draggingDocumentIds = [],
|
||||||
onDocumentDragStart,
|
onDocumentDragStart,
|
||||||
onDocumentDragEnd,
|
onDocumentDragEnd,
|
||||||
onDocumentRename,
|
onDocumentRename,
|
||||||
onEntrySelection = null,
|
onEntryPointer = null,
|
||||||
onOpenDetailPanel = null,
|
|
||||||
tagLookupById,
|
tagLookupById,
|
||||||
activeCorrespondentIds = [],
|
activeCorrespondentIds = [],
|
||||||
onDocumentListFocus,
|
onDocumentListFocus,
|
||||||
@@ -118,6 +119,85 @@ const DocumentsPanel = ({
|
|||||||
}, []);
|
}, []);
|
||||||
const isGridView = viewMode === 'grid';
|
const isGridView = viewMode === 'grid';
|
||||||
const isDeskView = viewMode === 'desk';
|
const isDeskView = viewMode === 'desk';
|
||||||
|
|
||||||
|
const [previewDocId, setPreviewDocId] = useState(null);
|
||||||
|
|
||||||
|
const previewDoc = useMemo(() => {
|
||||||
|
if (!previewDocId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return rows.find((doc) => doc?.id === previewDocId) || null;
|
||||||
|
}, [previewDocId, rows]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (previewDocId && !previewDoc) {
|
||||||
|
setPreviewDocId(null);
|
||||||
|
}
|
||||||
|
}, [previewDocId, previewDoc]);
|
||||||
|
|
||||||
|
const previewNavigator = useAssetNavigator({
|
||||||
|
document: previewDoc,
|
||||||
|
assetType: 'preview',
|
||||||
|
ensureAssetUrl,
|
||||||
|
getAsset: getDocumentAsset,
|
||||||
|
prefetch: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentUrl: previewUrl,
|
||||||
|
canGoPrev: previewCanGoPrev,
|
||||||
|
canGoNext: previewCanGoNext,
|
||||||
|
goPrev: previewGoPrev,
|
||||||
|
goNext: previewGoNext,
|
||||||
|
} = previewNavigator;
|
||||||
|
|
||||||
|
const previewDisplay = useMemo(() => {
|
||||||
|
if (!previewDoc || !previewUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
url: previewUrl,
|
||||||
|
alt: previewDoc.title,
|
||||||
|
canGoPrev: Boolean(previewCanGoPrev),
|
||||||
|
canGoNext: Boolean(previewCanGoNext),
|
||||||
|
goPrev: previewGoPrev,
|
||||||
|
goNext: previewGoNext,
|
||||||
|
};
|
||||||
|
}, [previewDoc, previewUrl, previewCanGoPrev, previewCanGoNext, previewGoPrev, previewGoNext]);
|
||||||
|
|
||||||
|
const closePreviewOverlay = useCallback(() => {
|
||||||
|
setPreviewDocId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDocumentPreviewZoom = useCallback(
|
||||||
|
(doc) => {
|
||||||
|
if (!doc || !doc.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previewAsset = typeof getDocumentAsset === 'function' ? getDocumentAsset(doc, 'preview') : null;
|
||||||
|
if (!previewAsset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPreviewDocId(doc.id);
|
||||||
|
},
|
||||||
|
[getDocumentAsset],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDocumentActivate = useCallback(
|
||||||
|
(doc, event) => {
|
||||||
|
if (event) {
|
||||||
|
if (typeof event.preventDefault === 'function') {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
if (typeof event.stopPropagation === 'function') {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleDocumentPreviewZoom(doc);
|
||||||
|
},
|
||||||
|
[handleDocumentPreviewZoom],
|
||||||
|
);
|
||||||
|
|
||||||
const isListView = viewMode === 'list';
|
const isListView = viewMode === 'list';
|
||||||
const gridIconSize = DEFAULT_GRID_ICON_SIZE;
|
const gridIconSize = DEFAULT_GRID_ICON_SIZE;
|
||||||
const handleSetViewMode = useCallback(
|
const handleSetViewMode = useCallback(
|
||||||
@@ -240,56 +320,20 @@ const DocumentsPanel = ({
|
|||||||
[isTagDragEvent, onDocumentTagDrop],
|
[isTagDragEvent, onDocumentTagDrop],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEntryClick = useCallback(
|
|
||||||
(entry, event) => {
|
|
||||||
if (!entry || !entry.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (entry.type === EntryType.document && suppressDocumentClickRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowKey = entry.type === EntryType.document ? `document:${entry.id}` : `folder:${entry.id}`;
|
|
||||||
|
|
||||||
if (rowKey && typeof onEntrySelection === 'function') {
|
|
||||||
onEntrySelection(rowKey, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.type === EntryType.document) {
|
|
||||||
const hasModifier = Boolean(
|
|
||||||
event && (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey),
|
|
||||||
);
|
|
||||||
if (!hasModifier && typeof onOpenDetailPanel === 'function') {
|
|
||||||
onOpenDetailPanel();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.type === EntryType.folder) {
|
|
||||||
const hasModifier = Boolean(
|
|
||||||
event && (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey),
|
|
||||||
);
|
|
||||||
const isPrimaryClick = Boolean(event && event.type === 'click' && event.button === 0);
|
|
||||||
if (!hasModifier && isPrimaryClick && typeof onFolderSelect === 'function') {
|
|
||||||
onFolderSelect(entry.id);
|
|
||||||
}
|
|
||||||
if (scrollRef.current) {
|
|
||||||
scrollRef.current.focus({ preventScroll: true });
|
|
||||||
}
|
|
||||||
onFocusedRowChange?.(rowKey);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onEntrySelection, onOpenDetailPanel, onFocusedRowChange, onFolderSelect],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDocumentClick = useCallback(
|
const handleDocumentClick = useCallback(
|
||||||
(doc, event) => {
|
(doc, event) => {
|
||||||
if (!doc) {
|
if (!doc || suppressDocumentClickRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleEntryClick({ type: EntryType.document, id: doc.id, document: doc }, event);
|
|
||||||
|
if (typeof onEntryPointer === 'function') {
|
||||||
|
onEntryPointer(
|
||||||
|
{ type: EntryType.document, id: doc.id, key: `document:${doc.id}`, document: doc },
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[handleEntryClick],
|
[onEntryPointer],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFolderClick = useCallback(
|
const handleFolderClick = useCallback(
|
||||||
@@ -297,9 +341,24 @@ const DocumentsPanel = ({
|
|||||||
if (!folder) {
|
if (!folder) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleEntryClick({ type: EntryType.folder, id: folder.id, folder }, event);
|
|
||||||
|
if (typeof onEntryPointer === 'function') {
|
||||||
|
onEntryPointer(
|
||||||
|
{ type: EntryType.folder, id: folder.id, key: `folder:${folder.id}`, folder },
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isPointerModifierEvent(event)
|
||||||
|
&& isPrimaryPointerEvent(event)
|
||||||
|
&& scrollRef.current
|
||||||
|
) {
|
||||||
|
scrollRef.current.focus({ preventScroll: true });
|
||||||
|
onFocusedRowChange?.(`folder:${folder.id}`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[handleEntryClick],
|
[onEntryPointer, onFocusedRowChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDocumentDragStartLocal = useCallback(
|
const handleDocumentDragStartLocal = useCallback(
|
||||||
@@ -345,7 +404,8 @@ const DocumentsPanel = ({
|
|||||||
}, [breadcrumbEntries, currentFolderName, onFolderSelect]);
|
}, [breadcrumbEntries, currentFolderName, onFolderSelect]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<>
|
||||||
|
<section
|
||||||
className={`documents-panel documents-panel--view-${isGridView ? 'grid' : 'list'}`}
|
className={`documents-panel documents-panel--view-${isGridView ? 'grid' : 'list'}`}
|
||||||
>
|
>
|
||||||
{showHeader ? (
|
{showHeader ? (
|
||||||
@@ -455,7 +515,7 @@ const DocumentsPanel = ({
|
|||||||
onFolderDragStart={onFolderDragStart}
|
onFolderDragStart={onFolderDragStart}
|
||||||
onFolderDragEnd={onFolderDragEnd}
|
onFolderDragEnd={onFolderDragEnd}
|
||||||
onDocumentClick={handleDocumentClick}
|
onDocumentClick={handleDocumentClick}
|
||||||
onDocumentOpen={onDocumentOpen}
|
onDocumentActivate={handleDocumentActivate}
|
||||||
onDocumentDragStart={handleDocumentDragStartLocal}
|
onDocumentDragStart={handleDocumentDragStartLocal}
|
||||||
onDocumentDragEnd={handleDocumentDragEndLocal}
|
onDocumentDragEnd={handleDocumentDragEndLocal}
|
||||||
onDocumentTagDragOver={handleDocumentTagDragOver}
|
onDocumentTagDragOver={handleDocumentTagDragOver}
|
||||||
@@ -492,7 +552,7 @@ const DocumentsPanel = ({
|
|||||||
onFolderDragEnd={onFolderDragEnd}
|
onFolderDragEnd={onFolderDragEnd}
|
||||||
onFolderRename={onFolderRename}
|
onFolderRename={onFolderRename}
|
||||||
onDocumentClick={handleDocumentClick}
|
onDocumentClick={handleDocumentClick}
|
||||||
onDocumentOpen={onDocumentOpen}
|
onDocumentActivate={handleDocumentActivate}
|
||||||
onDocumentDragStart={handleDocumentDragStartLocal}
|
onDocumentDragStart={handleDocumentDragStartLocal}
|
||||||
onDocumentDragEnd={handleDocumentDragEndLocal}
|
onDocumentDragEnd={handleDocumentDragEndLocal}
|
||||||
onDocumentTagDragOver={handleDocumentTagDragOver}
|
onDocumentTagDragOver={handleDocumentTagDragOver}
|
||||||
@@ -516,6 +576,12 @@ const DocumentsPanel = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
<PreviewZoomOverlay
|
||||||
|
open={Boolean(previewDocId)}
|
||||||
|
display={previewDisplay}
|
||||||
|
onClose={closePreviewOverlay}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
export const isPointerModifierEvent = (event) =>
|
||||||
|
Boolean(event && (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey));
|
||||||
|
|
||||||
|
export const isPrimaryPointerEvent = (event) => {
|
||||||
|
if (!event) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof event.button === 'number' && event.button !== 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const type = typeof event.type === 'string' ? event.type.toLowerCase() : '';
|
||||||
|
return type === 'click' || type === 'pointerdown' || type === 'pointerup';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useEntryPointerHandler = ({
|
||||||
|
resolveDocumentRowKey,
|
||||||
|
resolveFolderRowKey,
|
||||||
|
onSelectDocument,
|
||||||
|
onSelectFolder,
|
||||||
|
}) =>
|
||||||
|
useCallback(
|
||||||
|
(entry, event) => {
|
||||||
|
if (!entry || !entry.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, id } = entry;
|
||||||
|
if (type !== 'document' && type !== 'folder') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowKey = entry.key
|
||||||
|
|| (type === 'document' ? resolveDocumentRowKey?.(id) : resolveFolderRowKey?.(id));
|
||||||
|
if (!rowKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifierClick = isPointerModifierEvent(event);
|
||||||
|
const primaryClick = isPrimaryPointerEvent(event);
|
||||||
|
|
||||||
|
if (type === 'document') {
|
||||||
|
if (typeof onSelectDocument === 'function') {
|
||||||
|
onSelectDocument(id, event, { modifierClick, primaryClick, rowKey });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof onSelectFolder === 'function') {
|
||||||
|
onSelectFolder(id, event, { modifierClick, primaryClick, rowKey });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
resolveDocumentRowKey,
|
||||||
|
resolveFolderRowKey,
|
||||||
|
onSelectDocument,
|
||||||
|
onSelectFolder,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export default useEntryPointerHandler;
|
||||||
Reference in New Issue
Block a user