feat: listen to file dnd event on window instead of just popover (#921)

This commit is contained in:
Aman Harwara
2022-03-12 19:30:51 +05:30
committed by GitHub
parent fc2a350cca
commit 5b42eedd97
3 changed files with 138 additions and 161 deletions

View File

@@ -15,12 +15,12 @@ import { useCloseOnClickOutside } from '../utils';
import { ChallengeReason, ContentType, SNFile } from '@standardnotes/snjs';
import { confirmDialog } from '@/services/alertService';
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit';
import { parseFileName } from '@standardnotes/filepicker';
import { parseFileName, StreamingFileReader } from '@standardnotes/filepicker';
import {
PopoverFileItemAction,
PopoverFileItemActionType,
} from './PopoverFileItemAction';
import { PopoverDragNDropWrapper } from './PopoverDragNDropWrapper';
import { AttachedFilesPopover, PopoverTabs } from './AttachedFilesPopover';
type Props = {
application: WebApplication;
@@ -68,7 +68,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
};
}, [application, reloadAttachedFilesCount]);
const toggleAttachedFilesMenu = async () => {
const toggleAttachedFilesMenu = useCallback(async () => {
const rect = buttonRef.current?.getBoundingClientRect();
if (rect) {
const { clientHeight } = document.documentElement;
@@ -98,7 +98,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
setOpen(newOpenState);
}
};
}, [onClickPreprocessing, open]);
const deleteFile = async (file: SNFile) => {
const shouldDelete = await confirmDialog({
@@ -123,9 +123,12 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
appState.files.downloadFile(file);
};
const attachFileToNote = async (file: SNFile) => {
await application.items.associateFileWithNote(file, note);
};
const attachFileToNote = useCallback(
async (file: SNFile) => {
await application.items.associateFileWithNote(file, note);
},
[application.items, note]
);
const detachFileFromNote = async (file: SNFile) => {
await application.items.disassociateFileWithNote(file, note);
@@ -210,6 +213,98 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
return true;
};
const [isDraggingFiles, setIsDraggingFiles] = useState(false);
const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles);
const dragCounter = useRef(0);
const handleDrag = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
};
const handleDragIn = useCallback(
(event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
dragCounter.current = dragCounter.current + 1;
if (event.dataTransfer?.items.length) {
setIsDraggingFiles(true);
if (!open) {
toggleAttachedFilesMenu();
}
}
},
[open, toggleAttachedFilesMenu]
);
const handleDragOut = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
dragCounter.current = dragCounter.current - 1;
if (dragCounter.current > 0) {
return;
}
setIsDraggingFiles(false);
};
const handleDrop = useCallback(
(event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
setIsDraggingFiles(false);
if (event.dataTransfer?.items.length) {
Array.from(event.dataTransfer.items).forEach(async (item) => {
const fileOrHandle = StreamingFileReader.available()
? ((await item.getAsFileSystemHandle()) as FileSystemFileHandle)
: item.getAsFile();
if (!fileOrHandle) {
return;
}
const uploadedFiles = await appState.files.uploadNewFile(
fileOrHandle
);
if (!uploadedFiles) {
return;
}
if (currentTab === PopoverTabs.AttachedFiles) {
uploadedFiles.forEach((file) => {
attachFileToNote(file);
});
}
});
event.dataTransfer.clearData();
dragCounter.current = 0;
}
},
[appState.files, attachFileToNote, currentTab]
);
useEffect(() => {
window.addEventListener('dragenter', handleDragIn);
window.addEventListener('dragleave', handleDragOut);
window.addEventListener('dragover', handleDrag);
window.addEventListener('drop', handleDrop);
return () => {
window.removeEventListener('dragenter', handleDragIn);
window.removeEventListener('dragleave', handleDragOut);
window.removeEventListener('dragover', handleDrag);
window.removeEventListener('drop', handleDrop);
};
}, [handleDragIn, handleDrop]);
return (
<div ref={containerRef}>
<Disclosure open={open} onChange={toggleAttachedFilesMenu}>
@@ -245,11 +340,14 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed"
>
{open && (
<PopoverDragNDropWrapper
<AttachedFilesPopover
application={application}
appState={appState}
note={note}
fileActionHandler={handleFileAction}
handleFileAction={handleFileAction}
currentTab={currentTab}
setCurrentTab={setCurrentTab}
isDraggingFiles={isDraggingFiles}
/>
)}
</DisclosurePanel>

View File

@@ -1,16 +1,30 @@
import { ContentType, SNFile } from '@standardnotes/snjs';
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { ContentType, SNFile, SNNote } from '@standardnotes/snjs';
import { FilesIllustration } from '@standardnotes/stylekit';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { StateUpdater, useCallback, useEffect, useState } from 'preact/hooks';
import { Button } from '../Button';
import { Icon } from '../Icon';
import { PopoverTabs, PopoverWrapperProps } from './PopoverDragNDropWrapper';
import { PopoverFileItem } from './PopoverFileItem';
import { PopoverFileItemActionType } from './PopoverFileItemAction';
import {
PopoverFileItemAction,
PopoverFileItemActionType,
} from './PopoverFileItemAction';
type Props = PopoverWrapperProps & {
export enum PopoverTabs {
AttachedFiles,
AllFiles,
}
type Props = {
application: WebApplication;
appState: AppState;
currentTab: PopoverTabs;
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>;
isDraggingFiles: boolean;
note: SNNote;
setCurrentTab: StateUpdater<PopoverTabs>;
};
@@ -18,9 +32,10 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
({
application,
appState,
note,
fileActionHandler,
currentTab,
handleFileAction,
isDraggingFiles,
note,
setCurrentTab,
}) => {
const [attachedFiles, setAttachedFiles] = useState<SNFile[]>([]);
@@ -74,7 +89,7 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
}
if (currentTab === PopoverTabs.AttachedFiles) {
uploadedFiles.forEach((file) => {
fileActionHandler({
handleFileAction({
type: PopoverFileItemActionType.AttachFileToNote,
payload: file,
});
@@ -83,7 +98,14 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
};
return (
<div className="flex flex-col">
<div
className="flex flex-col"
style={{
border: isDraggingFiles
? '2px dashed var(--sn-stylekit-info-color)'
: '',
}}
>
<div className="flex border-0 border-b-1 border-solid border-main">
<button
className={`bg-default border-0 cursor-pointer px-3 py-2.5 relative focus:bg-info-backdrop focus:shadow-bottom ${
@@ -146,7 +168,7 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
key={file.uuid}
file={file}
isAttachedToNote={attachedFiles.includes(file)}
handleFileAction={fileActionHandler}
handleFileAction={handleFileAction}
/>
);
})

View File

@@ -1,143 +0,0 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/constants';
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { StreamingFileReader } from '@standardnotes/filepicker';
import { SNNote } from '@standardnotes/snjs';
import { FunctionComponent } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { AttachedFilesPopover } from './AttachedFilesPopover';
import {
PopoverFileItemAction,
PopoverFileItemActionType,
} from './PopoverFileItemAction';
export enum PopoverTabs {
AttachedFiles,
AllFiles,
}
export type PopoverWrapperProps = {
application: WebApplication;
appState: AppState;
note: SNNote;
fileActionHandler: (action: PopoverFileItemAction) => Promise<boolean>;
};
export const PopoverDragNDropWrapper: FunctionComponent<
PopoverWrapperProps
> = ({ fileActionHandler, appState, application, note }) => {
const dropzoneRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles);
const dragCounter = useRef(0);
const handleDrag = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
};
const handleDragIn = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
dragCounter.current = dragCounter.current + 1;
if (event.dataTransfer?.items.length) {
setIsDragging(true);
}
};
const handleDragOut = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
dragCounter.current = dragCounter.current - 1;
if (dragCounter.current > 0) {
return;
}
setIsDragging(false);
};
const handleDrop = useCallback(
(event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
setIsDragging(false);
if (event.dataTransfer?.items.length) {
Array.from(event.dataTransfer.items).forEach(async (item) => {
let fileOrHandle;
if (StreamingFileReader.available()) {
fileOrHandle =
(await item.getAsFileSystemHandle()) as FileSystemFileHandle;
} else {
fileOrHandle = item.getAsFile();
}
if (fileOrHandle) {
const uploadedFiles = await appState.files.uploadNewFile(
fileOrHandle
);
if (!uploadedFiles) {
return;
}
if (currentTab === PopoverTabs.AttachedFiles) {
uploadedFiles.forEach((file) => {
fileActionHandler({
type: PopoverFileItemActionType.AttachFileToNote,
payload: file,
});
});
}
}
});
event.dataTransfer.clearData();
dragCounter.current = 0;
}
},
[appState.files, currentTab, fileActionHandler]
);
useEffect(() => {
const dropzoneElement = dropzoneRef.current;
if (dropzoneElement) {
dropzoneElement.addEventListener('dragenter', handleDragIn);
dropzoneElement.addEventListener('dragleave', handleDragOut);
dropzoneElement.addEventListener('dragover', handleDrag);
dropzoneElement.addEventListener('drop', handleDrop);
}
return () => {
dropzoneElement?.removeEventListener('dragenter', handleDragIn);
dropzoneElement?.removeEventListener('dragleave', handleDragOut);
dropzoneElement?.removeEventListener('dragover', handleDrag);
dropzoneElement?.removeEventListener('drop', handleDrop);
};
}, [handleDrop]);
return (
<div
ref={dropzoneRef}
className="focus:shadow-none"
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
style={{
border: isDragging ? '2px dashed var(--sn-stylekit-info-color)' : '',
}}
>
<AttachedFilesPopover
application={application}
appState={appState}
note={note}
fileActionHandler={fileActionHandler}
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
</div>
);
};