first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
import React, {RefObject, useLayoutEffect, useRef} from 'react';
import {draggables, dragSession, droppables} from './drag-state';
import {readFilesFromDataTransfer} from './read-files-from-data-transfer';
import {asyncIterableToArray} from '@common/utils/array/async-iterable-to-array';
import {InteractableRect} from '../interactable-event';
import {DraggableId, MixedDraggable} from './use-draggable';
import {UploadedFile} from '@common/uploads/uploaded-file';
export interface ConnectedDroppable {
id: DraggableId;
rect?: InteractableRect;
disabled?: boolean;
ref: RefObject<HTMLElement>;
}
// File dragged in from desktop
export interface NativeFileDraggable {
type: 'nativeFile';
el: null;
ref: null;
getData: () => Promise<UploadedFile[]>;
}
interface UseDroppableProps<T extends HTMLElement> {
id: DraggableId;
disabled?: boolean;
types: ('nativeFile' | string)[];
ref: RefObject<T>;
// this will fire dragEnter/dragLeave/dragOver events when same element is both draggable and drop target and dragging target over itself. Used for showing line previews before/after element during sort.
allowDragEventsFromItself?: boolean;
onDragEnter?: (target: MixedDraggable) => void;
onDragLeave?: (target: MixedDraggable) => void;
onDragOver?: (
target: MixedDraggable,
e: React.DragEvent<HTMLElement>
) => void;
// Handler that is called after draggable is held over droppable for a period of time.
// This typically opens the item so that the user can drop within it.
onDropActivate?: (e: MixedDraggable) => void;
onDrop?: (target: MixedDraggable) => void | Promise<void> | false;
acceptsDrop?: (target: MixedDraggable) => boolean;
}
interface DroppableState {
dragOverElements: Set<Element>;
dropActivateTimer: ReturnType<typeof setTimeout> | undefined;
}
const DROP_ACTIVATE_TIMEOUT = 400;
export function useDroppable<T extends HTMLElement>({
id,
disabled,
ref,
...options
}: UseDroppableProps<T>) {
const state = useRef<DroppableState>({
dragOverElements: new Set<Element>(),
dropActivateTimer: undefined,
}).current;
const optionsRef = useRef(options);
optionsRef.current = options;
useLayoutEffect(() => {
droppables.set(id, {
...droppables.get(id),
disabled,
id,
ref,
});
return () => {
droppables.delete(id);
};
}, [id, optionsRef, disabled, ref]);
// check if drop target accepts drag target
const canDrop = (draggable: MixedDraggable): boolean => {
const options = optionsRef.current;
const allowEventsOnSelf =
options.allowDragEventsFromItself ||
ref.current !== draggable.ref?.current;
return !!(
draggable?.type &&
allowEventsOnSelf &&
options.types.includes(draggable.type) &&
(!options.acceptsDrop || options.acceptsDrop(draggable))
);
};
const fireDragLeave = (e: React.DragEvent<HTMLElement>) => {
const draggable = getDraggable(e);
if (draggable) {
optionsRef.current.onDragLeave?.(draggable);
}
};
const onDragEnter = (e: React.DragEvent<HTMLElement>) => {
e.stopPropagation();
state.dragOverElements.add(e.target as Element);
if (state.dragOverElements.size > 1) {
return;
}
const draggable = getDraggable(e);
if (draggable && canDrop(draggable)) {
optionsRef.current.onDragEnter?.(draggable);
clearTimeout(state.dropActivateTimer);
if (typeof optionsRef.current.onDropActivate === 'function') {
state.dropActivateTimer = setTimeout(() => {
if (draggable) {
optionsRef.current.onDropActivate?.(draggable);
}
}, DROP_ACTIVATE_TIMEOUT);
}
}
};
const onDragLeave = (e: React.DragEvent<HTMLElement>) => {
e.stopPropagation();
// Track all the targets of dragenter events in a set, and remove them
// in dragleave. When the set becomes empty, we've left the drop target completely.
// We must also remove any elements that are no longer in the DOM, because dragleave
// events will never be fired for these. This can happen, for example, with drop
// indicators between items, which disappear when the drop target changes.
state.dragOverElements.delete(e.target as Element);
for (const element of state.dragOverElements) {
if (!e.currentTarget.contains(element)) {
state.dragOverElements.delete(element);
}
}
if (state.dragOverElements.size > 0) {
return;
}
const draggable = getDraggable(e);
if (draggable && canDrop(draggable)) {
fireDragLeave(e);
clearTimeout(state.dropActivateTimer);
}
};
const onDrop = async (e: React.DragEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
state.dragOverElements.clear();
fireDragLeave(e);
clearTimeout(state.dropActivateTimer);
const draggable = getDraggable(e);
if (draggable) {
optionsRef.current.onDragLeave?.(draggable);
// drop target does not accept this type of droppable
if (!canDrop(draggable)) {
if (dragSession.status !== 'inactive') {
dragSession.status = 'dropFail';
}
// drop target accepts this type, but it might still reject the drop in callback
} else {
// allow callback to mark drop as failed
const dropResult = optionsRef.current.onDrop?.(draggable);
// drag session will only be active for draggables within the app, never for files dragged in from desktop
if (dragSession.status !== 'inactive') {
dragSession.status =
dropResult === false ? 'dropFail' : 'dropSuccess';
}
}
}
};
const droppableProps = {
onDragOver: (e: React.DragEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
const draggable = getDraggable(e);
if (draggable && canDrop(draggable)) {
optionsRef.current.onDragOver?.(draggable, e);
}
},
onDragEnter,
onDragLeave,
onDrop,
};
return {
droppableProps: disabled ? {} : droppableProps,
};
}
function getDraggable(
e: React.DragEvent<HTMLElement>
): MixedDraggable | undefined {
if (dragSession.dragTargetId != null) {
return draggables.get(dragSession.dragTargetId);
} else if (e.dataTransfer.types.includes('Files')) {
return {
type: 'nativeFile',
el: null,
ref: null,
getData: () => {
return asyncIterableToArray(readFilesFromDataTransfer(e.dataTransfer));
},
};
}
}