Files
papercrate/frontend/src/desktop/pointer/pointerUtils.ts
T

207 lines
6.9 KiB
TypeScript

import { safeInvoke } from '../events';
import type { DocumentId } from '../../types/identifiers';
import {
CLICK_ACTIONS,
DRAG_ACTIONS,
LONG_PRESS_DURATION_MS,
POINTER_DRAG_THRESHOLD_SQUARED,
STACK_HIT_EPSILON,
} from '../../constants/desktop';
export { CLICK_ACTIONS, DRAG_ACTIONS, STACK_HIT_EPSILON, POINTER_DRAG_THRESHOLD_SQUARED, LONG_PRESS_DURATION_MS };
export type ClickAction = (typeof CLICK_ACTIONS)[keyof typeof CLICK_ACTIONS];
export type DragAction = (typeof DRAG_ACTIONS)[keyof typeof DRAG_ACTIONS];
export const withinThreshold = (dx: number, dy: number, thresholdSquared: number): boolean => (dx * dx + dy * dy) <= thresholdSquared;
interface PointerIntentArgs {
doc: { id: string };
entryDescriptor: unknown;
selectedDocumentIds: Array<string>;
metaKey: boolean;
pointerButton?: number;
pointerType?: string;
stackHits?: string[] | null;
}
export interface PointerIntent {
docId: DocumentId;
entryDescriptor: unknown;
pointerType?: string;
pointerButton?: number;
selectedAtDown: boolean;
selectionCountAtDown: number;
metaKey: boolean;
clickAction: ClickAction;
dragAction: DragAction;
stackDocIdsForDrag: string[] | null;
stackDocIdsForClick: string[] | null;
stackReplaceOnClick: boolean;
stackReplaceOnDrag: boolean;
clickSelectionApplied: boolean;
stackSelectionApplied: boolean;
longPressTriggered: boolean;
optimisticSelection: string[];
}
export const createPointerIntent = ({
doc,
entryDescriptor,
selectedDocumentIds,
metaKey,
pointerButton,
pointerType,
stackHits,
}: PointerIntentArgs): PointerIntent => {
const alreadySelected = selectedDocumentIds.includes(doc.id);
const selectionCount = Array.isArray(selectedDocumentIds) ? selectedDocumentIds.length : 0;
let clickAction: ClickAction = CLICK_ACTIONS.none;
let dragAction: DragAction = DRAG_ACTIONS.none;
if (metaKey) {
clickAction = CLICK_ACTIONS.addStack;
dragAction = DRAG_ACTIONS.dragSelection;
} else if (alreadySelected) {
clickAction = CLICK_ACTIONS.openDetail;
dragAction = selectionCount > 1 ? DRAG_ACTIONS.dragSelection : DRAG_ACTIONS.dragSelectSingle;
} else {
clickAction = CLICK_ACTIONS.selectSingle;
dragAction = DRAG_ACTIONS.dragSelectSingle;
}
const stackList: string[] = Array.isArray(stackHits) && stackHits.length > 0
? stackHits.map((value) => String(value))
: [String(doc.id)];
const stackDocIdsForClick = clickAction === CLICK_ACTIONS.addStack ? stackList : null;
const stackDocIdsForDrag = metaKey ? stackList : null;
// Calculate optimistic selection
let optimisticSelection: string[] = [];
if (metaKey) {
// Additive selection (stack or single)
const currentSelection = new Set(selectedDocumentIds);
stackList.forEach(id => currentSelection.add(id));
optimisticSelection = Array.from(currentSelection);
} else if (alreadySelected) {
// Already selected: keep current selection
optimisticSelection = [...selectedDocumentIds];
} else {
// New single selection
optimisticSelection = [doc.id];
}
return {
docId: doc.id,
entryDescriptor,
pointerType,
pointerButton,
selectedAtDown: alreadySelected,
selectionCountAtDown: selectionCount,
metaKey,
clickAction,
dragAction,
stackDocIdsForDrag,
stackDocIdsForClick,
stackReplaceOnClick: clickAction === CLICK_ACTIONS.addStack,
stackReplaceOnDrag: false,
clickSelectionApplied: false,
stackSelectionApplied: false,
longPressTriggered: false,
optimisticSelection,
};
};
export const applyClickPlanImmediately = ({ intent, event, onEntryPointer, onDocumentStackSelect, onSelect }: {
intent: PointerIntent;
event?: unknown;
onEntryPointer?: (descriptor: unknown, event?: unknown) => void;
onDocumentStackSelect?: (docIds: string[], event?: unknown, options?: { replace?: boolean }) => void;
onSelect?: (descriptor: unknown, event?: unknown) => void;
}) => {
switch (intent.clickAction) {
case CLICK_ACTIONS.selectSingle:
case CLICK_ACTIONS.addCard:
if (onSelect) {
safeInvoke(onSelect, intent.entryDescriptor, event);
} else {
safeInvoke(onEntryPointer, intent.entryDescriptor, event);
}
intent.clickSelectionApplied = true;
break;
case CLICK_ACTIONS.addStack:
if (Array.isArray(intent.stackDocIdsForClick) && intent.stackDocIdsForClick.length > 0) {
// Use onSelect for stack selection (batch)
if (onSelect) {
// Map docIds to descriptors if necessary, or just pass IDs if onSelect handles it.
// The current onSelect adapter in DesktopWorkspace expects { id } objects or just IDs?
// Let's assume it expects descriptors like selectSingle.
const descriptors = intent.stackDocIdsForClick.map(id => ({
type: 'document',
id,
key: `document:${id}`,
}));
safeInvoke(onSelect, descriptors, event);
} else {
// Fallback to legacy if onSelect not provided (shouldn't happen in new flow)
safeInvoke(
onDocumentStackSelect,
intent.stackDocIdsForClick,
event,
{ replace: intent.stackReplaceOnClick },
);
}
intent.clickSelectionApplied = true;
intent.stackSelectionApplied = true;
}
break;
case CLICK_ACTIONS.openDetail:
default:
intent.clickSelectionApplied = true;
break;
}
};
export const finalizeClickSelection = ({ intent, event, onEntryPointer, onDocumentStackSelect, onSelect }: {
intent: PointerIntent;
event?: unknown;
onEntryPointer?: (descriptor: unknown, event?: unknown) => void;
onDocumentStackSelect?: (docIds: string[], event?: unknown, options?: { replace?: boolean }) => void;
onSelect?: (descriptor: unknown, event?: unknown) => void;
}) => {
if (!intent || intent.clickSelectionApplied) {
return;
}
applyClickPlanImmediately({ intent, event, onEntryPointer, onDocumentStackSelect, onSelect });
};
export const applyLongPressSelection = ({ intent, stackDocIds, syntheticEvent, onDocumentStackSelect }: {
intent: PointerIntent;
stackDocIds?: string[] | null;
syntheticEvent?: unknown;
onDocumentStackSelect?: (docIds: string[], event?: unknown, options?: { replace?: boolean }) => void;
}) => {
if (!intent) {
return;
}
const stackCopy: string[] = Array.isArray(stackDocIds) && stackDocIds.length > 0
? stackDocIds.map((value) => String(value))
: [String(intent.docId)];
safeInvoke(onDocumentStackSelect, stackCopy, syntheticEvent, { replace: true });
intent.clickAction = CLICK_ACTIONS.addStack;
intent.dragAction = DRAG_ACTIONS.dragSelectStack;
intent.stackDocIdsForClick = stackCopy;
intent.stackDocIdsForDrag = stackCopy;
intent.stackReplaceOnClick = true;
intent.stackReplaceOnDrag = true;
intent.clickSelectionApplied = true;
intent.stackSelectionApplied = true;
intent.longPressTriggered = true;
};