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

@uppy/utils: modernize getDroppedFiles #3534

Merged
merged 2 commits into from Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions e2e/cypress/fixtures/images/cat-symbolic-link
1 change: 1 addition & 0 deletions e2e/cypress/fixtures/images/cat-symbolic-link.jpg
16 changes: 16 additions & 0 deletions e2e/cypress/integration/dashboard-ui.spec.ts
Expand Up @@ -2,6 +2,7 @@ describe('dashboard-ui', () => {
beforeEach(() => {
cy.visit('/dashboard-ui')
cy.get('.uppy-Dashboard-input:first').as('file-input')
cy.get('.uppy-Dashboard-AddFiles').as('drop-target')
})

it('should not throw when calling uppy.close()', () => {
Expand All @@ -18,4 +19,19 @@ describe('dashboard-ui', () => {
.should('have.length', 2)
.each((element) => expect(element).attr('src').to.include('blob:'))
})

it('should support drag&drop', () => {
cy.get('@drop-target').selectFile([
'cypress/fixtures/images/cat.jpg',
'cypress/fixtures/images/cat-symbolic-link',
'cypress/fixtures/images/cat-symbolic-link.jpg',
'cypress/fixtures/images/traffic.jpg',
], { action: 'drag-drop' })

cy.get('.uppy-Dashboard-Item')
.should('have.length', 4)
cy.get('.uppy-Dashboard-Item-previewImg')
.should('have.length', 3)
.each((element) => expect(element).attr('src').to.include('blob:'))
})
})
13 changes: 9 additions & 4 deletions packages/@uppy/utils/src/getDroppedFiles/index.js
Expand Up @@ -15,11 +15,16 @@ import fallbackApi from './utils/fallbackApi.js'
*
* @returns {Promise} - Array<File>
*/
export default function getDroppedFiles (dataTransfer, { logDropError = () => {} } = {}) {
export default async function getDroppedFiles (dataTransfer, { logDropError = () => {} } = {}) {
// Get all files from all subdirs. Works (at least) in Chrome, Mozilla, and Safari
if (dataTransfer.items?.[0] && 'webkitGetAsEntry' in dataTransfer.items[0]) {
return webkitGetAsEntryApi(dataTransfer, logDropError)
try {
const accumulator = []
for await (const file of webkitGetAsEntryApi(dataTransfer, logDropError)) {
accumulator.push(file)
}
return accumulator
// Otherwise just return all first-order files
} catch {
return fallbackApi(dataTransfer)
}
return fallbackApi(dataTransfer)
}
Expand Up @@ -13,9 +13,9 @@ export default function getFilesAndDirectoriesFromDirectory (directoryReader, ol
// According to the FileSystem API spec, getFilesAndDirectoriesFromDirectory()
// must be called until it calls the onSuccess with an empty array.
if (entries.length) {
setTimeout(() => {
queueMicrotask(() => {
getFilesAndDirectoriesFromDirectory(directoryReader, newEntries, logDropError, { onSuccess })
}, 0)
})
// Done iterating this particular directory
} else {
onSuccess(newEntries)
Expand Down
@@ -1,56 +1,61 @@
import getRelativePath from './getRelativePath.js'
import getFilesAndDirectoriesFromDirectory from './getFilesAndDirectoriesFromDirectory.js'
import toArray from '../../../toArray.js'

export default function webkitGetAsEntryApi (dataTransfer, logDropError) {
const files = []

const rootPromises = []

/**
* Returns a resolved promise, when :files array is enhanced
*
* @param {(FileSystemFileEntry|FileSystemDirectoryEntry)} entry
* @returns {Promise} - empty promise that resolves when :files is enhanced with a file
*/
const createPromiseToAddFileOrParseDirectory = (entry) => new Promise((resolve) => {
// This is a base call
if (entry.isFile) {
// Creates a new File object which can be used to read the file.
entry.file(
(file) => {
// eslint-disable-next-line no-param-reassign
file.relativePath = getRelativePath(entry)
files.push(file)
resolve()
},
// Make sure we resolve on error anyway, it's fine if only one file couldn't be read!
(error) => {
logDropError(error)
resolve()
},
)
// This is a recursive call
} else if (entry.isDirectory) {
/**
* Interop between deprecated webkitGetAsEntry and standard getAsFileSystemHandle.
*/
function getAsFileSystemHandleFromEntry (entry, logDropError) {
if (entry == null) return entry
return {
// eslint-disable-next-line no-nested-ternary
kind: entry.isFile ? 'file' : entry.isDirectory ? 'directory' : undefined,
getFile () {
return new Promise((resolve, reject) => entry.file(resolve, reject))
},
async* values () {
// If the file is a directory.
const directoryReader = entry.createReader()
getFilesAndDirectoriesFromDirectory(directoryReader, [], logDropError, {
onSuccess: (entries) => resolve(Promise.all(
entries.map(createPromiseToAddFileOrParseDirectory),
)),
const entries = await new Promise(resolve => {
getFilesAndDirectoriesFromDirectory(directoryReader, [], logDropError, {
onSuccess: (dirEntries) => resolve(dirEntries.map(file => getAsFileSystemHandleFromEntry(file, logDropError))),
})
})
}
})
yield* entries
},
}
}

async function* createPromiseToAddFileOrParseDirectory (entry) {
// For each dropped item, - make sure it's a file/directory, and start deepening in!
toArray(dataTransfer.items)
.forEach((item) => {
const entry = item.webkitGetAsEntry()
// :entry can be null when we drop the url e.g.
if (entry) {
rootPromises.push(createPromiseToAddFileOrParseDirectory(entry))
}
})
if (entry.kind === 'file') {
const file = await entry.getFile()
if (file !== null) {
file.relativePath = getRelativePath(entry)
yield file
}
} else if (entry.kind === 'directory') {
for await (const handle of entry.values()) {
yield* createPromiseToAddFileOrParseDirectory(handle)
}
}
}

return Promise.all(rootPromises)
.then(() => files)
export default async function* getFilesFromDataTransfer (dataTransfer, logDropError) {
for (const item of dataTransfer.items) {
const lastResortFile = item.getAsFile() // Chromium bug, see https://github.com/transloadit/uppy/issues/3505.
const entry = await item.getAsFileSystemHandle?.()
?? getAsFileSystemHandleFromEntry(item.webkitGetAsEntry(), logDropError)
// :entry can be null when we drop the url e.g.
if (entry != null) {
try {
yield* createPromiseToAddFileOrParseDirectory(entry, logDropError)
} catch (err) {
if (lastResortFile) {
yield lastResortFile
} else {
logDropError(err)
}
}
}
}
}