More drag and drop tweaks

This commit is contained in:
2025-02-26 20:53:28 -05:00
parent 2d62cac5f7
commit c185d4e3ca

View File

@@ -88,9 +88,10 @@ export const ImageUploadStep = <T extends string>({
// Set up sensors for drag and drop with enhanced configuration // Set up sensors for drag and drop with enhanced configuration
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor, { useSensor(PointerSensor, {
// Make it responsive with minimal constraints // Make it responsive with less restrictive constraints
activationConstraint: { activationConstraint: {
distance: 3, // Reduced distance for more responsive drag distance: 1, // Reduced distance for more responsive drag
delay: 0, // No delay
tolerance: 5 tolerance: 5
}, },
}), }),
@@ -104,73 +105,15 @@ export const ImageUploadStep = <T extends string>({
// Custom collision detection algorithm that prioritizes product containers // Custom collision detection algorithm that prioritizes product containers
const customCollisionDetection: CollisionDetection = (args) => { const customCollisionDetection: CollisionDetection = (args) => {
const { droppableContainers, active, pointerCoordinates } = args; // Use the built-in pointerWithin algorithm first for better performance
if (!pointerCoordinates) {
return [];
}
// Get the active container (product index)
const activeContainer = findContainer(active.id.toString());
// Get all collisions
const pointerCollisions = pointerWithin(args); const pointerCollisions = pointerWithin(args);
const rectCollisions = rectIntersection(args);
// Combine collision methods for more reliable detection if (pointerCollisions.length > 0) {
const allCollisions = [...pointerCollisions, ...rectCollisions]; return pointerCollisions;
// Check for image collisions first - for reordering within the same container
const imageCollisions = allCollisions.filter(collision => {
const collisionId = collision.id.toString();
// Only detect other images, not the active image
if (collisionId === active.id.toString()) return false;
// Check if it's an image by looking for it in productImages
return productImages.some(img => img.id === collisionId);
});
// If we have image collisions within the same container, prioritize those for reordering
if (activeContainer && imageCollisions.length > 0) {
const sameContainerCollisions = imageCollisions.filter(collision => {
const image = productImages.find(img => img.id === collision.id);
return image && image.productIndex.toString() === activeContainer;
});
if (sameContainerCollisions.length > 0) {
return [sameContainerCollisions[0]]; // Return the first image collision in the same container
}
} }
// If no image collisions in the same container, check for product container collisions // Fall back to rectIntersection if no pointer collisions
const productContainerCollisions = allCollisions.filter( return rectIntersection(args);
collision => typeof collision.id === 'string' && collision.id.toString().startsWith('product-')
);
// If the active container is different from container collisions, prioritize those
if (activeContainer && productContainerCollisions.length > 0) {
const differentContainerCollisions = productContainerCollisions.filter(collision => {
const containerIndex = collision.id.toString().split('-')[1];
return containerIndex !== activeContainer;
});
if (differentContainerCollisions.length > 0) {
return [differentContainerCollisions[0]];
}
}
// If we have any product container collisions, use those
if (productContainerCollisions.length > 0) {
return [productContainerCollisions[0]];
}
// Finally, check images in different containers
if (imageCollisions.length > 0) {
return [imageCollisions[0]];
}
// Fall back to all collisions
return allCollisions.length > 0 ? [allCollisions[0]] : [];
}; };
// Handle drag start to set active image and prevent default behavior // Handle drag start to set active image and prevent default behavior
@@ -255,22 +198,13 @@ export const ImageUploadStep = <T extends string>({
// Check if the container has images // Check if the container has images
const hasImages = getProductImages(index).length > 0; const hasImages = getProductImages(index).length > 0;
// Add stronger attributes for empty containers to ensure they stand out as drop targets // Set data-empty attribute for tracking purposes
if (!hasImages) { container.setAttribute('data-empty', hasImages ? 'false' : 'true');
container.setAttribute('data-empty', 'true');
// Setting tabindex makes the element focusable which can help with accessibility
container.setAttribute('tabindex', '0');
// Add ARIA attributes to improve accessibility and recognition // Ensure the container has sufficient size to be a drop target
container.setAttribute('aria-label', `Empty drop zone for product ${index + 1}`);
// Ensure the empty container has sufficient size to be a drop target
if (container.offsetHeight < 100) { if (container.offsetHeight < 100) {
container.style.minHeight = '100px'; container.style.minHeight = '100px';
} }
} else {
container.setAttribute('data-empty', 'false');
}
} }
}); });
}, [data, productImages]); // Add productImages as a dependency to re-run when images change }, [data, productImages]); // Add productImages as a dependency to re-run when images change
@@ -285,32 +219,23 @@ export const ImageUploadStep = <T extends string>({
// Define handlers for native browser drag events // Define handlers for native browser drag events
const handleNativeDragOver = (e: DragEvent) => { const handleNativeDragOver = (e: DragEvent) => {
e.preventDefault(); e.preventDefault();
if (getProductImages(index).length === 0) { // Highlight all containers during dragover
// Highlight empty containers during dragover
container.classList.add('border-primary', 'bg-primary/5'); container.classList.add('border-primary', 'bg-primary/5');
setActiveDroppableId(`product-${index}`); setActiveDroppableId(`product-${index}`);
}
}; };
const handleNativeDragLeave = (e: DragEvent) => { const handleNativeDragLeave = (e: DragEvent) => {
if (getProductImages(index).length === 0) {
// Remove highlight when drag leaves // Remove highlight when drag leaves
container.classList.remove('border-primary', 'bg-primary/5'); container.classList.remove('border-primary', 'bg-primary/5');
if (activeDroppableId === `product-${index}`) { if (activeDroppableId === `product-${index}`) {
setActiveDroppableId(null); setActiveDroppableId(null);
} }
}
}; };
// Add these handlers // Add these handlers
container.addEventListener('dragover', handleNativeDragOver); container.addEventListener('dragover', handleNativeDragOver);
container.addEventListener('dragleave', handleNativeDragLeave); container.addEventListener('dragleave', handleNativeDragLeave);
// Log this for debugging
console.log(`Added native drag handlers to product-${index}`, {
isEmpty: getProductImages(index).length === 0
});
// Return cleanup function // Return cleanup function
return () => { return () => {
container.removeEventListener('dragover', handleNativeDragOver); container.removeEventListener('dragover', handleNativeDragOver);
@@ -318,7 +243,7 @@ export const ImageUploadStep = <T extends string>({
}; };
} }
}); });
}, [data, productImages]); // Re-run when data or productImages change }, [data, productImages, activeDroppableId]); // Re-run when data or productImages change
// Function to remove an image URL from a product // Function to remove an image URL from a product
const removeImageFromProduct = (productIndex: number, imageUrl: string) => { const removeImageFromProduct = (productIndex: number, imageUrl: string) => {
@@ -1166,17 +1091,19 @@ export const ImageUploadStep = <T extends string>({
}); });
// Create a new style object with fixed dimensions to prevent distortion // Create a new style object with fixed dimensions to prevent distortion
const style = { const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
opacity: isDragging ? 0.5 : 1, opacity: isDragging ? 0.5 : 1,
zIndex: isDragging ? 10 : 0, // Higher z-index when dragging zIndex: isDragging ? 999 : 1, // Higher z-index when dragging
touchAction: 'none' as 'none', // Prevent touch scrolling during drag touchAction: 'none', // Prevent touch scrolling during drag
userSelect: 'none', // Prevent text selection during drag
cursor: isDragging ? 'grabbing' : 'grab',
width: '96px', width: '96px',
height: '96px', height: '96px',
flexShrink: 0, flexShrink: 0,
flexGrow: 0, flexGrow: 0,
position: 'relative' as 'relative', position: 'relative',
}; };
// Create a ref for the buttons to exclude them from drag listeners // Create a ref for the buttons to exclude them from drag listeners
@@ -1190,6 +1117,11 @@ export const ImageUploadStep = <T extends string>({
className="relative border rounded-md overflow-hidden flex items-center justify-center shrink-0 cursor-grab active:cursor-grabbing select-none no-native-drag" className="relative border rounded-md overflow-hidden flex items-center justify-center shrink-0 cursor-grab active:cursor-grabbing select-none no-native-drag"
{...attributes} {...attributes}
{...listeners} {...listeners}
onDragStart={(e) => {
// This ensures the native drag doesn't interfere
e.preventDefault();
e.stopPropagation();
}}
> >
{image.loading ? ( {image.loading ? (
<div className="flex flex-col items-center justify-center p-2"> <div className="flex flex-col items-center justify-center p-2">
@@ -1200,7 +1132,7 @@ export const ImageUploadStep = <T extends string>({
<> <>
<img <img
src={getFullImageUrl(image.imageUrl)} src={getFullImageUrl(image.imageUrl)}
alt={`Product ${productIndex + 1} - Image ${imgIndex + 1}`} alt={`${data[productIndex].name || `Product #${productIndex + 1}`} - Image ${imgIndex + 1}`}
className="h-full w-full object-cover select-none no-native-drag" className="h-full w-full object-cover select-none no-native-drag"
draggable={false} draggable={false}
/> />
@@ -1262,12 +1194,12 @@ export const ImageUploadStep = <T extends string>({
<div className="overflow-hidden rounded-md border border-border shadow-md bg-white dark:bg-black"> <div className="overflow-hidden rounded-md border border-border shadow-md bg-white dark:bg-black">
<img <img
src={getFullImageUrl(image.imageUrl)} src={getFullImageUrl(image.imageUrl)}
alt={`Product ${productIndex + 1} - Image ${imgIndex + 1}`} alt={`${data[productIndex].name || `Product #${productIndex + 1}`} - Image ${imgIndex + 1}`}
className="max-h-[70vh] max-w-full object-contain" className="max-h-[70vh] max-w-full object-contain"
/> />
</div> </div>
<div className="mt-2 text-sm text-muted-foreground text-center"> <div className="mt-2 text-sm text-muted-foreground text-center">
{`Product ${productIndex + 1} - Image ${imgIndex + 1}`} {`${data[productIndex].name || `Product #${productIndex + 1}`} - Image ${imgIndex + 1}`}
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
@@ -1282,24 +1214,19 @@ export const ImageUploadStep = <T extends string>({
const getProductContainerClasses = (index: number) => { const getProductContainerClasses = (index: number) => {
const isValidDropTarget = activeId && findContainer(activeId) !== index.toString(); const isValidDropTarget = activeId && findContainer(activeId) !== index.toString();
const isActiveDropTarget = activeDroppableId === `product-${index}`; const isActiveDropTarget = activeDroppableId === `product-${index}`;
const hasImages = getProductImages(index).length > 0;
return cn( return cn(
"flex-1 min-h-[6rem] rounded-md p-2 transition-all", "flex-1 min-h-[6rem] rounded-md p-2 transition-all",
// Always add a border for empty containers to make them visible as drop targets // Only show borders during active drag operations
!hasImages && "border-2 border-dashed border-secondary-foreground/30",
// Active drop target styling
isValidDropTarget && isActiveDropTarget isValidDropTarget && isActiveDropTarget
? "border-2 border-dashed border-primary bg-primary/10" ? "border-2 border-dashed border-primary bg-primary/10"
: isValidDropTarget && !hasImages
? "border-2 border-dashed border-muted-foreground/40 bg-muted/20"
: isValidDropTarget : isValidDropTarget
? "border border-dashed border-muted-foreground/30" ? "border border-dashed border-muted-foreground/30"
: "" : ""
); );
}; };
// Add a DroppableContainer component for empty product containers // Add a DroppableContainer component for product containers
const DroppableContainer = ({ id, children, isEmpty }: { id: string; children: React.ReactNode; isEmpty: boolean }) => { const DroppableContainer = ({ id, children, isEmpty }: { id: string; children: React.ReactNode; isEmpty: boolean }) => {
const { setNodeRef } = useDroppable({ const { setNodeRef } = useDroppable({
id, id,
@@ -1315,7 +1242,8 @@ export const ImageUploadStep = <T extends string>({
id={id} id={id}
data-droppable="true" data-droppable="true"
data-empty={isEmpty ? "true" : "false"} data-empty={isEmpty ? "true" : "false"}
className={`w-full h-full ${!isEmpty ? "flex flex-row flex-wrap gap-2" : ""}`} className="w-full h-full flex flex-row flex-wrap gap-2"
style={{ minHeight: '100px' }} // Ensure minimum height for empty containers
> >
{children} {children}
</div> </div>
@@ -1398,15 +1326,13 @@ export const ImageUploadStep = <T extends string>({
style={{ style={{
pointerEvents: 'auto', pointerEvents: 'auto',
touchAction: 'none', touchAction: 'none',
minHeight: getProductImages(index).length === 0 ? '100px' : 'auto' minHeight: '100px'
}} }}
onDragOver={(e) => { onDragOver={(e) => {
// This is a native event handler to ensure the browser recognizes the drop zone // This is a native event handler to ensure the browser recognizes the drop zone
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (getProductImages(index).length === 0) {
setActiveDroppableId(`product-${index}`); setActiveDroppableId(`product-${index}`);
}
}} }}
> >
<DroppableContainer <DroppableContainer
@@ -1428,9 +1354,7 @@ export const ImageUploadStep = <T extends string>({
))} ))}
</SortableContext> </SortableContext>
) : ( ) : (
<div className="w-full h-full flex items-center justify-center text-muted-foreground text-sm py-8"> <div className="w-full h-full" data-empty-placeholder="true"></div>
Drop images here
</div>
)} )}
</DroppableContainer> </DroppableContainer>
</div> </div>