securityos/components/system/Files/FileManager/useDraggableEntries.ts

237 lines
7.3 KiB
TypeScript
Raw Permalink Normal View History

2024-09-06 15:32:35 +00:00
import type { FocusEntryFunctions } from "components/system/Files/FileManager/useFocusableEntries";
import { useSession } from "contexts/session";
import { join } from "path";
import { useCallback, useEffect, useRef, useState } from "react";
import type { Position } from "react-rnd";
import { MILLISECONDS_IN_SECOND, UNKNOWN_ICON } from "utils/constants";
import {
getHtmlToImage,
haltEvent,
updateIconPositions,
} from "utils/functions";
type DraggableEntryProps = {
draggable: boolean;
onDragEnd: React.DragEventHandler;
onDragStart: React.DragEventHandler;
style?: React.CSSProperties;
};
type DraggableEntry = (
url: string,
file: string,
renaming: boolean
) => DraggableEntryProps;
export type DragPosition = Partial<
Position & { offsetX: number; offsetY: number }
>;
const useDraggableEntries = (
focusedEntries: string[],
{ blurEntry, focusEntry }: FocusEntryFunctions,
fileManagerRef: React.MutableRefObject<HTMLOListElement | null>,
isSelecting: boolean,
allowMoving?: boolean
): DraggableEntry => {
const lastfileManagerChildRef = useRef<Element | null | undefined>(
fileManagerRef.current?.lastElementChild
);
const [dropIndex, setDropIndex] = useState(-1);
const { iconPositions, sortOrders, setIconPositions, setSortOrder } =
useSession();
const dragImageRef = useRef<HTMLImageElement | null>();
const dragPositionRef = useRef<DragPosition>(
Object.create(null) as DragPosition
);
const draggedOnceRef = useRef(false);
const onDragging = ({ clientX: x, clientY: y }: DragEvent): void => {
dragPositionRef.current = { ...dragPositionRef.current, x, y };
};
const isMainContainer =
fileManagerRef.current?.parentElement?.tagName === "MAIN";
const onDragEnd =
(entryUrl: string): React.DragEventHandler =>
(event) => {
haltEvent(event);
if (allowMoving && focusedEntries.length > 0) {
updateIconPositions(
entryUrl,
fileManagerRef.current,
iconPositions,
sortOrders,
dragPositionRef.current,
focusedEntries,
setIconPositions
);
fileManagerRef.current?.removeEventListener("dragover", onDragging);
} else if (dropIndex !== -1) {
setSortOrder(entryUrl, (currentSortOrders) => {
const sortedEntries = currentSortOrders.filter(
(entry) => !focusedEntries.includes(entry)
);
sortedEntries.splice(dropIndex, 0, ...focusedEntries);
return sortedEntries;
});
}
blurEntry();
};
const onDragOver =
(file: string): React.DragEventHandler =>
({ target }) => {
if (!allowMoving && target instanceof HTMLLIElement) {
const { children = [] } = target.parentElement || {};
const dragOverFocused = focusedEntries.includes(file);
setDropIndex(dragOverFocused ? -1 : [...children].indexOf(target));
}
};
const onDragStart =
(
entryUrl: string,
file: string,
renaming: boolean
): React.DragEventHandler =>
(event) => {
if (renaming) {
haltEvent(event);
return;
}
focusEntry(file);
event.nativeEvent.dataTransfer?.setData(
"application/json",
JSON.stringify(
focusedEntries.length <= 1
? [join(entryUrl, file)]
: focusedEntries.map((entryFile) => join(entryUrl, entryFile))
)
);
if (focusedEntries.length > 1 && dragImageRef.current) {
const iconPositionKeys = Object.keys(iconPositions);
if (
allowMoving &&
!draggedOnceRef.current &&
((lastfileManagerChildRef.current &&
fileManagerRef.current?.lastElementChild &&
fileManagerRef.current.lastElementChild !==
lastfileManagerChildRef.current) ||
focusedEntries.every((entryFile) =>
iconPositionKeys.includes(`${entryUrl}/${entryFile}`)
))
) {
draggedOnceRef.current = true;
}
const dragX = isMainContainer
? event.nativeEvent.clientX
: event.nativeEvent.offsetX;
const dragY = draggedOnceRef.current
? event.nativeEvent.clientY
: event.nativeEvent.offsetY;
event.nativeEvent.dataTransfer?.setDragImage(
dragImageRef.current,
dragX,
dragY
);
if (allowMoving && !draggedOnceRef.current) {
draggedOnceRef.current = true;
}
}
Object.assign(event.dataTransfer, { effectAllowed: "move" });
if (allowMoving) {
dragPositionRef.current =
focusedEntries.length > 1
? {
offsetX: event.nativeEvent.offsetX,
offsetY: event.nativeEvent.offsetY,
}
: (Object.create(null) as DragPosition);
fileManagerRef.current?.addEventListener("dragover", onDragging, {
passive: true,
});
}
};
const updateDragImage = useCallback(async () => {
if (fileManagerRef.current) {
const focusedElements = [
...fileManagerRef.current.querySelectorAll(".focus-within"),
];
if (focusedElements.length > 1) {
if (dragImageRef.current) dragImageRef.current.src = "";
else dragImageRef.current = new Image();
const htmlToImage = await getHtmlToImage();
let newDragImage: string | undefined;
try {
newDragImage = await htmlToImage?.toPng(fileManagerRef.current, {
filter: (element) => {
return (
!(element instanceof HTMLSourceElement) &&
focusedElements.some((focusedElement) =>
focusedElement.contains(element)
)
);
},
imagePlaceholder: UNKNOWN_ICON,
skipAutoScale: true,
});
} catch {
// Ignore failure to capture
}
if (newDragImage) {
dragImageRef.current.src = newDragImage;
}
}
}
}, [fileManagerRef]);
const debounceTimer = useRef<number>();
useEffect(() => {
if (
fileManagerRef.current?.lastElementChild &&
!lastfileManagerChildRef.current
) {
lastfileManagerChildRef.current = fileManagerRef.current.lastElementChild;
}
}, [fileManagerRef, focusedEntries]);
useEffect(() => {
if (debounceTimer.current) window.clearTimeout(debounceTimer.current);
debounceTimer.current = window.setTimeout(() => {
debounceTimer.current = undefined;
updateDragImage();
}, MILLISECONDS_IN_SECOND / 2);
}, [focusedEntries, updateDragImage]);
useEffect(() => {
if (!isSelecting && focusedEntries.length > 1) {
updateDragImage();
}
}, [focusedEntries.length, isSelecting, updateDragImage]);
return (entryUrl: string, file: string, renaming: boolean) => ({
draggable: true,
onDragEnd: onDragEnd(entryUrl),
onDragOver: onDragOver(file),
onDragStart: onDragStart(entryUrl, file, renaming),
style: isMainContainer ? iconPositions[join(entryUrl, file)] : undefined,
});
};
export default useDraggableEntries;