feat: listen to file dnd event on window instead of just popover (#921)
This commit is contained in:
@@ -15,12 +15,12 @@ import { useCloseOnClickOutside } from '../utils';
|
|||||||
import { ChallengeReason, ContentType, SNFile } from '@standardnotes/snjs';
|
import { ChallengeReason, ContentType, SNFile } from '@standardnotes/snjs';
|
||||||
import { confirmDialog } from '@/services/alertService';
|
import { confirmDialog } from '@/services/alertService';
|
||||||
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit';
|
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit';
|
||||||
import { parseFileName } from '@standardnotes/filepicker';
|
import { parseFileName, StreamingFileReader } from '@standardnotes/filepicker';
|
||||||
import {
|
import {
|
||||||
PopoverFileItemAction,
|
PopoverFileItemAction,
|
||||||
PopoverFileItemActionType,
|
PopoverFileItemActionType,
|
||||||
} from './PopoverFileItemAction';
|
} from './PopoverFileItemAction';
|
||||||
import { PopoverDragNDropWrapper } from './PopoverDragNDropWrapper';
|
import { AttachedFilesPopover, PopoverTabs } from './AttachedFilesPopover';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
@@ -68,7 +68,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
};
|
};
|
||||||
}, [application, reloadAttachedFilesCount]);
|
}, [application, reloadAttachedFilesCount]);
|
||||||
|
|
||||||
const toggleAttachedFilesMenu = async () => {
|
const toggleAttachedFilesMenu = useCallback(async () => {
|
||||||
const rect = buttonRef.current?.getBoundingClientRect();
|
const rect = buttonRef.current?.getBoundingClientRect();
|
||||||
if (rect) {
|
if (rect) {
|
||||||
const { clientHeight } = document.documentElement;
|
const { clientHeight } = document.documentElement;
|
||||||
@@ -98,7 +98,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
|
|
||||||
setOpen(newOpenState);
|
setOpen(newOpenState);
|
||||||
}
|
}
|
||||||
};
|
}, [onClickPreprocessing, open]);
|
||||||
|
|
||||||
const deleteFile = async (file: SNFile) => {
|
const deleteFile = async (file: SNFile) => {
|
||||||
const shouldDelete = await confirmDialog({
|
const shouldDelete = await confirmDialog({
|
||||||
@@ -123,9 +123,12 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
appState.files.downloadFile(file);
|
appState.files.downloadFile(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachFileToNote = async (file: SNFile) => {
|
const attachFileToNote = useCallback(
|
||||||
await application.items.associateFileWithNote(file, note);
|
async (file: SNFile) => {
|
||||||
};
|
await application.items.associateFileWithNote(file, note);
|
||||||
|
},
|
||||||
|
[application.items, note]
|
||||||
|
);
|
||||||
|
|
||||||
const detachFileFromNote = async (file: SNFile) => {
|
const detachFileFromNote = async (file: SNFile) => {
|
||||||
await application.items.disassociateFileWithNote(file, note);
|
await application.items.disassociateFileWithNote(file, note);
|
||||||
@@ -210,6 +213,98 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
return true;
|
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 (
|
return (
|
||||||
<div ref={containerRef}>
|
<div ref={containerRef}>
|
||||||
<Disclosure open={open} onChange={toggleAttachedFilesMenu}>
|
<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"
|
className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed"
|
||||||
>
|
>
|
||||||
{open && (
|
{open && (
|
||||||
<PopoverDragNDropWrapper
|
<AttachedFilesPopover
|
||||||
application={application}
|
application={application}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
note={note}
|
note={note}
|
||||||
fileActionHandler={handleFileAction}
|
handleFileAction={handleFileAction}
|
||||||
|
currentTab={currentTab}
|
||||||
|
setCurrentTab={setCurrentTab}
|
||||||
|
isDraggingFiles={isDraggingFiles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DisclosurePanel>
|
</DisclosurePanel>
|
||||||
|
|||||||
@@ -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 { FilesIllustration } from '@standardnotes/stylekit';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { StateUpdater, useCallback, useEffect, useState } from 'preact/hooks';
|
import { StateUpdater, useCallback, useEffect, useState } from 'preact/hooks';
|
||||||
import { Button } from '../Button';
|
import { Button } from '../Button';
|
||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
import { PopoverTabs, PopoverWrapperProps } from './PopoverDragNDropWrapper';
|
|
||||||
import { PopoverFileItem } from './PopoverFileItem';
|
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;
|
currentTab: PopoverTabs;
|
||||||
|
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>;
|
||||||
|
isDraggingFiles: boolean;
|
||||||
|
note: SNNote;
|
||||||
setCurrentTab: StateUpdater<PopoverTabs>;
|
setCurrentTab: StateUpdater<PopoverTabs>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -18,9 +32,10 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
({
|
({
|
||||||
application,
|
application,
|
||||||
appState,
|
appState,
|
||||||
note,
|
|
||||||
fileActionHandler,
|
|
||||||
currentTab,
|
currentTab,
|
||||||
|
handleFileAction,
|
||||||
|
isDraggingFiles,
|
||||||
|
note,
|
||||||
setCurrentTab,
|
setCurrentTab,
|
||||||
}) => {
|
}) => {
|
||||||
const [attachedFiles, setAttachedFiles] = useState<SNFile[]>([]);
|
const [attachedFiles, setAttachedFiles] = useState<SNFile[]>([]);
|
||||||
@@ -74,7 +89,7 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
}
|
}
|
||||||
if (currentTab === PopoverTabs.AttachedFiles) {
|
if (currentTab === PopoverTabs.AttachedFiles) {
|
||||||
uploadedFiles.forEach((file) => {
|
uploadedFiles.forEach((file) => {
|
||||||
fileActionHandler({
|
handleFileAction({
|
||||||
type: PopoverFileItemActionType.AttachFileToNote,
|
type: PopoverFileItemActionType.AttachFileToNote,
|
||||||
payload: file,
|
payload: file,
|
||||||
});
|
});
|
||||||
@@ -83,7 +98,14 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex border-0 border-b-1 border-solid border-main">
|
||||||
<button
|
<button
|
||||||
className={`bg-default border-0 cursor-pointer px-3 py-2.5 relative focus:bg-info-backdrop focus:shadow-bottom ${
|
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}
|
key={file.uuid}
|
||||||
file={file}
|
file={file}
|
||||||
isAttachedToNote={attachedFiles.includes(file)}
|
isAttachedToNote={attachedFiles.includes(file)}
|
||||||
handleFileAction={fileActionHandler}
|
handleFileAction={handleFileAction}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user