mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-30 20:06:30 +01:00
Fixes
This commit is contained in:
parent
7de8347472
commit
3bd1c9f83a
@ -1,7 +1,7 @@
|
||||
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { Box } from '@mantine/core';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { GRID_CONSTANTS } from '@app/components/pageEditor/constants';
|
||||
import { GRID_CONSTANTS } from './constants';
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
@ -24,20 +24,7 @@ interface DragDropItem {
|
||||
interface DragDropGridProps<T extends DragDropItem> {
|
||||
items: T[];
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
||||
renderItem: (
|
||||
item: T,
|
||||
index: number,
|
||||
refs: React.MutableRefObject<Map<string, HTMLDivElement>>,
|
||||
boxSelectedIds: string[],
|
||||
clearBoxSelection: () => void,
|
||||
getBoxSelection: () => string[],
|
||||
activeId: string | null,
|
||||
activeDragIds: string[],
|
||||
justMoved: boolean,
|
||||
isOver: boolean,
|
||||
dragHandleProps?: any,
|
||||
zoomLevel?: number,
|
||||
) => React.ReactNode;
|
||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>, boxSelectedIds: string[], clearBoxSelection: () => void, getBoxSelection: () => string[], activeId: string | null, activeDragIds: string[], justMoved: boolean, isOver: boolean, dragHandleProps?: any, zoomLevel?: number) => React.ReactNode;
|
||||
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||
zoomLevel?: number;
|
||||
}
|
||||
@ -154,6 +141,7 @@ function resolveTargetIndex<T extends DragDropItem>(
|
||||
return null;
|
||||
}
|
||||
|
||||
// Lightweight wrapper that handles dnd-kit hooks for each visible item
|
||||
interface DraggableItemProps<T extends DragDropItem> {
|
||||
item: T;
|
||||
index: number;
|
||||
@ -166,38 +154,11 @@ interface DraggableItemProps<T extends DragDropItem> {
|
||||
justMoved: boolean;
|
||||
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||
onUpdateDropTarget: (itemId: string | null) => void;
|
||||
renderItem: (
|
||||
item: T,
|
||||
index: number,
|
||||
refs: React.MutableRefObject<Map<string, HTMLDivElement>>,
|
||||
boxSelectedIds: string[],
|
||||
clearBoxSelection: () => void,
|
||||
getBoxSelection: () => string[],
|
||||
activeId: string | null,
|
||||
activeDragIds: string[],
|
||||
justMoved: boolean,
|
||||
isOver: boolean,
|
||||
dragHandleProps?: any,
|
||||
zoomLevel?: number,
|
||||
) => React.ReactNode;
|
||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>, boxSelectedIds: string[], clearBoxSelection: () => void, getBoxSelection: () => string[], activeId: string | null, activeDragIds: string[], justMoved: boolean, isOver: boolean, dragHandleProps?: any, zoomLevel?: number) => React.ReactNode;
|
||||
zoomLevel: number;
|
||||
}
|
||||
|
||||
const DraggableItem = <T extends DragDropItem>({
|
||||
item,
|
||||
index,
|
||||
itemRefs,
|
||||
boxSelectedPageIds,
|
||||
clearBoxSelection,
|
||||
getBoxSelection,
|
||||
activeId,
|
||||
activeDragIds,
|
||||
justMoved,
|
||||
getThumbnailData,
|
||||
renderItem,
|
||||
onUpdateDropTarget,
|
||||
zoomLevel,
|
||||
}: DraggableItemProps<T>) => {
|
||||
const DraggableItem = <T extends DragDropItem>({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, getThumbnailData, renderItem, onUpdateDropTarget, zoomLevel }: DraggableItemProps<T>) => {
|
||||
const { attributes, listeners, setNodeRef: setDraggableRef } = useDraggable({
|
||||
id: item.id,
|
||||
data: {
|
||||
@ -214,19 +175,20 @@ const DraggableItem = <T extends DragDropItem>({
|
||||
if (imgElement?.src) {
|
||||
return {
|
||||
src: imgElement.src,
|
||||
rotation: imgElement.dataset.originalRotation ? parseInt(imgElement.dataset.originalRotation) : 0,
|
||||
rotation: imgElement.dataset.originalRotation ? parseInt(imgElement.dataset.originalRotation) : 0
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { setNodeRef: setDroppableRef, isOver } = useDroppable({
|
||||
id: item.id,
|
||||
data: { index, pageNumber: index + 1 },
|
||||
data: { index, pageNumber: index + 1 }
|
||||
});
|
||||
|
||||
// Notify parent when hover state changes
|
||||
React.useEffect(() => {
|
||||
if (isOver) {
|
||||
onUpdateDropTarget(item.id);
|
||||
@ -238,149 +200,140 @@ const DraggableItem = <T extends DragDropItem>({
|
||||
const setNodeRef = useCallback((element: HTMLDivElement | null) => {
|
||||
setDraggableRef(element);
|
||||
setDroppableRef(element);
|
||||
if (element) {
|
||||
itemRefs.current.set(item.id, element);
|
||||
} else {
|
||||
itemRefs.current.delete(item.id);
|
||||
}
|
||||
}, [item.id, setDraggableRef, setDroppableRef]);
|
||||
|
||||
return renderItem(
|
||||
item,
|
||||
index,
|
||||
itemRefs,
|
||||
boxSelectedPageIds,
|
||||
clearBoxSelection,
|
||||
getBoxSelection,
|
||||
activeId,
|
||||
activeDragIds,
|
||||
justMoved,
|
||||
isOver,
|
||||
{
|
||||
...attributes,
|
||||
...listeners,
|
||||
ref: setNodeRef,
|
||||
onPointerDown: (event: React.PointerEvent) => {
|
||||
event.preventDefault();
|
||||
listeners.onPointerDown?.(event as any);
|
||||
},
|
||||
},
|
||||
zoomLevel,
|
||||
);
|
||||
};
|
||||
|
||||
interface DragOverlayContentProps<T extends DragDropItem> {
|
||||
activeItem: T | null;
|
||||
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||
zoomLevel: number;
|
||||
}
|
||||
|
||||
const DragOverlayContent = <T extends DragDropItem>({ activeItem, getThumbnailData, zoomLevel }: DragOverlayContentProps<T>) => {
|
||||
const thumbnailData = activeItem && getThumbnailData ? getThumbnailData(activeItem.id) : null;
|
||||
|
||||
if (!activeItem) {
|
||||
return null;
|
||||
}
|
||||
}, [setDraggableRef, setDroppableRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
transform: zoomLevel !== 1 ? `scale(${zoomLevel})` : undefined,
|
||||
transformOrigin: 'top left',
|
||||
pointerEvents: 'none',
|
||||
backgroundColor: 'var(--mantine-color-body)',
|
||||
border: '1px solid var(--mantine-color-border)',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
{thumbnailData?.src ? (
|
||||
<img
|
||||
src={thumbnailData.src}
|
||||
alt="drag-preview"
|
||||
style={{
|
||||
display: 'block',
|
||||
width: `calc(${GRID_CONSTANTS.ITEM_WIDTH} * ${zoomLevel})`,
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: GRID_CONSTANTS.ITEM_WIDTH,
|
||||
height: GRID_CONSTANTS.ITEM_HEIGHT,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'var(--mantine-color-dimmed)',
|
||||
}}
|
||||
>
|
||||
Moving page...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
{renderItem(item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, isOver, { ref: setNodeRef, ...attributes, ...listeners }, zoomLevel)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DragDropGrid = <T extends DragDropItem>({
|
||||
items,
|
||||
onReorderPages,
|
||||
renderItem,
|
||||
onReorderPages,
|
||||
getThumbnailData,
|
||||
zoomLevel = 1,
|
||||
zoomLevel = 1.0,
|
||||
}: DragDropGridProps<T>) => {
|
||||
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const boxSelectionRef = useRef<Set<string>>(new Set());
|
||||
const getBoxSelection = useCallback(() => Array.from(boxSelectionRef.current), []);
|
||||
const clearBoxSelection = useCallback(() => {
|
||||
boxSelectionRef.current.clear();
|
||||
const getScrollElement = useCallback(() => {
|
||||
return containerRef.current?.closest('[data-scrolling-container]') as HTMLElement | null;
|
||||
}, []);
|
||||
|
||||
const [itemsPerRow, setItemsPerRow] = useState(4);
|
||||
const overscan = items.length > 1000 ? GRID_CONSTANTS.OVERSCAN_LARGE : GRID_CONSTANTS.OVERSCAN_SMALL;
|
||||
const { filteredItems: visibleItems, filteredToOriginalIndex } = useMemo(() => {
|
||||
const filtered: T[] = [];
|
||||
const indexMap: number[] = [];
|
||||
|
||||
items.forEach((item, index) => {
|
||||
if (!item.isPlaceholder) {
|
||||
filtered.push(item);
|
||||
indexMap.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
return { filteredItems: filtered, filteredToOriginalIndex: indexMap };
|
||||
}, [items]);
|
||||
|
||||
// Box selection state
|
||||
const [boxSelectStart, setBoxSelectStart] = useState<{ x: number; y: number } | null>(null);
|
||||
const [boxSelectEnd, setBoxSelectEnd] = useState<{ x: number; y: number } | null>(null);
|
||||
const [isBoxSelecting, setIsBoxSelecting] = useState(false);
|
||||
const [boxSelectedPageIds, setBoxSelectedPageIds] = useState<string[]>([]);
|
||||
const [justMovedIds, setJustMovedIds] = useState<string[]>([]);
|
||||
const highlightTimeoutRef = useRef<number | null>(null);
|
||||
|
||||
// Drag state
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [activeDragIds, setActiveDragIds] = useState<string[]>([]);
|
||||
const [justMovedIds, setJustMovedIds] = useState<Set<string>>(new Set());
|
||||
const [dropHint, setDropHint] = useState<DropHint>({ hoveredId: null, dropSide: null });
|
||||
const [dropTargetId, setDropTargetId] = useState<string | null>(null);
|
||||
const [dragPreview, setDragPreview] = useState<{ src: string; rotation: number } | null>(null);
|
||||
const [hoveredItemId, setHoveredItemId] = useState<string | null>(null);
|
||||
const [dropSide, setDropSide] = useState<DropSide>(null);
|
||||
|
||||
// Configure sensors for dnd-kit with activation constraint
|
||||
// Require 10px movement before drag starts to allow clicks for selection
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
distance: 10,
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
// Throttled pointer move handler for drop indicator
|
||||
// Calculate drop position based on cursor location relative to ALL items, not just hovered item
|
||||
useEffect(() => {
|
||||
if (!activeId) {
|
||||
setDropSide(null);
|
||||
setHoveredItemId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let rafId: number | null = null;
|
||||
|
||||
const handlePointerMove = (e: PointerEvent) => {
|
||||
// Use the actual cursor position (pointer coordinates)
|
||||
const cursorX = e.clientX;
|
||||
const cursorY = e.clientY;
|
||||
|
||||
if (rafId === null) {
|
||||
rafId = requestAnimationFrame(() => {
|
||||
const hint = resolveDropHint(activeId, itemRefs, cursorX, cursorY);
|
||||
setHoveredItemId(hint.hoveredId);
|
||||
setDropSide(hint.dropSide);
|
||||
rafId = null;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('pointermove', handlePointerMove, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
if (rafId !== null) {
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
};
|
||||
}, [activeId]);
|
||||
|
||||
// Responsive grid configuration
|
||||
const [itemsPerRow, setItemsPerRow] = useState(4);
|
||||
const OVERSCAN = visibleItems.length > 1000 ? GRID_CONSTANTS.OVERSCAN_LARGE : GRID_CONSTANTS.OVERSCAN_SMALL;
|
||||
|
||||
// Calculate items per row based on container width
|
||||
const calculateItemsPerRow = useCallback(() => {
|
||||
if (!containerRef.current) return 4;
|
||||
if (!containerRef.current) return 4; // Default fallback
|
||||
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
if (containerWidth === 0) return 4;
|
||||
if (containerWidth === 0) return 4; // Container not measured yet
|
||||
|
||||
// Convert rem to pixels for calculation
|
||||
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
const ITEM_WIDTH = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx;
|
||||
const ITEM_GAP = parseFloat(GRID_CONSTANTS.ITEM_GAP) * remToPx;
|
||||
const ITEM_WIDTH = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx * zoomLevel;
|
||||
const ITEM_GAP = parseFloat(GRID_CONSTANTS.ITEM_GAP) * remToPx * zoomLevel;
|
||||
|
||||
const availableWidth = containerWidth - ITEM_GAP;
|
||||
// Calculate how many items fit: (width - gap) / (itemWidth + gap)
|
||||
const availableWidth = containerWidth - ITEM_GAP; // Account for first gap
|
||||
const itemWithGap = ITEM_WIDTH + ITEM_GAP;
|
||||
const calculated = Math.floor(availableWidth / itemWithGap);
|
||||
|
||||
return Math.max(1, calculated);
|
||||
}, []);
|
||||
return Math.max(1, calculated); // At least 1 item per row
|
||||
}, [zoomLevel]);
|
||||
|
||||
// Update items per row when container resizes or zoom changes
|
||||
useEffect(() => {
|
||||
const updateLayout = () => {
|
||||
const newItemsPerRow = calculateItemsPerRow();
|
||||
setItemsPerRow(newItemsPerRow);
|
||||
};
|
||||
|
||||
// Initial calculation
|
||||
updateLayout();
|
||||
|
||||
// Listen for window resize
|
||||
window.addEventListener('resize', updateLayout);
|
||||
|
||||
// Use ResizeObserver for container size changes
|
||||
const resizeObserver = new ResizeObserver(updateLayout);
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
@ -390,159 +343,285 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
window.removeEventListener('resize', updateLayout);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [calculateItemsPerRow]);
|
||||
|
||||
const filteredItems = useMemo(() => items.filter(item => !item.isPlaceholder), [items]);
|
||||
const filteredToOriginalIndex = useMemo(() => {
|
||||
const result: number[] = [];
|
||||
items.forEach((item, index) => {
|
||||
if (!item.isPlaceholder) {
|
||||
result.push(index);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}, [items]);
|
||||
}, [calculateItemsPerRow, zoomLevel]);
|
||||
|
||||
// Virtualization with react-virtual library
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: Math.ceil(filteredItems.length / itemsPerRow),
|
||||
getScrollElement: () => containerRef.current?.closest('[data-scrolling-container]') as Element,
|
||||
count: Math.ceil(visibleItems.length / itemsPerRow),
|
||||
getScrollElement,
|
||||
estimateSize: () => {
|
||||
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
return parseFloat(GRID_CONSTANTS.ITEM_HEIGHT) * remToPx;
|
||||
return parseFloat(GRID_CONSTANTS.ITEM_HEIGHT) * remToPx * zoomLevel;
|
||||
},
|
||||
overscan,
|
||||
overscan: OVERSCAN,
|
||||
});
|
||||
|
||||
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
const itemWidth = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx;
|
||||
const itemGap = parseFloat(GRID_CONSTANTS.ITEM_GAP) * remToPx;
|
||||
const gridWidth = itemsPerRow * itemWidth + (itemsPerRow - 1) * itemGap;
|
||||
// Re-measure virtualizer when zoom or items per row changes
|
||||
useEffect(() => {
|
||||
rowVirtualizer.measure();
|
||||
}, [zoomLevel, itemsPerRow]);
|
||||
|
||||
const activeItem = activeId ? items.find(item => item.id === activeId) || null : null;
|
||||
// Cleanup highlight timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (highlightTimeoutRef.current) {
|
||||
window.clearTimeout(highlightTimeoutRef.current);
|
||||
highlightTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||
const { active } = event;
|
||||
setActiveId(active.id as string);
|
||||
// Box selection handlers
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
if (e.button !== 0) return; // Only respond to primary button
|
||||
|
||||
const activeElement = itemRefs.current.get(active.id as string);
|
||||
if (activeElement) {
|
||||
activeElement.style.opacity = '0.2';
|
||||
}
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const selectedIds = getBoxSelection();
|
||||
if (selectedIds.includes(active.id as string)) {
|
||||
setActiveDragIds(selectedIds);
|
||||
} else {
|
||||
setActiveDragIds([active.id as string]);
|
||||
}
|
||||
}, [getBoxSelection]);
|
||||
const clickTarget = e.target as Node;
|
||||
let clickedPageId: string | null = null;
|
||||
|
||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active } = event;
|
||||
const activeIndex = filteredItems.findIndex(item => item.id === active.id);
|
||||
const fallbackIndex = activeIndex !== -1 ? filteredToOriginalIndex[activeIndex] : null;
|
||||
itemRefs.current.forEach((element, pageId) => {
|
||||
if (element.contains(clickTarget)) {
|
||||
clickedPageId = pageId;
|
||||
}
|
||||
});
|
||||
|
||||
if (!dropHint.hoveredId && fallbackIndex === null) {
|
||||
setDropHint({ hoveredId: null, dropSide: null });
|
||||
setActiveId(null);
|
||||
setActiveDragIds([]);
|
||||
if (clickedPageId) {
|
||||
// Clicking directly on a page shouldn't initiate box selection
|
||||
// but clear previous box selection if clicking outside current group
|
||||
if (boxSelectedPageIds.length > 0 && !boxSelectedPageIds.includes(clickedPageId)) {
|
||||
setBoxSelectedPageIds([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const targetIndex = resolveTargetIndex(
|
||||
dropHint.hoveredId,
|
||||
dropHint.dropSide,
|
||||
filteredItems,
|
||||
e.preventDefault();
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
setIsBoxSelecting(true);
|
||||
setBoxSelectStart({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
||||
setBoxSelectEnd({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
||||
setBoxSelectedPageIds([]);
|
||||
}, [boxSelectedPageIds]);
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
||||
if (!isBoxSelecting || !boxSelectStart) return;
|
||||
|
||||
const rect = containerRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
setBoxSelectEnd({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
||||
|
||||
// Calculate which pages intersect with selection box
|
||||
const boxLeft = Math.min(boxSelectStart.x, e.clientX - rect.left);
|
||||
const boxRight = Math.max(boxSelectStart.x, e.clientX - rect.left);
|
||||
const boxTop = Math.min(boxSelectStart.y, e.clientY - rect.top);
|
||||
const boxBottom = Math.max(boxSelectStart.y, e.clientY - rect.top);
|
||||
|
||||
const selectedIds: string[] = [];
|
||||
itemRefs.current.forEach((pageEl, pageId) => {
|
||||
const pageRect = pageEl.getBoundingClientRect();
|
||||
const pageLeft = pageRect.left - rect.left;
|
||||
const pageRight = pageRect.right - rect.left;
|
||||
const pageTop = pageRect.top - rect.top;
|
||||
const pageBottom = pageRect.bottom - rect.top;
|
||||
|
||||
// Check if page intersects with selection box
|
||||
const intersects = !(
|
||||
pageRight < boxLeft ||
|
||||
pageLeft > boxRight ||
|
||||
pageBottom < boxTop ||
|
||||
pageTop > boxBottom
|
||||
);
|
||||
|
||||
if (intersects) {
|
||||
selectedIds.push(pageId);
|
||||
}
|
||||
});
|
||||
|
||||
setBoxSelectedPageIds(selectedIds);
|
||||
}, [isBoxSelecting, boxSelectStart]);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
if (isBoxSelecting) {
|
||||
// Keep box-selected pages highlighted (don't clear boxSelectedPageIds yet)
|
||||
// They will remain highlighted until next interaction
|
||||
setIsBoxSelecting(false);
|
||||
setBoxSelectStart(null);
|
||||
setBoxSelectEnd(null);
|
||||
}
|
||||
}, [isBoxSelecting]);
|
||||
|
||||
// Function to clear box selection (exposed to child components)
|
||||
const clearBoxSelection = useCallback(() => {
|
||||
setBoxSelectedPageIds([]);
|
||||
}, []);
|
||||
|
||||
// Function to get current box selection (exposed to child components)
|
||||
const getBoxSelection = useCallback(() => {
|
||||
return boxSelectedPageIds;
|
||||
}, [boxSelectedPageIds]);
|
||||
|
||||
// Handle drag start
|
||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||
const activeId = event.active.id as string;
|
||||
setActiveId(activeId);
|
||||
|
||||
// Call the getter function to get fresh thumbnail data
|
||||
const getThumbnail = event.active.data.current?.getThumbnail;
|
||||
if (getThumbnail) {
|
||||
const thumbnailData = getThumbnail();
|
||||
if (thumbnailData?.src) {
|
||||
setDragPreview({ src: thumbnailData.src, rotation: thumbnailData.rotation });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setDragPreview(null);
|
||||
}, []);
|
||||
|
||||
|
||||
// Handle drag cancel
|
||||
const handleDragCancel = useCallback(() => {
|
||||
setActiveId(null);
|
||||
setDragPreview(null);
|
||||
setHoveredItemId(null);
|
||||
setDropSide(null);
|
||||
}, []);
|
||||
|
||||
// Handle drag end
|
||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
const finalDropSide = dropSide;
|
||||
setActiveId(null);
|
||||
setDragPreview(null);
|
||||
setHoveredItemId(null);
|
||||
setDropSide(null);
|
||||
|
||||
if (!over || active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get data from hooks
|
||||
const activeData = active.data.current;
|
||||
if (!activeData) return;
|
||||
|
||||
const sourcePageNumber = activeData.pageNumber;
|
||||
|
||||
const overData = over?.data.current;
|
||||
let targetIndex = resolveTargetIndex(
|
||||
hoveredItemId,
|
||||
finalDropSide,
|
||||
visibleItems,
|
||||
filteredToOriginalIndex,
|
||||
items.length,
|
||||
fallbackIndex,
|
||||
overData ? overData.index : null
|
||||
);
|
||||
|
||||
if (targetIndex !== null) {
|
||||
const pageNumber = filteredItems.findIndex(item => item.id === active.id) + 1;
|
||||
if (pageNumber > 0) {
|
||||
onReorderPages(pageNumber, targetIndex, activeDragIds);
|
||||
if (targetIndex === null) return;
|
||||
if (targetIndex < 0) targetIndex = 0;
|
||||
if (targetIndex > items.length) targetIndex = items.length;
|
||||
|
||||
const updatedJustMoved = new Set<string>(activeDragIds);
|
||||
setJustMovedIds(updatedJustMoved);
|
||||
// Check if this page is box-selected
|
||||
const isBoxSelected = boxSelectedPageIds.includes(active.id as string);
|
||||
const pagesToDrag = isBoxSelected && boxSelectedPageIds.length > 0 ? boxSelectedPageIds : undefined;
|
||||
|
||||
setTimeout(() => {
|
||||
setJustMovedIds(prev => {
|
||||
const next = new Set(prev);
|
||||
activeDragIds.forEach(id => next.delete(id));
|
||||
return next;
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
// Call reorder with page number and target index
|
||||
onReorderPages(sourcePageNumber, targetIndex, pagesToDrag);
|
||||
|
||||
// Highlight moved pages briefly
|
||||
const movedIds = pagesToDrag ?? [active.id as string];
|
||||
setJustMovedIds(movedIds);
|
||||
if (highlightTimeoutRef.current) {
|
||||
window.clearTimeout(highlightTimeoutRef.current);
|
||||
}
|
||||
highlightTimeoutRef.current = window.setTimeout(() => {
|
||||
setJustMovedIds([]);
|
||||
highlightTimeoutRef.current = null;
|
||||
}, 1200);
|
||||
|
||||
setDropHint({ hoveredId: null, dropSide: null });
|
||||
setActiveId(null);
|
||||
setActiveDragIds([]);
|
||||
|
||||
const activeElement = itemRefs.current.get(active.id as string);
|
||||
if (activeElement) {
|
||||
activeElement.style.opacity = '';
|
||||
// Clear box selection after drag
|
||||
if (pagesToDrag) {
|
||||
clearBoxSelection();
|
||||
}
|
||||
}, [activeDragIds, dropHint, filteredItems, filteredToOriginalIndex, items.length, onReorderPages]);
|
||||
}, [boxSelectedPageIds, dropSide, hoveredItemId, visibleItems, filteredToOriginalIndex, items, onReorderPages, clearBoxSelection]);
|
||||
|
||||
const handleDragCancel = useCallback(() => {
|
||||
setDropHint({ hoveredId: null, dropSide: null });
|
||||
setActiveId(null);
|
||||
setActiveDragIds([]);
|
||||
}, []);
|
||||
// Calculate optimal width for centering
|
||||
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
const itemWidth = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx * zoomLevel;
|
||||
const itemGap = parseFloat(GRID_CONSTANTS.ITEM_GAP) * remToPx * zoomLevel;
|
||||
const gridWidth = itemsPerRow * itemWidth + (itemsPerRow - 1) * itemGap;
|
||||
|
||||
const handleDragMove = useCallback((event: DragStartEvent | DragEndEvent) => {
|
||||
const { active, delta } = event;
|
||||
if (!active) return;
|
||||
// Calculate selection box dimensions
|
||||
const selectionBoxStyle = isBoxSelecting && boxSelectStart && boxSelectEnd ? {
|
||||
position: 'absolute' as const,
|
||||
left: Math.min(boxSelectStart.x, boxSelectEnd.x),
|
||||
top: Math.min(boxSelectStart.y, boxSelectEnd.y),
|
||||
width: Math.abs(boxSelectEnd.x - boxSelectStart.x),
|
||||
height: Math.abs(boxSelectEnd.y - boxSelectStart.y),
|
||||
border: '2px dashed #3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
pointerEvents: 'none' as const,
|
||||
zIndex: 1000,
|
||||
} : null;
|
||||
|
||||
const referenceElement = itemRefs.current.get(active.id as string);
|
||||
if (!referenceElement) return;
|
||||
// Calculate drop indicator position
|
||||
const dropIndicatorStyle = useMemo(() => {
|
||||
if (!hoveredItemId || !dropSide || !activeId) return null;
|
||||
|
||||
const referenceRect = referenceElement.getBoundingClientRect();
|
||||
const cursorX = referenceRect.left + delta.x;
|
||||
const cursorY = referenceRect.top + delta.y;
|
||||
const element = itemRefs.current.get(hoveredItemId);
|
||||
const container = containerRef.current;
|
||||
if (!element || !container) return null;
|
||||
|
||||
const hint = resolveDropHint(active.id as string, itemRefs, cursorX, cursorY);
|
||||
setDropHint(hint);
|
||||
}, []);
|
||||
const itemRect = element.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
useEffect(() => {
|
||||
const scrollContainer = containerRef.current?.closest('[data-scrolling-container]');
|
||||
if (!scrollContainer) return;
|
||||
const top = itemRect.top - containerRect.top;
|
||||
const height = itemRect.height;
|
||||
const left = dropSide === 'left'
|
||||
? itemRect.left - containerRect.left - itemGap / 2
|
||||
: itemRect.right - containerRect.left + itemGap / 2;
|
||||
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
return {
|
||||
position: 'absolute' as const,
|
||||
left: `${left}px`,
|
||||
top: `${top}px`,
|
||||
width: '4px',
|
||||
height: `${height}px`,
|
||||
backgroundColor: 'rgba(96, 165, 250, 0.8)',
|
||||
borderRadius: '2px',
|
||||
zIndex: 1001,
|
||||
pointerEvents: 'none' as const,
|
||||
};
|
||||
}, [hoveredItemId, dropSide, activeId, itemGap, zoomLevel]);
|
||||
|
||||
scrollContainer.addEventListener('wheel', handleWheel, { passive: false });
|
||||
const activeDragIds = useMemo(() => {
|
||||
if (!activeId) return [];
|
||||
if (boxSelectedPageIds.includes(activeId)) {
|
||||
return boxSelectedPageIds;
|
||||
}
|
||||
return [activeId];
|
||||
}, [activeId, boxSelectedPageIds]);
|
||||
|
||||
return () => {
|
||||
scrollContainer.removeEventListener('wheel', handleWheel);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getDropIndicatorStyle = useCallback((itemId: string) => {
|
||||
if (dropHint.hoveredId !== itemId) {
|
||||
return {};
|
||||
const handleWheelWhileDragging = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
|
||||
if (!activeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dropHint.dropSide === 'left') {
|
||||
return {
|
||||
boxShadow: '-4px 0 0 0 var(--mantine-primary-color-filled)',
|
||||
};
|
||||
const scrollElement = getScrollElement();
|
||||
if (!scrollElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dropHint.dropSide === 'right') {
|
||||
return {
|
||||
boxShadow: '4px 0 0 0 var(--mantine-primary-color-filled)',
|
||||
};
|
||||
}
|
||||
scrollElement.scrollBy({
|
||||
top: event.deltaY,
|
||||
left: event.deltaX,
|
||||
});
|
||||
|
||||
return {};
|
||||
}, [dropHint]);
|
||||
event.preventDefault();
|
||||
}, [activeId, getScrollElement]);
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
@ -551,111 +630,152 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
onDragMove={handleDragMove}
|
||||
>
|
||||
<Box
|
||||
ref={containerRef}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onWheel={handleWheelWhileDragging}
|
||||
style={{
|
||||
// Basic container styles
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
margin: '0 auto',
|
||||
maxWidth: `${gridWidth}px`,
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const startIndex = virtualRow.index * itemsPerRow;
|
||||
const endIndex = Math.min(startIndex + itemsPerRow, filteredItems.length);
|
||||
const rowItems = filteredItems.slice(startIndex, endIndex);
|
||||
{/* Selection box overlay */}
|
||||
{selectionBoxStyle && <div style={selectionBoxStyle} />}
|
||||
|
||||
return (
|
||||
{/* Global drop indicator */}
|
||||
{dropIndicatorStyle && <div style={dropIndicatorStyle} />}
|
||||
|
||||
<div
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
maxWidth: `${gridWidth}px`,
|
||||
position: 'relative',
|
||||
margin: '0 auto',
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const startIndex = virtualRow.index * itemsPerRow;
|
||||
const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length);
|
||||
const rowItems = visibleItems.slice(startIndex, endIndex);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={virtualRow.index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
key={virtualRow.index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
display: 'flex',
|
||||
gap: `calc(${GRID_CONSTANTS.ITEM_GAP} * ${zoomLevel})`,
|
||||
justifyContent: 'flex-start',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: GRID_CONSTANTS.ITEM_GAP,
|
||||
justifyContent: 'flex-start',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{rowItems.map((item, itemIndex) => {
|
||||
const actualIndex = startIndex + itemIndex;
|
||||
const originalIndex = filteredToOriginalIndex[actualIndex];
|
||||
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
<DraggableItem
|
||||
item={item}
|
||||
index={originalIndex}
|
||||
itemRefs={itemRefs}
|
||||
boxSelectedPageIds={getBoxSelection()}
|
||||
clearBoxSelection={clearBoxSelection}
|
||||
getBoxSelection={getBoxSelection}
|
||||
activeId={activeId}
|
||||
activeDragIds={activeDragIds}
|
||||
justMoved={justMovedIds.has(item.id)}
|
||||
getThumbnailData={getThumbnailData}
|
||||
zoomLevel={zoomLevel}
|
||||
onUpdateDropTarget={setDropTargetId}
|
||||
renderItem={(...args) => {
|
||||
const node = renderItem(...args);
|
||||
const style = getDropIndicatorStyle(item.id);
|
||||
return <div style={style}>{node}</div>;
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{dropTargetId && dropHint.hoveredId === dropTargetId && dropHint.dropSide && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: dropHint.dropSide === 'left' ? '-0.25rem' : undefined,
|
||||
right: dropHint.dropSide === 'right' ? '-0.25rem' : undefined,
|
||||
width: '0.25rem',
|
||||
backgroundColor: 'var(--mantine-primary-color-filled)',
|
||||
borderRadius: '9999px',
|
||||
}}
|
||||
{rowItems.map((item, itemIndex) => {
|
||||
const actualIndex = startIndex + itemIndex;
|
||||
return (
|
||||
<DraggableItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
index={actualIndex}
|
||||
itemRefs={itemRefs}
|
||||
boxSelectedPageIds={boxSelectedPageIds}
|
||||
clearBoxSelection={clearBoxSelection}
|
||||
getBoxSelection={getBoxSelection}
|
||||
activeId={activeId}
|
||||
activeDragIds={activeDragIds}
|
||||
justMoved={justMovedIds.includes(item.id)}
|
||||
getThumbnailData={getThumbnailData}
|
||||
onUpdateDropTarget={setHoveredItemId}
|
||||
renderItem={renderItem}
|
||||
zoomLevel={zoomLevel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
<DragOverlay dropAnimation={null}>
|
||||
<DragOverlayContent
|
||||
activeItem={activeItem || null}
|
||||
getThumbnailData={getThumbnailData}
|
||||
zoomLevel={zoomLevel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{/* Drag Overlay */}
|
||||
<DragOverlay>
|
||||
{activeId && (
|
||||
<div style={{ position: 'relative', cursor: 'grabbing' }}>
|
||||
{/* Multi-page badge */}
|
||||
{boxSelectedPageIds.includes(activeId) && boxSelectedPageIds.length > 1 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
borderRadius: '50%',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
|
||||
zIndex: 1001
|
||||
}}
|
||||
>
|
||||
{boxSelectedPageIds.length}
|
||||
</div>
|
||||
)}
|
||||
{/* Just the thumbnail image */}
|
||||
{dragPreview ? (
|
||||
<img
|
||||
src={dragPreview.src}
|
||||
alt="Dragging"
|
||||
style={{
|
||||
width: `calc(20rem * ${zoomLevel})`,
|
||||
height: `calc(20rem * ${zoomLevel})`,
|
||||
objectFit: 'contain',
|
||||
transform: `rotate(${dragPreview.rotation}deg)`,
|
||||
pointerEvents: 'none',
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div style={{
|
||||
width: `calc(20rem * ${zoomLevel})`,
|
||||
height: `calc(20rem * ${zoomLevel})`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '48px',
|
||||
opacity: 0.5,
|
||||
}}>
|
||||
??
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export type { DragDropItem };
|
||||
export default DragDropGrid;
|
||||
|
||||
@ -75,6 +75,7 @@ const PageEditor = ({
|
||||
const fileObjectsRef = useRef(new Map<FileId, any>());
|
||||
const pagePositionCacheRef = useRef<Map<string, number>>(new Map());
|
||||
const pageNeighborCacheRef = useRef<Map<string, string | null>>(new Map());
|
||||
const gridItemRefsRef = useRef<React.MutableRefObject<Map<string, HTMLDivElement>> | null>(null);
|
||||
|
||||
const pageEditorFiles = useMemo(() => {
|
||||
const cache = fileObjectsRef.current;
|
||||
@ -1094,45 +1095,57 @@ const PageEditor = ({
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
// Calculate remToPx once outside the map to avoid layout thrashing
|
||||
const containerWidth = containerDimensions.width;
|
||||
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
const ITEM_WIDTH = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx;
|
||||
const ITEM_HEIGHT = parseFloat(GRID_CONSTANTS.ITEM_HEIGHT) * remToPx;
|
||||
const ITEM_GAP = parseFloat(GRID_CONSTANTS.ITEM_GAP) * remToPx;
|
||||
const refsMap = gridItemRefsRef.current?.current;
|
||||
const containerEl = gridContainerRef.current;
|
||||
if (!refsMap || !containerEl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const containerRect = containerEl.getBoundingClientRect();
|
||||
|
||||
return Array.from(splitPositions).map((position) => {
|
||||
const currentPage = displayedPages[position];
|
||||
if (!currentPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate items per row using DragDropGrid's logic
|
||||
const availableWidth = containerWidth - ITEM_GAP; // Account for first gap
|
||||
const itemWithGap = ITEM_WIDTH + ITEM_GAP;
|
||||
const itemsPerRow = Math.max(1, Math.floor(availableWidth / itemWithGap));
|
||||
const currentEl = refsMap.get(currentPage.id);
|
||||
if (!currentEl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate position within the grid (same as DragDropGrid)
|
||||
const row = Math.floor(position / itemsPerRow);
|
||||
const col = position % itemsPerRow;
|
||||
const currentRect = currentEl.getBoundingClientRect();
|
||||
const nextPage = displayedPages[position + 1];
|
||||
let lineLeft = currentRect.right;
|
||||
|
||||
// Position split line between pages (after the current page)
|
||||
// Calculate grid centering offset (same as DragDropGrid)
|
||||
const gridWidth = itemsPerRow * ITEM_WIDTH + (itemsPerRow - 1) * ITEM_GAP;
|
||||
const gridOffset = Math.max(0, (containerWidth - gridWidth) / 2);
|
||||
if (nextPage) {
|
||||
const nextEl = refsMap.get(nextPage.id);
|
||||
if (nextEl) {
|
||||
const nextRect = nextEl.getBoundingClientRect();
|
||||
const sameRow = Math.abs(nextRect.top - currentRect.top) < currentRect.height / 2;
|
||||
if (sameRow) {
|
||||
lineLeft = (currentRect.right + nextRect.left) / 2;
|
||||
} else {
|
||||
lineLeft = currentRect.right + nextRect.width * 0.1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lineLeft = currentRect.right + currentRect.width * 0.1;
|
||||
}
|
||||
|
||||
const leftPosition = gridOffset + col * itemWithGap + ITEM_WIDTH + (ITEM_GAP / 2);
|
||||
const topPosition = row * ITEM_HEIGHT + (ITEM_HEIGHT * 0.05) + ITEM_GAP; // Center vertically (5% offset since page is 90% height) + gap offset
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`split-${position}`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: leftPosition,
|
||||
top: topPosition,
|
||||
width: '1px',
|
||||
height: `calc(${GRID_CONSTANTS.ITEM_HEIGHT} * 0.9)`, // Match page container height (90%)
|
||||
borderLeft: '1px dashed #3b82f6'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={`split-${position}`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${lineLeft - containerRect.left}px`,
|
||||
top: `${currentRect.top - containerRect.top}px`,
|
||||
width: '1px',
|
||||
height: `${currentRect.height}px`,
|
||||
borderLeft: '1px dashed #3b82f6',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
@ -1151,6 +1164,7 @@ const PageEditor = ({
|
||||
};
|
||||
}}
|
||||
renderItem={(page, index, refs, boxSelectedIds, clearBoxSelection, _getBoxSelection, _activeId, activeDragIds, justMoved, _isOver, dragHandleProps, zoomLevel) => {
|
||||
gridItemRefsRef.current = refs;
|
||||
const fileColorIndex = page.originalFileId ? fileColorIndexMap.get(page.originalFileId) ?? 0 : 0;
|
||||
const isBoxSelected = boxSelectedIds.includes(page.id);
|
||||
return (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user