207 lines
6.9 KiB
TypeScript
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;
|
|
};
|