s3: improves drag-and-drop behavior

Fixes #3992

...to the extent possible. Adds handlers for better releasing drag state
This commit is contained in:
Tyler Brown Cifu Shuster 2020-11-26 09:32:21 -08:00
parent fb6488bc68
commit 1eb6c47c83
5 changed files with 66 additions and 25 deletions

View File

@ -1,15 +1,39 @@
import { useState, useCallback, useMemo, useEffect } from "react"; import { useState, useCallback, useMemo, useEffect } from "react";
function validateDragEvent(e: DragEvent): FileList | null { function validateDragEvent(e: DragEvent): FileList | File[] | true | null {
const files = e.dataTransfer?.files; const files: File[] = [];
console.log(files); let valid = false;
if(!files?.length) { if (e.dataTransfer?.files) {
return null; Array.from(e.dataTransfer.files).forEach(f => files.push(f));
} }
return files || null; if (e.dataTransfer?.items) {
Array.from(e.dataTransfer.items || [])
.filter((i) => i.kind === 'file')
.forEach(f => {
valid = true; // Valid if file exists, but on DragOver, won't reveal its contents for security
const data = f.getAsFile();
if (data) {
files.push(data);
}
});
}
if (files.length) {
return [...new Set(files)];
}
if (navigator.userAgent.includes('Safari')) {
if (e.dataTransfer?.effectAllowed === 'all') {
valid = true;
} else if (e.dataTransfer?.files.length) {
return e.dataTransfer.files;
}
}
if (valid) {
return true;
}
return null;
} }
export function useFileDrag(dragged: (f: FileList) => void) { export function useFileDrag(dragged: (f: FileList | File[]) => void) {
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
const onDragEnter = useCallback( const onDragEnter = useCallback(
@ -25,11 +49,11 @@ export function useFileDrag(dragged: (f: FileList) => void) {
const onDrop = useCallback( const onDrop = useCallback(
(e: DragEvent) => { (e: DragEvent) => {
setDragging(false); setDragging(false);
e.preventDefault();
const files = validateDragEvent(e); const files = validateDragEvent(e);
if (!files) { if (!files || files === true) {
return; return;
} }
e.preventDefault();
dragged(files); dragged(files);
}, },
[setDragging, dragged] [setDragging, dragged]
@ -37,6 +61,9 @@ export function useFileDrag(dragged: (f: FileList) => void) {
const onDragOver = useCallback( const onDragOver = useCallback(
(e: DragEvent) => { (e: DragEvent) => {
if (!validateDragEvent(e)) {
return;
}
e.preventDefault(); e.preventDefault();
setDragging(true); setDragging(true);
}, },
@ -53,6 +80,18 @@ export function useFileDrag(dragged: (f: FileList) => void) {
[setDragging] [setDragging]
); );
useEffect(() => {
const mouseleave = (e) => {
if (!e.relatedTarget && !e.toElement) {
setDragging(false);
}
};
document.body.addEventListener('mouseout', mouseleave);
return () => {
document.body.removeEventListener('mouseout', mouseleave);
}
}, [setDragging]);
const bind = { const bind = {
onDragLeave, onDragLeave,
onDragOver, onDragOver,

View File

@ -68,7 +68,7 @@ export function ChatResource(props: ChatResourceProps) {
const chatInput = useRef<ChatInput>(); const chatInput = useRef<ChatInput>();
const onFileDrag = useCallback( const onFileDrag = useCallback(
(files: FileList) => { (files: FileList | File[]) => {
if (!chatInput.current) { if (!chatInput.current) {
return; return;
} }

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import ChatEditor from './chat-editor'; import ChatEditor from './chat-editor';
import { S3Upload } from '~/views/components/s3-upload' ; import { S3Upload } from '~/views/components/s3-upload' ;
import { uxToHex } from '~/logic/lib/util'; import { fileListFromFileArray, uxToHex } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil'; import { Sigil } from '~/logic/lib/sigil';
import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage'; import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
import GlobalApi from '~/logic/api/global'; import GlobalApi from '~/logic/api/global';
@ -150,16 +150,14 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
this.uploadFiles(event.clipboardData.files); this.uploadFiles(event.clipboardData.files);
} }
uploadFiles(files: FileList) { uploadFiles(files: FileList | File[]) {
if (!this.readyToUpload()) { if (!this.readyToUpload()) {
return; return;
} }
if (!this.s3Uploader.current || !this.s3Uploader.current.inputRef.current) if (!this.s3Uploader.current) {
return; return;
this.s3Uploader.current.inputRef.current.files = files; }
const fire = document.createEvent('HTMLEvents'); this.s3Uploader.current.uploadFiles(files);
fire.initEvent('change', true, true);
this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire);
} }
render() { render() {

View File

@ -51,6 +51,7 @@ export class S3Upload extends Component<S3UploadProps, S3UploadState> {
this.s3 = new S3Client(); this.s3 = new S3Client();
this.setCredentials(props.credentials, props.configuration); this.setCredentials(props.credentials, props.configuration);
this.inputRef = React.createRef(); this.inputRef = React.createRef();
this.uploadFiles = this.uploadFiles.bind(this);
} }
isReady(creds, config): boolean { isReady(creds, config): boolean {
@ -84,13 +85,9 @@ export class S3Upload extends Component<S3UploadProps, S3UploadState> {
); );
} }
onChange(): void { uploadFiles(files: FileList | File[]) {
const { props } = this; const { props } = this;
if (!this.inputRef.current) { return; } let file = ('item' in files) ? files.item(0) : files[0];
let files = this.inputRef.current.files;
if (!files || files.length <= 0) { return; }
let file = files.item(0);
if (!file) { return; } if (!file) { return; }
const fileParts = file.name.split('.'); const fileParts = file.name.split('.');
const fileName = fileParts.slice(0, -1); const fileName = fileParts.slice(0, -1);
@ -118,6 +115,13 @@ export class S3Upload extends Component<S3UploadProps, S3UploadState> {
}, 200); }, 200);
} }
onChange(): void {
if (!this.inputRef.current) { return; }
let files = this.inputRef.current.files;
if (!files || files.length <= 0) { return; }
this.uploadFiles(files);
}
onClick() { onClick() {
if (!this.inputRef.current) { return; } if (!this.inputRef.current) { return; }
this.inputRef.current.click(); this.inputRef.current.click();