1 import type { DragEvent } from 'react';
2 import { useCallback, useEffect, useRef, useState } from 'react';
4 import useHandler from '@proton/components/hooks/useHandler';
5 import generateUID from '@proton/utils/generateUID';
7 import { DRAG_ITEM_ID_KEY, DRAG_ITEM_KEY } from './constants';
11 type AbstractItem = { ID?: string };
14 * Implement the draggable logic for an item
15 * Linked to the selection logic to drag the currently selected elements
16 * or to restore the selection after the drag
17 * Also take care of rendering the drag element and including the needed data in the transfer
18 * Items can be any object containing an ID
19 * @param items List of all items in the list
20 * @param checkedIDs List of the currently checked IDs
21 * @param onCheck Check handler to update selection
22 * @param getDragHtml Callback to return HTML content of the drag element
23 * @param selectAll Use select all feature so that we know how many items to drag
24 * @returns Currently dragged ids and drag handler to pass to items
26 const useItemsDraggable = <Item extends AbstractItem>(
29 onCheck: (IDs: string[], checked: boolean, replace: boolean) => void,
30 getDragHtml: (draggedIDs: string[]) => string,
33 // HTML reference to the drag element
34 const dragElementRef = useRef<HTMLDivElement>();
36 // List of currently dragged item ids
37 const [draggedIDs, setDraggedIDs] = useState<string[]>([]);
39 // Saved selection when dragging an item not selected
40 const [savedCheck, setSavedCheck] = useState<string[]>();
46 const clearDragElement = () => {
47 if (dragElementRef.current) {
48 document.body.removeChild(dragElementRef.current);
49 dragElementRef.current = undefined;
53 const handleDragCanceled = useHandler(() => {
57 onCheck(savedCheck, true, true);
58 setSavedCheck(undefined);
63 * Drag end handler to use on the draggable element
65 const handleDragEnd = useCallback((event: DragEvent) => {
66 // Always clear the drag element no matter why the drag has ended
69 // We discover that Chrome initialize the dropEffect to 'copy' and only set it to 'none' just after
70 // We don't use 'copy' at all so both 'none' and 'copy' effects can be considered as canceled drags
71 if (event.dataTransfer.dropEffect === 'none' || event.dataTransfer.dropEffect === 'copy') {
72 return handleDragCanceled();
76 const handleDragSucceed = useHandler((action: string | undefined) => {
82 if (action === 'link') {
84 onCheck(savedCheck, true, true);
86 setSavedCheck(undefined);
91 * Drag start handler to use on the draggable element
93 const handleDragStart = useCallback(
94 (event: DragEvent, item: Item) => {
97 const ID = item.ID || '';
98 const dragInSelection = checkedIDs.includes(ID);
99 const selection = dragInSelection ? checkedIDs : [ID];
101 setDraggedIDs(selection);
102 setSavedCheck(checkedIDs);
104 if (!dragInSelection) {
105 onCheck([], true, true);
108 const dragElement = document.createElement('div');
109 dragElement.innerHTML = getDragHtml(selection);
110 dragElement.className = 'drag-element p-4 border rounded';
111 dragElement.style.insetInlineStart = '-9999px';
112 dragElement.id = generateUID(DRAG_ITEM_ID_KEY);
113 // Wiring the dragend event on the drag element because the one from drag start is not reliable
114 dragElement.addEventListener('dragend', (event) => handleDragSucceed(event.dataTransfer?.dropEffect));
115 document.body.appendChild(dragElement);
116 event.dataTransfer.setDragImage(dragElement, 0, 0);
117 event.dataTransfer.setData(DRAG_ITEM_KEY, JSON.stringify(selection));
118 event.dataTransfer.setData(DRAG_ITEM_ID_KEY, dragElement.id);
120 dragElementRef.current = dragElement;
122 [checkedIDs, onCheck, selectAll]
125 return { draggedIDs, handleDragStart, handleDragEnd };
128 export default useItemsDraggable;