first commit
This commit is contained in:
20
src/component/FileManager/Dnd/DisableDropDelay.tsx
Executable file
20
src/component/FileManager/Dnd/DisableDropDelay.tsx
Executable file
@@ -0,0 +1,20 @@
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const DisableDropDelay = () => {
|
||||
const [_, bodyDropRef] = useDrop(() => ({
|
||||
accept: "file",
|
||||
drop: () => {
|
||||
// do something
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
bodyDropRef(document.body);
|
||||
return () => {
|
||||
bodyDropRef(null);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default DisableDropDelay;
|
||||
151
src/component/FileManager/Dnd/DndWrappedFile.tsx
Executable file
151
src/component/FileManager/Dnd/DndWrappedFile.tsx
Executable file
@@ -0,0 +1,151 @@
|
||||
import { memo, useCallback, useContext, useEffect } from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
import { getEmptyImage } from "react-dnd-html5-backend";
|
||||
import { FileResponse, FileType } from "../../../api/explorer.ts";
|
||||
import { setDragging } from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { processDnd } from "../../../redux/thunks/file.ts";
|
||||
import { getFileLinkedUri, mergeRefs } from "../../../util";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import CrUri, { Filesystem } from "../../../util/uri.ts";
|
||||
import { FileBlockProps } from "../Explorer/Explorer.tsx";
|
||||
import { FileManagerIndex } from "../FileManager.tsx";
|
||||
import { FmIndexContext } from "../FmIndexContext.tsx";
|
||||
|
||||
export interface DragItem {
|
||||
target: FileResponse;
|
||||
includeSelected?: boolean;
|
||||
}
|
||||
|
||||
export interface DropResult {
|
||||
dropEffect: string;
|
||||
uri?: string;
|
||||
}
|
||||
|
||||
export const DropEffect = {
|
||||
copy: "copy",
|
||||
move: "move",
|
||||
};
|
||||
|
||||
export interface UseFileDragProps {
|
||||
file?: FileResponse;
|
||||
includeSelected?: boolean;
|
||||
dropUri?: string;
|
||||
}
|
||||
|
||||
export const NoOpDropUri = "noop";
|
||||
export const useFileDrag = ({ file, includeSelected, dropUri }: UseFileDragProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const fmIndex = useContext(FmIndexContext);
|
||||
// const { addEventListenerForWindow, removeEventListenerForWindow } =
|
||||
// useDragScrolling(["#" + MainExplorerContainerID]);
|
||||
|
||||
// @ts-ignore
|
||||
const [{ isDragging }, drag, preview] = useDrag({
|
||||
type: "file",
|
||||
item: {
|
||||
target: file,
|
||||
includeSelected,
|
||||
},
|
||||
end: (item, monitor) => {
|
||||
// Ignore NoOpDropUri
|
||||
const target = monitor.getDropResult<DropResult>();
|
||||
if (!item || !target || !target.uri || target.uri == NoOpDropUri) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(processDnd(0, item as DragItem, target));
|
||||
},
|
||||
canDrag: () => {
|
||||
if (!file || fmIndex == FileManagerIndex.selector || isTablet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const crUri = new CrUri(file.path);
|
||||
return file.owned && crUri.fs() != Filesystem.share;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
const [{ canDrop, isOver }, drop] = useDrop({
|
||||
accept: "file",
|
||||
drop: () => (file ? { uri: getFileLinkedUri(file) } : { uri: dropUri }),
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
}),
|
||||
canDrop: (item, _monitor) => {
|
||||
const dropExist = !!dropUri || (!!file && file.type == FileType.folder);
|
||||
if (!dropExist || fmIndex == FileManagerIndex.selector) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const isActive = canDrop && isOver;
|
||||
|
||||
useEffect(() => {
|
||||
preview(getEmptyImage(), { captureDraggingState: true });
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
// addEventListenerForWindow();
|
||||
}
|
||||
dispatch(
|
||||
setDragging({
|
||||
dragging: isDragging,
|
||||
draggingWithSelected: !!includeSelected,
|
||||
}),
|
||||
);
|
||||
}, [isDragging]);
|
||||
|
||||
return [drag, drop, isActive, isDragging] as const;
|
||||
};
|
||||
|
||||
export interface DndWrappedFileProps extends FileBlockProps {
|
||||
component: React.MemoExoticComponent<(props: FileBlockProps) => JSX.Element>;
|
||||
}
|
||||
|
||||
const DndWrappedFile = memo((props: DndWrappedFileProps) => {
|
||||
const fmIndex = useContext(FmIndexContext);
|
||||
const globalDragging = useAppSelector((state) => state.globalState.dndState);
|
||||
const isSelected = useAppSelector((state) => state.fileManager[fmIndex].selected[props.file.path]);
|
||||
|
||||
const [drag, drop, isOver, isDragging] = useFileDrag({
|
||||
file: props.file.placeholder ? undefined : props.file,
|
||||
includeSelected: true,
|
||||
});
|
||||
|
||||
const mergedRef = useCallback(
|
||||
(val: any) => {
|
||||
mergeRefs(drop, drag)(val);
|
||||
},
|
||||
[drop, drag],
|
||||
);
|
||||
|
||||
const Component = props.component;
|
||||
|
||||
return (
|
||||
<Component
|
||||
dragRef={mergedRef}
|
||||
isDropOver={isOver}
|
||||
isDragging={isDragging || (!!globalDragging.dragging && !!isSelected && globalDragging.draggingWithSelected)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default DndWrappedFile;
|
||||
112
src/component/FileManager/Dnd/DragLayer.tsx
Executable file
112
src/component/FileManager/Dnd/DragLayer.tsx
Executable file
@@ -0,0 +1,112 @@
|
||||
import { useDragLayer, XYCoord } from "react-dnd";
|
||||
import { FileResponse } from "../../../api/explorer.ts";
|
||||
import { Badge, Box, Paper, PaperProps } from "@mui/material";
|
||||
import { useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { DragItem } from "./DndWrappedFile.tsx";
|
||||
import DisableDropDelay from "./DisableDropDelay.tsx";
|
||||
import { FileNameText, Header } from "../Explorer/GridView/GridFile.tsx";
|
||||
import FileSmallIcon from "../Explorer/FileSmallIcon.tsx";
|
||||
|
||||
interface DragPreviewProps extends PaperProps {
|
||||
files: FileResponse[];
|
||||
pointerOffset: XYCoord | null;
|
||||
}
|
||||
|
||||
const DragPreview = ({ pointerOffset, files, ...rest }: DragPreviewProps) => {
|
||||
const [size, setSize] = useState([0, 0]);
|
||||
useEffect(() => {
|
||||
setSize([220, 48]);
|
||||
}, []);
|
||||
if (!files || files.length == 0) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
<Badge
|
||||
badgeContent={files.length <= 1 ? undefined : files.length}
|
||||
color="primary"
|
||||
sx={{
|
||||
"& .MuiBadge-badge": { zIndex: 1612 },
|
||||
|
||||
transform: `translate(${pointerOffset?.x}px, ${pointerOffset?.y}px)`,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
width: size[0],
|
||||
height: size[1],
|
||||
zIndex: 1610,
|
||||
transition: (theme) => theme.transitions.create(["width", "height"]),
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<Header>
|
||||
<FileSmallIcon ignoreHovered selected={false} file={files[0]} />
|
||||
<FileNameText variant="body2">{files[0]?.name}</FileNameText>
|
||||
</Header>
|
||||
</Paper>
|
||||
{[...Array(Math.min(2, files.length - 1)).keys()].map((i) => (
|
||||
<Paper
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: size[0],
|
||||
height: size[1],
|
||||
zIndex: 1610 - i - 1,
|
||||
top: (i + 1) * 4,
|
||||
left: (i + 1) * 4,
|
||||
transition: (theme) => theme.transitions.create(["width", "height"]),
|
||||
}}
|
||||
elevation={3}
|
||||
/>
|
||||
))}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const DragLayer = () => {
|
||||
DisableDropDelay();
|
||||
|
||||
const { itemType, isDragging, item, pointerOffset } = useDragLayer((monitor) => ({
|
||||
item: monitor.getItem(),
|
||||
itemType: monitor.getItemType(),
|
||||
pointerOffset: monitor.getClientOffset(),
|
||||
isDragging: monitor.isDragging(),
|
||||
}));
|
||||
|
||||
const selected = useAppSelector((state) => state.fileManager[0].selected);
|
||||
const draggingFiles = useMemo(() => {
|
||||
if (item && (item as DragItem) && item.target) {
|
||||
const selectedList = item.includeSelected
|
||||
? Object.keys(selected)
|
||||
.map((key) => selected[key])
|
||||
.filter((x) => x.path != item.target.path)
|
||||
: [];
|
||||
return [item.target, ...selectedList];
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [selected, item]);
|
||||
|
||||
if (!isDragging) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
pointerEvents: "none",
|
||||
zIndex: 1600,
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<DragPreview files={draggingFiles} pointerOffset={pointerOffset} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DragLayer;
|
||||
72
src/component/FileManager/Dnd/useDndScrolling.ts
Executable file
72
src/component/FileManager/Dnd/useDndScrolling.ts
Executable file
@@ -0,0 +1,72 @@
|
||||
import { useRef } from "react";
|
||||
import { throttle } from "lodash";
|
||||
|
||||
const threshold = 0.1;
|
||||
|
||||
const useDragScrolling = (containers: string[]) => {
|
||||
const isScrolling = useRef(false);
|
||||
const targets = containers.map((id) => document.querySelector(id) as HTMLElement);
|
||||
const rects = useRef<DOMRect[]>([]);
|
||||
|
||||
const goDown = (target: HTMLElement) => {
|
||||
return () => {
|
||||
target.scrollTop += 5;
|
||||
|
||||
const { offsetHeight, scrollTop, scrollHeight } = target;
|
||||
const isScrollEnd = offsetHeight + scrollTop >= scrollHeight;
|
||||
|
||||
if (isScrolling.current && !isScrollEnd) {
|
||||
window.requestAnimationFrame(goDown(target));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const goUp = (target: HTMLElement) => {
|
||||
return () => {
|
||||
target.scrollTop -= 5;
|
||||
if (isScrolling.current && target.scrollTop > 0) {
|
||||
window.requestAnimationFrame(goUp(target));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const onDragOver = (event: MouseEvent) => {
|
||||
// detect if mouse is in any rect
|
||||
rects.current.forEach((rect, index) => {
|
||||
if (event.clientX < rect.left || event.clientX > rect.right) {
|
||||
isScrolling.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const height = rect.bottom - rect.top;
|
||||
if (event.clientY > rect.top && event.clientY < rect.top + threshold * height) {
|
||||
isScrolling.current = true;
|
||||
window.requestAnimationFrame(goUp(targets[index]));
|
||||
} else if (event.clientY < rect.bottom && event.clientY > rect.bottom - threshold * height) {
|
||||
isScrolling.current = true;
|
||||
window.requestAnimationFrame(goDown(targets[index]));
|
||||
} else {
|
||||
isScrolling.current = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const throttleOnDragOver = throttle(onDragOver, 300);
|
||||
|
||||
const addEventListenerForWindow = () => {
|
||||
rects.current = targets.map((t) => t.getBoundingClientRect());
|
||||
window.addEventListener("dragover", throttleOnDragOver, false);
|
||||
};
|
||||
|
||||
const removeEventListenerForWindow = () => {
|
||||
window.removeEventListener("dragover", throttleOnDragOver, false);
|
||||
isScrolling.current = false;
|
||||
};
|
||||
|
||||
return {
|
||||
addEventListenerForWindow,
|
||||
removeEventListenerForWindow,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDragScrolling;
|
||||
Reference in New Issue
Block a user