diff --git a/package.json b/package.json index 650868cb91e..ed654d0825d 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "nuxt": "^2.14.3", "postcss-cli": "^7.1.1", "prettier": "1.14.3", + "regenerator-runtime": "^0.13.7", "require-context": "^1.1.0", "rollup": "^2.26.5", "rollup-plugin-babel": "^4.4.0", diff --git a/scripts/create-web-types.js b/scripts/create-web-types.js index 68127073c7f..579c1e87c07 100644 --- a/scripts/create-web-types.js +++ b/scripts/create-web-types.js @@ -1,4 +1,5 @@ // Creates a web-types.json, tags.json and attributes.json files and places them in /dist +require('regenerator-runtime/runtime') const path = require('path') const fs = require('fs') const requireContext = require('require-context') diff --git a/src/components/form-file/form-file.js b/src/components/form-file/form-file.js index 3eda52041f0..4d82d751b75 100644 --- a/src/components/form-file/form-file.js +++ b/src/components/form-file/form-file.js @@ -24,6 +24,51 @@ const VALUE_EMPTY_DEPRECATED_MSG = const isValidValue = value => isFile(value) || (isArray(value) && value.every(v => isValidValue(v))) +// Drop handler function to get all files +/* istanbul ignore next: not supported in JSDOM */ +const getAllFileEntries = async dataTransferItemList => { + const fileEntries = [] + const queue = [] + // Unfortunately `dataTransferItemList` is not iterable i.e. no `.forEach()` + for (let i = 0; i < dataTransferItemList.length; i++) { + queue.push(dataTransferItemList[i].webkitGetAsEntry()) + } + while (queue.length > 0) { + const entry = queue.shift() + if (entry.isFile) { + fileEntries.push(entry) + } else if (entry.isDirectory) { + queue.push(...(await readAllDirectoryEntries(entry.createReader()))) + } + } + return fileEntries +} + +// Get all the entries (files or sub-directories) in a directory +// by calling `.readEntries()` until it returns empty array +/* istanbul ignore next: not supported in JSDOM */ +const readAllDirectoryEntries = async directoryReader => { + const entries = [] + let readEntries = await readEntriesPromise(directoryReader) + while (readEntries.length > 0) { + entries.push(...readEntries) + readEntries = await readEntriesPromise(directoryReader) + } + return entries +} + +// Wrap `.readEntries()` in a promise to make working with it easier +// `.readEntries()` will return only some of the entries in a directory +// (e.g. Chrome returns at most 100 entries at a time) +/* istanbul ignore next: not supported in JSDOM */ +const readEntriesPromise = async directoryReader => { + try { + return await new Promise((resolve, reject) => { + directoryReader.readEntries(resolve, reject) + }) + } catch {} +} + // @vue/component export const BFormFile = /*#__PURE__*/ Vue.extend({ name: NAME, @@ -202,40 +247,23 @@ export const BFormFile = /*#__PURE__*/ Vue.extend({ // Always emit original event this.$emit('change', evt) // Check if special `items` prop is available on event (drop mode) - // Can be disabled by setting no-traverse - const items = evt.dataTransfer && evt.dataTransfer.items + // Can be disabled by setting `no-traverse` + const { files, items } = evt.dataTransfer || {} /* istanbul ignore next: not supported in JSDOM */ if (items && !this.noTraverse) { - const queue = [] - for (let i = 0; i < items.length; i++) { - const item = items[i].webkitGetAsEntry() - if (item) { - queue.push(this.traverseFileTree(item)) - } - } - Promise.all(queue).then(filesArr => { - this.setFiles(arrayFrom(filesArr)) + getAllFileEntries(items).then(files => { + this.setFiles(files) }) - return + } else { + // Normal handling + this.setFiles(evt.target.files || files) } - // Normal handling - this.setFiles(evt.target.files || evt.dataTransfer.files) }, - setFiles(files = []) { - if (!files) { - /* istanbul ignore next: this will probably not happen */ - this.selectedFile = null - } else if (this.multiple) { - // Convert files to array - const filesArray = [] - for (let i = 0; i < files.length; i++) { - filesArray.push(files[i]) - } - // Return file(s) as array - this.selectedFile = filesArray + setFiles(files) { + if (this.multiple) { + this.selectedFile = arrayFrom(files || []) } else { - // Return single file object - this.selectedFile = files[0] || null + this.selectedFile = files ? files[0] || null : null } }, onReset() { @@ -265,39 +293,12 @@ export const BFormFile = /*#__PURE__*/ Vue.extend({ return } this.dragging = false - if (evt.dataTransfer.files && evt.dataTransfer.files.length > 0) { - this.onFileChange(evt) - } - }, - /* istanbul ignore next: not supported in JSDOM */ - traverseFileTree(item, path) /* istanbul ignore next */ { - // Based on https://stackoverflow.com/questions/3590058 - return new Promise(resolve => { - path = path || '' - if (item.isFile) { - // Get file - item.file(file => { - file.$path = path // Inject $path to file obj - resolve(file) - }) - } else if (item.isDirectory) { - // Get folder contents - item.createReader().readEntries(entries => { - const queue = [] - for (let i = 0; i < entries.length; i++) { - queue.push(this.traverseFileTree(entries[i], path + item.name + '/')) - } - Promise.all(queue).then(filesArr => { - resolve(arrayFrom(filesArr)) - }) - }) - } - }) + this.onFileChange(evt) } }, render(h) { // Form Input - const input = h('input', { + const $input = h('input', { ref: 'input', class: [ { @@ -317,11 +318,11 @@ export const BFormFile = /*#__PURE__*/ Vue.extend({ }) if (this.plain) { - return input + return $input } // Overlay Labels - const label = h( + const $label = h( 'label', { staticClass: 'custom-file-label', @@ -352,7 +353,7 @@ export const BFormFile = /*#__PURE__*/ Vue.extend({ drop: this.onDrop } }, - [input, label] + [$input, $label] ) } }) diff --git a/yarn.lock b/yarn.lock index ffbaf7a2e4b..f4e93d381cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11580,7 +11580,7 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.4: +regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: version "0.13.7" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==