Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add option to allow dropping a directory and reading the contents #834

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions ember-file-upload/src/components/file-dropzone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export default class FileDropzoneComponent extends Component<FileDropzoneArgs> {
return this.args.multiple ?? true;
}

get allowFolderDrop() {
return this.args.allowFolderDrop ?? false;
}

get files(): File[] {
const files = this.dataTransferWrapper?.files ?? [];
if (this.multiple) return files;
Expand Down Expand Up @@ -132,7 +136,7 @@ export default class FileDropzoneComponent extends Component<FileDropzoneArgs> {
}

@action
didDrop(event: FileUploadDragEvent) {
async didDrop(event: FileUploadDragEvent) {
if (this.dataTransferWrapper) {
this.dataTransferWrapper.dataTransfer = event.dataTransfer;
}
Expand Down Expand Up @@ -217,7 +221,14 @@ export default class FileDropzoneComponent extends Component<FileDropzoneArgs> {
// }

if (this.dataTransferWrapper) {
const addedFiles = this.addFiles(this.files);
let files;
if (this.allowFolderDrop) {
files = await this.dataTransferWrapper.getFilesAndDirectories();
} else {
files = this.files;
}

const addedFiles = this.addFiles(files);
this.args.onDrop?.(addedFiles, this.dataTransferWrapper);

this.active = false;
Expand Down
7 changes: 7 additions & 0 deletions ember-file-upload/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ export interface FileDropzoneArgs {
* */
allowUploadsFromWebsites?: boolean;

/**
*
*
* @defaulValue false
* */
allowFolderDrop?: boolean;

/**
* This is the type of cursor that should
* be shown when a drag event happens.
Expand Down
63 changes: 63 additions & 0 deletions ember-file-upload/src/system/data-transfer-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,69 @@ export default class DataTransferWrapper {
return this.files.length ? this.files : this.items;
}

async getFilesAndDirectories() {
const files: File[] = [];

const readEntry = async (entry: FileSystemEntry): Promise<File> => {
return new Promise((resolve, reject) => {
if (entry.isFile) {
(entry as FileSystemFileEntry).file((fileEntry: File) => {
resolve(fileEntry);
});
} else {
reject('Directory contains nested directories');
}
});
};

const readAllFilesInDirectory = (item: DataTransferItem): Promise<File[]> =>
new Promise((resolve) => {
(item.webkitGetAsEntry() as FileSystemDirectoryEntry)
?.createReader()
?.readEntries(async (entries: FileSystemEntry[]) => {
const readFiles: File[] = await Promise.all(
entries.map(readEntry)
).catch((err) => {
throw err;
});
resolve(readFiles.filter((file) => file !== undefined) as File[]);
});
});

const readDataTransferItem = async (
item: DataTransferItem
): Promise<File[]> => {
if (item.webkitGetAsEntry()?.isDirectory) {
const directoryFile = item.getAsFile() as File;
const filesInDirectory: File[] = await readAllFilesInDirectory(item);
return [directoryFile, ...filesInDirectory];
} else {
const fileItem = item.getAsFile() as File;
return [fileItem];
}
};

if (this.dataTransfer?.items) {
const allFilesInDataTransferItems: File[][] = await Promise.all(
Array.from(this.dataTransfer?.items).map(readDataTransferItem)
);

const flattenedFileArray: File[] = allFilesInDataTransferItems.reduce(
(flattenedList, fileList) => {
return [...flattenedList, ...fileList];
},
[]
);

files.push(...flattenedFileArray);
} else {
const droppedFiles: File[] = Array.from(this.dataTransfer?.files ?? []);
files.push(...droppedFiles);
}

return files;
}

get files() {
return Array.from(this.dataTransfer?.files ?? []);
}
Expand Down
54 changes: 54 additions & 0 deletions ember-file-upload/src/test-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,60 @@ export async function dragAndDrop(selector: string, ...files: (File | Blob)[]) {
return triggerEvent(dropzone, 'drop', { dataTransfer });
}

interface FileSystemEntryStub {
isFile: boolean;
file: (callback: (file: File | Blob) => void) => void;
}

export async function dragAndDropDirectory(
selector: string,
folderName: string,
filesInDirectory: (File | Blob)[],
...singleFiles: (File | Blob)[]
) {
const dropzone = find(selector);
assert(`Selector '${dropzone}' could not be found.`, dropzone);
assert(
'All files must be instances of File/Blob type',
filesInDirectory.every((file) => file instanceof Blob)
);

const folderItem = {
webkitGetAsEntry: () => ({
isDirectory: true,
createReader: () => ({
readEntries: (callback: (entries: FileSystemEntryStub[]) => void) => {
const entryFiles = filesInDirectory.map((file) => {
return {
isFile: true,
file: (callback: (file: File | Blob) => void) => {
callback(file);
},
};
});
callback(entryFiles);
},
}),
}),
getAsFile: () => new File([], folderName, { type: '' }),
};

const singleFileItem = (singleFile: File) => ({
webkitGetAsEntry: () => ({
isDirectory: false,
}),
getAsFile: () => singleFile,
});

const dataTransfer = {
items: [folderItem, ...singleFiles.map(singleFileItem)],
};

await triggerEvent(dropzone, 'dragenter', { dataTransfer });
await triggerEvent(dropzone, 'dragover', { dataTransfer });
return triggerEvent(dropzone, 'drop', { dataTransfer });
}

/**
Triggers a `dragenter` event on a `FileDropzone` with `files`.

Expand Down
2 changes: 1 addition & 1 deletion test-app/app/components/demo-upload.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{{#let (file-queue name="photos" onFileAdded=this.uploadProof) as |queue|}}
<div class="docs-my-8 text-center">
<FileDropzone @queue={{queue}} class="demo-dropzone" as |dropzone|>
<FileDropzone @queue={{queue}} @allowFolderDrop={{@allowFolderDrop}} class="demo-dropzone" as |dropzone|>
<div class="dropzone-upload-area upload {{if dropzone.active "active"}}">
{{#if dropzone.supported}}
<div class="emoji mb-16">
Expand Down
30 changes: 28 additions & 2 deletions test-app/tests/integration/components/file-dropzone-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { render, triggerEvent } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import {
dragAndDrop,
dragAndDropDirectory,
dragEnter,
dragLeave,
} from 'ember-file-upload/test-support';
Expand Down Expand Up @@ -160,9 +161,31 @@ module('Integration | Component | FileDropzone', function (hooks) {
assert.verifySteps(['dingus.txt']);
});

test('allowFolderDrop=true allows dropping a directory and reads out the content', async function (assert) {
this.onDrop = (files) => {
files.forEach((file) => assert.step(file.name));
};

await render(hbs`
<FileDropzone
class="test-dropzone"
@queue={{this.queue}}
@allowFolderDrop={{true}}
@onDrop={{this.onDrop}} />
`);

await dragAndDropDirectory('.test-dropzone', 'directory-name', [
new File([], 'dingus.txt'),
new File([], 'dingus.png'),
]);

assert.verifySteps(['directory-name', 'dingus.txt', 'dingus.png']);
});

// Check for regression of: https://github.com/adopted-ember-addons/ember-file-upload/issues/446
test('regression: drop events from other DOM nodes are not prevented', async function (assert) {
this.documentDragListener = () => assert.step('documentDragListener called');
this.documentDragListener = () =>
assert.step('documentDragListener called');
await render(hbs`
<FileDropzone @queue={{this.queue}} />

Expand All @@ -172,6 +195,9 @@ module('Integration | Component | FileDropzone', function (hooks) {

await triggerEvent('.independent-drag-target', 'drop');

assert.verifySteps(['documentDragListener called'], 'event reached documentDragListener');
assert.verifySteps(
['documentDragListener called'],
'event reached documentDragListener'
);
});
});
33 changes: 33 additions & 0 deletions test-app/tests/unit/system/data-transfer-wrapper-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,37 @@ module('Unit | DataTransferWrapper', function (hooks) {
};
assert.strictEqual(this.subject.filesOrItems.length, 2);
});

test('directory dropped', async function (assert) {
const transfer = {
items: [
folderItem('directory-name', [
new File([], 'fileName.txt'),
new File([], 'otherFileName.txt'),
]),
],
};
this.subject.dataTransfer = transfer;
assert.strictEqual((await this.subject.getFilesAndDirectories()).length, 3);
});

const folderItem = (folderName, filesInDirectory) => ({
webkitGetAsEntry: () => ({
isDirectory: true,
createReader: () => ({
readEntries: (callback) => {
const entryFiles = filesInDirectory.map((file) => {
return {
isFile: true,
file: (callback) => {
callback(file);
},
};
});
callback(entryFiles);
},
}),
}),
getAsFile: () => new File([], folderName, { type: '' }),
});
});