From 840f8d94fa52c0c007322e05638516cf9178a155 Mon Sep 17 00:00:00 2001 From: Giles Thompson Date: Tue, 22 Feb 2022 10:36:13 +1300 Subject: [PATCH] Update FileDropzone API (#678) Adds new arguments `@queue` and `@filter` to FileDropzone. Includes basic test coverage of these new APIs while deprecated API tests still pass. Adds typing to FileDropzone component and DataTransferWrapper internal utility. I could have taken this further but I think it's good to do this process incrementally. Honestly there's a lot to untangle in some of these internal utilities (regarding typing support). Registers/unregisters the FileDropzone as an event listener on the queue in the method from #654. Also drops interim file-filtering techniques (in onDrop and onSelect). Closes #679 --- docs/validation.md | 23 +- .../addon/components/file-dropzone.hbs | 7 +- .../addon/components/file-dropzone.js | 301 ---------------- .../addon/components/file-dropzone.ts | 323 ++++++++++++++++++ .../addon/components/file-upload.ts | 9 +- ember-file-upload/addon/queue.ts | 41 +-- .../addon/system/data-transfer-wrapper.ts | 57 ++++ .../addon/system/data-transfer.js | 72 ---- .../dummy/app/components/demo-upload.hbs | 90 ++--- .../tests/helpers/file-queue-helper-test.js | 2 +- .../components/file-dropzone-test.js | 290 +++++++++------- .../tests/unit/system/data-transfer-test.js | 126 ------- .../unit/system/data-transfer-wrapper-test.js | 69 ++++ 13 files changed, 674 insertions(+), 736 deletions(-) delete mode 100644 ember-file-upload/addon/components/file-dropzone.js create mode 100644 ember-file-upload/addon/components/file-dropzone.ts create mode 100644 ember-file-upload/addon/system/data-transfer-wrapper.ts delete mode 100644 ember-file-upload/addon/system/data-transfer.js delete mode 100644 ember-file-upload/tests/unit/system/data-transfer-test.js create mode 100644 ember-file-upload/tests/unit/system/data-transfer-wrapper-test.js diff --git a/docs/validation.md b/docs/validation.md index a62a1619..50cdc282 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -10,13 +10,9 @@ For more details see the [MDN accept attribute reference](https://developer.mozi ## Custom validation -Both components provide callback arguments with which you may implement custom validation of chosen files. +Both `` and `` components accept the `@filter` argument with which you may implement custom validation of chosen files. -`` may be passed `@onSelect` and `` may be passed `@onDrop`. - -These are called *after* files have been chosen and *before* `@onFileAdd` is called by the queue. - -To implement validation, you may (optionally) return a subset of files from these callbacks to restrict which files are queued for upload. +This are called *after* files have been chosen and *before* `fileAdded` is called by the queue. See the example below where the same validation callback is used for both selection methods. @@ -24,15 +20,14 @@ Commonly validated file properties are `type`, `name` and `size`. For more detai ```hbs ... ... @@ -50,8 +45,8 @@ const allowedTypes = [ export default class ExampleComponent extends Component { ... - validateFiles(files) { - return files.filter((file) => allowedTypes.includes(file.type)); + validateFile(file) { + return allowedTypes.includes(file.type); } } ``` diff --git a/ember-file-upload/addon/components/file-dropzone.hbs b/ember-file-upload/addon/components/file-dropzone.hbs index 99cfaa51..c89d2155 100644 --- a/ember-file-upload/addon/components/file-dropzone.hbs +++ b/ember-file-upload/addon/components/file-dropzone.hbs @@ -6,12 +6,7 @@ dragover=this.didDragOver drop=this.didDrop }} - {{update-queue - this.queue - multiple=@multiple - disabled=@disabled - onFileAdd=@onFileAdd - }} + {{this.bindListeners}} > {{yield (hash supported=this.supported active=this.active) diff --git a/ember-file-upload/addon/components/file-dropzone.js b/ember-file-upload/addon/components/file-dropzone.js deleted file mode 100644 index 4e7cf449..00000000 --- a/ember-file-upload/addon/components/file-dropzone.js +++ /dev/null @@ -1,301 +0,0 @@ -import Component from '@glimmer/component'; -import { inject as service } from '@ember/service'; -import { getOwner } from '@ember/application'; -import DataTransfer from '../system/data-transfer'; -import parseHTML from '../system/parse-html'; -import { action } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; - -let supported = (function () { - return ( - typeof window !== 'undefined' && - window.document && - 'draggable' in document.createElement('span') - ); -})(); - -/** - Whether multiple files can be selected when uploading. - @argument multiple - @type {boolean} - */ - -/** - The name of the queue to upload the file to. - - @argument name - @type {string} - @required - */ - -/** - If set, disables input and prevents files from being added to the queue - - @argument disabled - @type {boolean} - @default false - */ - -/** - `onFileAdd` is called when a file is added to the upload queue. - - When multiple files are added, this function - is called once for every file. - - @argument onFileAdd - @type {function(file: File)} - @required - */ - -/** - `onDragEnter` is called when files have entered - the dropzone. - - @argument onDragEnter - @type {function(files: File[], dataTransfer: DataTransfer)} - */ - -/** - `onDragLeave` is called when files have left - the dropzone. - - @argument onDragLeave - @type {function(files: File[], dataTransfer: DataTransfer)} - */ - -/** - `onDrop` is called when file have been dropped on the dropzone. - - Optionally restrict which files are added to the upload queue by - returning an array of File objects. - - @argument onDrop - @type {function(files: File[], dataTransfer: DataTransfer)} - */ - -/** - Whether users can upload content - from websites by dragging images from - another webpage and dropping it into - your app. The default is `false` to - prevent cross-site scripting issues. - - @argument allowUploadsFromWebsites - @type {boolean} - @default false - */ - -/** - This is the type of cursor that should - be shown when a drag event happens. - - Corresponds to `dropEffect`. - - This is one of the following: - - - `copy` - - `move` - - `link` - - @argument cursor - @type {string} - @default null - */ - -/** - `FileDropzone` is a component that will allow users to upload files by - drag and drop. - - ```hbs - - {{#if dropzone.active}} - Drop to upload - {{else if queue.files.length}} - Uploading {{queue.files.length}} files. ({{queue.progress}}%) - {{else}} -

Upload Images

-

- {{#if dropzone.supported}} - Drag and drop images onto this area to upload them or - {{/if}} - - Add an Image. - -

- {{/if}} -
- ``` - - ```js - import Component from '@glimmer/component'; - import { task } from 'ember-concurrency'; - - export default class ExampleComponent extends Component { - @task({ maxConcurrency: 3, enqueue: true }) - *uploadImage(file) { - const response = yield file.upload(url, options); - ... - } - } - ``` - - @class FileDropzoneComponent - @type Ember.Component - @yield {Hash} dropzone - @yield {boolean} dropzone.supported - @yield {boolean} dropzone.active - @yield {Queue} queue - */ -export default class FileDropzoneComponent extends Component { - @service fileQueue; - - @tracked supported = supported; - @tracked active = false; - @tracked dataTransfer; - - get queue() { - if (!this.args.name) return null; - - return ( - this.fileQueue.find(this.args.name) || - this.fileQueue.create(this.args.name) - ); - } - - get files() { - return this.dataTransfer?.files; - } - - isAllowed() { - const { environment } = - getOwner(this).resolveRegistration('config:environment'); - - return ( - environment === 'test' || - this.dataTransfer.source === 'os' || - this.args.allowUploadsFromWebsites - ); - } - - @action - didEnterDropzone(evt) { - this.dataTransfer = new DataTransfer({ - queue: this.queue, - source: evt.source, - dataTransfer: evt.dataTransfer, - itemDetails: evt.itemDetails, - }); - - if (this.isAllowed()) { - evt.dataTransfer.dropEffect = this.args.cursor; - this.active = true; - - this.args.onDragEnter?.(this.files, this.dataTransfer); - } - } - - @action - didLeaveDropzone(evt) { - this.dataTransfer.dataTransfer = evt.dataTransfer; - if (this.isAllowed()) { - if (evt.dataTransfer) { - evt.dataTransfer.dropEffect = this.args.cursor; - } - this.args.onDragLeave?.(this.files, this.dataTransfer); - this.dataTransfer = null; - - if (this.isDestroyed) { - return; - } - this.active = false; - } - } - - @action - didDragOver(evt) { - this.dataTransfer.dataTransfer = evt.dataTransfer; - if (this.isAllowed()) { - evt.dataTransfer.dropEffect = this.args.cursor; - } - } - - @action - didDrop(evt) { - this.dataTransfer.dataTransfer = evt.dataTransfer; - - if (!this.isAllowed()) { - evt.dataTransfer.dropEffect = this.args.cursor; - this.dataTransfer = null; - return; - } - - // Testing support for dragging and dropping images - // from other browser windows - let url; - - let html = this.dataTransfer.getData('text/html'); - if (html) { - let parsedHtml = parseHTML(html); - let img = parsedHtml.getElementsByTagName('img')[0]; - if (img) { - url = img.src; - } - } - - if (url == null) { - url = this.dataTransfer.getData('text/uri-list'); - } - - if (url) { - var image = new Image(); - var [filename] = url.split('/').slice(-1); - image.crossOrigin = 'anonymous'; - image.onload = () => { - var canvas = document.createElement('canvas'); - canvas.width = image.width; - canvas.height = image.height; - - var ctx = canvas.getContext('2d'); - ctx.drawImage(image, 0, 0); - - if (canvas.toBlob) { - canvas.toBlob((blob) => { - let [file] = this.queue._addFiles([blob], 'web'); - file.name = filename; - }); - } else { - let binStr = atob(canvas.toDataURL().split(',')[1]); - let len = binStr.length; - let arr = new Uint8Array(len); - - for (var i = 0; i < len; i++) { - arr[i] = binStr.charCodeAt(i); - } - let blob = new Blob([arr], { type: 'image/png' }); - blob.name = filename; - let [file] = this.queue._addFiles([blob], 'web'); - file.name = filename; - } - }; - /* eslint-disable no-console */ - image.onerror = function (e) { - console.log(e); - }; - /* eslint-enable no-console */ - image.src = url; - } - - const files = - this.args.onDrop?.(this.files, this.dataTransfer) ?? this.files; - - // Add files to upload queue. - this.active = false; - this.queue._addFiles(files, 'drag-and-drop'); - this.dataTransfer = null; - } -} diff --git a/ember-file-upload/addon/components/file-dropzone.ts b/ember-file-upload/addon/components/file-dropzone.ts new file mode 100644 index 00000000..8b2bd803 --- /dev/null +++ b/ember-file-upload/addon/components/file-dropzone.ts @@ -0,0 +1,323 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { getOwner } from '@ember/application'; +import DataTransferWrapper, { + FileUploadDragEvent, +} from '../system/data-transfer-wrapper'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import Queue from '../queue'; +import UploadFile, { FileSource } from 'ember-file-upload/upload-file'; +import FileQueueService, { DEFAULT_QUEUE } from '../services/file-queue'; +import { modifier } from 'ember-modifier'; + +interface FileDropzoneArgs { + queue?: Queue; + + /** + * Whether users can upload content from websites by dragging images from + * another webpage and dropping it into your app. The default is `false` + * to prevent cross-site scripting issues. + * */ + allowUploadsFromWebsites?: boolean; + + /** + * This is the type of cursor that should + * be shown when a drag event happens. + * + * Corresponds to `DataTransfer.dropEffect`. + * (https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/dropEffect) + */ + cursor?: 'link' | 'none' | 'copy' | 'move'; + + // actions + filter?: (file: UploadFile) => boolean; + + /** + * Called when files have entered the dropzone. + */ + onDragEnter?: ( + files: File[] | DataTransferItem[], + dataTransfer: DataTransferWrapper + ) => void; + + /** + * Called when files have left the dropzone. + */ + onDragLeave?: ( + files: File[] | DataTransferItem[], + dataTransfer: DataTransferWrapper + ) => void; + + /** + * Called when file have been dropped on the dropzone. + */ + onDrop?: (files: UploadFile[], dataTransfer: DataTransferWrapper) => void; + + // old/deprecated API + + /** + * @deprecated use `{{file-queue}}` helper with `{{queue.selectFile}}` modifier + */ + accept?: string; + + /** + * @deprecated use `{{file-queue}}` helper with `{{queue.selectFile}}` modifier + */ + disabled?: boolean; + + /** + * @deprecated use `{{file-queue}}` helper with `{{queue.selectFile}}` modifier + */ + multiple?: boolean; + + /** @deprecated use `queue` instead */ + name?: string; + + /** + * @deprecated use `{{file-queue}}` helper with `{{queue.selectFile}}` modifier + */ + capture?: string; + + /** + * @deprecated use `{{file-queue}}` helper with `{{queue.selectFile}}` modifier + */ + for?: string; + + /** + * @deprecated use `onDrop()` instead + */ + onFileAdd: (file: UploadFile) => void; +} + +/** + `FileDropzone` is a component that will allow users to upload files by + drag and drop. + + ```hbs + + {{#if dropzone.active}} + Drop to upload + {{else if queue.files.length}} + Uploading {{queue.files.length}} files. ({{queue.progress}}%) + {{else}} +

Upload Images

+

+ {{#if dropzone.supported}} + Drag and drop images onto this area to upload them or + {{/if}} +

+ {{/if}} +
+ ``` + + @class FileDropzoneComponent + @type Ember.Component + @yield {Hash} dropzone + @yield {boolean} dropzone.supported + @yield {boolean} dropzone.active + @yield {Queue} queue + */ +export default class FileDropzoneComponent extends Component { + @service declare fileQueue: FileQueueService; + + @tracked active = false; + @tracked dataTransferWrapper?: DataTransferWrapper; + + supported = (() => + typeof window !== 'undefined' && + window.document && + 'draggable' in document.createElement('span'))(); + + get queue() { + if (this.args.queue) { + return this.args.queue; + } + + return this.fileQueue.findOrCreate(this.args.name ?? DEFAULT_QUEUE); + } + + get multiple() { + return this.args.multiple ?? true; + } + + get files(): File[] | DataTransferItem[] { + const files = this.dataTransferWrapper?.filesOrItems ?? []; + if (this.multiple) return files; + + return files.slice(0, 1); + } + + get isAllowed() { + const { environment } = + getOwner(this).resolveRegistration('config:environment'); + + return ( + environment === 'test' || + (this.dataTransferWrapper && this.dataTransferWrapper.source === 'os') || + this.args.allowUploadsFromWebsites + ); + } + + get cursor() { + return this.args.cursor ?? 'copy'; + } + + bindListeners = modifier(() => { + this.queue.addListener(this); + return () => this.queue.removeListener(this); + }); + + fileAdded(file: UploadFile) { + this.args.onFileAdd?.(file); + } + + @action + didEnterDropzone(event: FileUploadDragEvent) { + this.dataTransferWrapper = new DataTransferWrapper(event); + + if (this.isAllowed) { + event.dataTransfer.dropEffect = this.cursor; + this.active = true; + + this.args.onDragEnter?.(this.files, this.dataTransferWrapper); + } + } + + @action + didLeaveDropzone(event: FileUploadDragEvent) { + if (this.dataTransferWrapper) { + this.dataTransferWrapper.dataTransfer = event.dataTransfer; + } + if (this.dataTransferWrapper && this.isAllowed) { + if (event.dataTransfer) { + event.dataTransfer.dropEffect = this.cursor; + } + this.args.onDragLeave?.(this.files, this.dataTransferWrapper); + + this.dataTransferWrapper = undefined; + + if (this.isDestroyed) { + return; + } + this.active = false; + } + } + + @action + didDragOver(event: FileUploadDragEvent) { + if (this.dataTransferWrapper) { + this.dataTransferWrapper.dataTransfer = event.dataTransfer; + } + if (this.isAllowed) { + event.dataTransfer.dropEffect = this.cursor; + } + } + + @action + didDrop(event: FileUploadDragEvent) { + if (this.dataTransferWrapper) { + this.dataTransferWrapper.dataTransfer = event.dataTransfer; + } + + if (!this.isAllowed) { + event.dataTransfer.dropEffect = this.cursor; + this.dataTransferWrapper = undefined; + return; + } + + // @TODO - add tests for these or remove them + // // Testing support for dragging and dropping images + // // from other browser windows + // let url; + + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // const html = this.dataTransferWrapper.getData('text/html'); + // if (html) { + // const parsedHtml = parseHTML(html); + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // const img = parsedHtml.getElementsByTagName('img')[0]; + // if (img) { + // url = img.src; + // } + // } + + // if (url == null) { + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // url = this.dataTransferWrapper.getData('text/uri-list'); + // } + + // if (url) { + // const image = new Image(); + // const [filename] = url.split('/').slice(-1); + // image.crossOrigin = 'anonymous'; + // image.onload = () => { + // const canvas = document.createElement('canvas'); + // canvas.width = image.width; + // canvas.height = image.height; + + // const ctx = canvas.getContext('2d'); + // ctx?.drawImage(image, 0, 0); + + // if (canvas.toBlob) { + // canvas.toBlob((blob) => { + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // const [file] = this.addFiles([blob], FileSource.web); + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // file.name = filename; + // }); + // } else { + // const binStr = atob(canvas.toDataURL().split(',')[1]); + // const len = binStr.length; + // const arr = new Uint8Array(len); + + // for (let i = 0; i < len; i++) { + // arr[i] = binStr.charCodeAt(i); + // } + // const blob = new Blob([arr], { type: 'image/png' }); + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // blob.name = filename; + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // const [file] = this.addFiles([blob], FileSource.web); + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // file.name = filename; + // } + // }; + // /* eslint-disable no-console */ + // image.onerror = function (e) { + // console.log(e); + // }; + // /* eslint-enable no-console */ + // image.src = url; + // } + + if (this.dataTransferWrapper) { + const addedFiles = this.addFiles(this.files); + this.args.onDrop?.(addedFiles, this.dataTransferWrapper); + + this.active = false; + this.dataTransferWrapper = undefined; + } + } + + addFiles(files: File[] | DataTransferItem[]) { + const addedFiles = []; + for (const file of files) { + if (file instanceof File) { + const uploadFile = new UploadFile(file, FileSource.DragAndDrop); + if (this.args.filter && !this.args.filter(uploadFile)) continue; + this.queue.add(uploadFile); + addedFiles.push(uploadFile); + } + } + return addedFiles; + } +} diff --git a/ember-file-upload/addon/components/file-upload.ts b/ember-file-upload/addon/components/file-upload.ts index e1046c24..e0a690e2 100644 --- a/ember-file-upload/addon/components/file-upload.ts +++ b/ember-file-upload/addon/components/file-upload.ts @@ -47,16 +47,9 @@ interface FileUploadArgs { for?: string; /** - * @deprecated use `fileAdded()` instead + * @deprecated use `filesSelected()` instead */ onFileAdd: (file: UploadFile) => void; - - // @TODO remove `onSelect` in favor of `filter()` - it never was officially - // of public API - /** - * @deprecated use `filter()` instead - */ - onSelect?: (files: UploadFile[]) => UploadFile[]; } /** diff --git a/ember-file-upload/addon/queue.ts b/ember-file-upload/addon/queue.ts index 75cb28d4..eed89d57 100644 --- a/ember-file-upload/addon/queue.ts +++ b/ember-file-upload/addon/queue.ts @@ -1,7 +1,6 @@ import { action } from '@ember/object'; -import { next } from '@ember/runloop'; import { modifier, ModifierArgs } from 'ember-modifier'; -import { TrackedArray, TrackedSet } from 'tracked-built-ins'; +import { TrackedSet } from 'tracked-built-ins'; import UploadFile, { FileSource, FileState } from './upload-file'; import FileQueueService from './services/file-queue'; @@ -76,9 +75,6 @@ export default class Queue { return [...this.#distinctFiles.values()]; } - // @TODO: Is this needed? I think, this is what each dropzone needs to manage - _dropzones = new TrackedArray([]); - /** * The total size of all files currently being uploaded in bytes. * @@ -173,41 +169,6 @@ export default class Queue { } } - /** - @private - @method _addFiles - @param {FileList} fileList The event triggered from the DOM that contains a list of files - */ - _addFiles(fileList: FileList, source: FileSource) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const onFileAdd = this.onFileAdd; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const disabled = this.disabled; - const files: UploadFile[] = []; - - if (!disabled) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - for (let i = 0, len = fileList.length || fileList.size; i < len; i++) { - const file = fileList.item ? fileList.item(i) : fileList[i]; - if (file instanceof File) { - const uploadFile = new UploadFile(file, source); - - files.push(uploadFile); - this.push(uploadFile); - - if (onFileAdd) { - next(onFileAdd, uploadFile); - } - } - } - } - - return files; - } - /** * Flushes the `files` property if they have settled. This * will only flush files when all files have arrived at a terminus diff --git a/ember-file-upload/addon/system/data-transfer-wrapper.ts b/ember-file-upload/addon/system/data-transfer-wrapper.ts new file mode 100644 index 00000000..ffbf1a74 --- /dev/null +++ b/ember-file-upload/addon/system/data-transfer-wrapper.ts @@ -0,0 +1,57 @@ +export interface FileUploadDragEvent extends DragEvent { + source: 'os' | 'web'; + dataTransfer: DataTransfer; + itemDetails: DataTransferItem[]; +} + +const getDataSupport = {}; + +export default class DataTransferWrapper { + dataTransfer?: DataTransfer; + itemDetails?: DataTransferItem[]; + source?: FileUploadDragEvent['source']; + + constructor(event: FileUploadDragEvent) { + this.source = event.source; + this.dataTransfer = event.dataTransfer; + this.itemDetails = event.itemDetails; + } + + getData(type: string) { + const dataTransfer = this.dataTransfer; + if (!dataTransfer) return; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (getDataSupport[type] == null) { + try { + const data = dataTransfer.getData(type); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + getDataSupport[type] = true; + return data; + } catch (e) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + getDataSupport[type] = false; + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + } else if (getDataSupport[type]) { + return dataTransfer.getData(type); + } + return ''; + } + + get filesOrItems() { + return this.files.length ? this.files : this.items; + } + + get files() { + return Array.from(this.dataTransfer?.files ?? []); + } + + get items() { + return this.itemDetails ?? Array.from(this.dataTransfer?.items ?? []); + } +} diff --git a/ember-file-upload/addon/system/data-transfer.js b/ember-file-upload/addon/system/data-transfer.js deleted file mode 100644 index 00beb24a..00000000 --- a/ember-file-upload/addon/system/data-transfer.js +++ /dev/null @@ -1,72 +0,0 @@ -import { A } from '@ember/array'; - -const getDataSupport = {}; - -export default class DataTransfer { - dataTransfer = null; - itemDetails = null; - - queue = null; - source = null; - - constructor({ queue, source, dataTransfer, itemDetails }) { - this.queue = queue; - this.source = source; - this.dataTransfer = dataTransfer; - this.itemDetails = itemDetails; - } - - getData(type) { - let dataTransfer = this.dataTransfer; - if (getDataSupport[type] == null) { - try { - let data = dataTransfer.getData(type); - getDataSupport[type] = true; - return data; - } catch (e) { - getDataSupport[type] = false; - } - } else if (getDataSupport[type]) { - return dataTransfer.getData(type); - } - } - - get files() { - let fileList = this.dataTransfer?.files || null; - let itemList = this.dataTransfer?.items || null; - let itemDetails = this.itemDetails; - - if ( - (fileList == null && itemList) || - (itemList != null && - fileList != null && - itemList.length > fileList.length) - ) { - fileList = itemList; - } - - if ( - (fileList == null && itemDetails) || - (itemDetails != null && - fileList != null && - itemDetails.length > fileList.length) - ) { - fileList = itemDetails; - } - - if (fileList == null) { - return null; - } - - let files = A(); - if (!this.queue?.multiple && fileList.length > 1) { - files.push(fileList[0]); - } else { - for (let i = 0, len = fileList.length; i < len; i++) { - files.push(fileList[i]); - } - } - - return files; - } -} diff --git a/ember-file-upload/tests/dummy/app/components/demo-upload.hbs b/ember-file-upload/tests/dummy/app/components/demo-upload.hbs index 45a63d3a..7e43118b 100644 --- a/ember-file-upload/tests/dummy/app/components/demo-upload.hbs +++ b/ember-file-upload/tests/dummy/app/components/demo-upload.hbs @@ -1,48 +1,50 @@ -
- -
- {{#if dropzone.supported}} -
- {{#if dropzone.active}} - ✨👽✨ - {{else}} - 👽 - {{/if}} -
- {{/if}} - -

+{{#let (file-queue name="photos" fileAdded=this.uploadProof) as |queue|}} +

+ +
{{#if dropzone.supported}} - Drag image, video or audio files here or +
+ {{#if dropzone.active}} + ✨👽✨ + {{else}} + 👽 + {{/if}} +
{{/if}} - - choose files - - to upload. -

- {{#if queue.files.length}} - Uploading {{queue.files.length}} files. ({{queue.progress}}%) - {{/if}} -
-
-
-
-
    - {{#each @files as |file|}} -
  • -
    - {{#if file.file}} -
    {{file.file.progress}}%
    - {{else if (equals file.type "image")}} - {{file.filename}} - {{else if (equals file.type "video")}} - +

    + {{#if dropzone.supported}} + Drag image, video or audio files here or {{/if}} - {{file.filename}} -

    -
  • - {{/each}} -
-
+ + choose files + + to upload. +

+ + {{#if queue.files.length}} + Uploading {{queue.files.length}} files. ({{queue.progress}}%) + {{/if}} +
+
+
+
+
    + {{#each @files as |file|}} +
  • +
    + {{#if file.file}} +
    {{file.file.progress}}%
    + {{else if (equals file.type "image")}} + {{file.filename}} + {{else if (equals file.type "video")}} + + {{/if}} + {{file.filename}} +
    +
  • + {{/each}} +
+
+{{/let}} diff --git a/ember-file-upload/tests/helpers/file-queue-helper-test.js b/ember-file-upload/tests/helpers/file-queue-helper-test.js index 62167815..d0c336d3 100644 --- a/ember-file-upload/tests/helpers/file-queue-helper-test.js +++ b/ember-file-upload/tests/helpers/file-queue-helper-test.js @@ -119,7 +119,7 @@ module('Integration | Helper | file-queue', function (hooks) { uploadHandler(function (/*schema, request*/) { // do sth }), - { timing: 2000 } + { timing: 200 } ); this.uploadImage = (file) => { diff --git a/ember-file-upload/tests/integration/components/file-dropzone-test.js b/ember-file-upload/tests/integration/components/file-dropzone-test.js index c79ce766..e81b677e 100644 --- a/ember-file-upload/tests/integration/components/file-dropzone-test.js +++ b/ember-file-upload/tests/integration/components/file-dropzone-test.js @@ -7,135 +7,177 @@ import { dragEnter, dragLeave, } from 'ember-file-upload/test-support'; +import Queue from 'ember-file-upload/queue'; module('Integration | Component | FileDropzone', function (hooks) { setupRenderingTest(hooks); - test('dropping a file calls onDrop', async function (assert) { - this.onDrop = (files) => files.forEach((file) => assert.step(file.name)); - - await render(hbs` - - `); - - await dragAndDrop('.test-dropzone', new File([], 'dingus.txt')); - - assert.verifySteps(['dingus.txt']); - }); - - test('only calls onFileAdd for files returned from onDrop', async function (assert) { - this.onDrop = (files) => { - assert.step(`onDrop: ${files.mapBy('name').join(',')}`); - return files.filter((f) => f.type.split('/')[0] === 'text'); - }; - this.onFileAdd = (file) => assert.step(`onFileAdd: ${file.name}`); - - await render(hbs` - - `); - - await dragAndDrop( - '.test-dropzone', - new File([], 'dingus.html', { type: 'text/html' }), - new File([], 'dingus.png', { type: 'image/png' }) - ); - - assert.verifySteps([ - 'onDrop: dingus.html,dingus.png', - 'onFileAdd: dingus.html', - ]); - }); - - test('dropping multiple files calls onDrop with one file', async function (assert) { - this.onDrop = (files) => files.forEach((file) => assert.step(file.name)); - - await render(hbs` - - `); - - await dragAndDrop( - '.test-dropzone', - new File([], 'dingus.txt'), - new File([], 'dingus.png') - ); - - assert.verifySteps(['dingus.txt']); - }); - - test('multiple=true dropping multiple files calls onDrop with both files', async function (assert) { - this.onDrop = (files) => files.forEach((file) => assert.step(file.name)); - - await render(hbs` - - `); - - await dragAndDrop( - '.test-dropzone', - new File([], 'dingus.txt'), - new File([], 'dingus.png') - ); - - assert.verifySteps(['dingus.txt', 'dingus.png']); - }); - - test('onDragEnter is called when a file is dragged over', async function (assert) { - this.onDragEnter = () => assert.step('onDragEnter'); - - await render(hbs` - - `); - - await dragEnter('.test-dropzone'); - - assert.verifySteps(['onDragEnter']); - }); - - test('onDragLeave is called when a file is dragged out', async function (assert) { - this.onDragLeave = () => assert.step('onDragLeave'); - - await render(hbs` - - `); - - await dragEnter('.test-dropzone', new File([], 'dingus.txt')); - await dragLeave('.test-dropzone', new File([], 'dingus.txt')); - - assert.verifySteps(['onDragLeave']); + module('new api', function (hooks) { + hooks.beforeEach(function () { + const fileQueueService = this.owner.lookup('service:file-queue'); + this.queue = new Queue({ name: 'test', fileQueue: fileQueueService }); + }); + + test('onDragEnter is called when a file is dragged over', async function (assert) { + this.onDragEnter = () => assert.step('onDragEnter'); + + await render(hbs` + + `); + + await dragEnter('.test-dropzone'); + + assert.verifySteps(['onDragEnter']); + }); + + test('filter and onDrop', async function (assert) { + this.filter = (file) => file.name.includes('.txt'); + this.onDrop = (files) => files.forEach((file) => assert.step(file.name)); + await render(hbs` + + `); + + await dragAndDrop( + '.test-dropzone', + new File([], 'dingus.txt'), + new File([], 'dangus.wmv'), + new File([], 'dongus.txt') + ); + + assert.verifySteps(['dingus.txt', 'dongus.txt']); + }); + + test('dropping a file calls onDrop', async function (assert) { + this.onDrop = (files) => files.forEach((file) => assert.step(file.name)); + + await render(hbs` + + `); + + await dragAndDrop('.test-dropzone', new File([], 'dingus.txt')); + + assert.verifySteps(['dingus.txt']); + }); + + test('onDragLeave is called when a file is dragged out', async function (assert) { + this.onDragLeave = () => assert.step('onDragLeave'); + + await render(hbs` + + `); + + await dragEnter('.test-dropzone', new File([], 'dingus.txt')); + await dragLeave('.test-dropzone', new File([], 'dingus.txt')); + + assert.verifySteps(['onDragLeave']); + }); + + test('yielded properties', async function (assert) { + await render(hbs` + +
{{dropzone.supported}}
+
{{dropzone.active}}
+
{{queue.name}}
+
+ `); + + assert.dom('.supported').hasText('true'); + assert.dom('.active').hasText('false'); + assert.dom('.queue-name').hasText('test'); + }); }); - test('yielded properties', async function (assert) { - await render(hbs` - -
{{dropzone.supported}}
-
{{dropzone.active}}
-
{{queue.name}}
-
- `); - - assert.dom('.supported').hasText('true'); - assert.dom('.active').hasText('false'); - assert.dom('.queue-name').hasText('test'); + module('deprecated api', function () { + test('dropping multiple files calls onFileAdd with each file', async function (assert) { + this.onFileAdd = (file) => assert.step(file.name); + + await render(hbs` + + `); + + await dragAndDrop( + '.test-dropzone', + new File([], 'dingus.txt'), + new File([], 'dingus.png') + ); + + assert.verifySteps(['dingus.txt', 'dingus.png']); + }); + + test('dropping multiple files calls onDrop with both files', async function (assert) { + this.onDrop = (files) => files.forEach((file) => assert.step(file.name)); + + await render(hbs` + + `); + + await dragAndDrop( + '.test-dropzone', + new File([], 'dingus.txt'), + new File([], 'dingus.png') + ); + + assert.verifySteps(['dingus.txt', 'dingus.png']); + }); + + test('multiple=true dropping multiple files calls onDrop with both files', async function (assert) { + this.onDrop = (files) => files.forEach((file) => assert.step(file.name)); + + await render(hbs` + + `); + + await dragAndDrop( + '.test-dropzone', + new File([], 'dingus.txt'), + new File([], 'dingus.png') + ); + + assert.verifySteps(['dingus.txt', 'dingus.png']); + }); + + test('multiple=false dropping multiple files calls onDrop with one file', async function (assert) { + this.onDrop = (files) => files.forEach((file) => assert.step(file.name)); + + await render(hbs` + + `); + + await dragAndDrop( + '.test-dropzone', + new File([], 'dingus.txt'), + new File([], 'dingus.png') + ); + + assert.verifySteps(['dingus.txt']); + }); }); }); diff --git a/ember-file-upload/tests/unit/system/data-transfer-test.js b/ember-file-upload/tests/unit/system/data-transfer-test.js deleted file mode 100644 index 6f5be1ca..00000000 --- a/ember-file-upload/tests/unit/system/data-transfer-test.js +++ /dev/null @@ -1,126 +0,0 @@ -import DataTransfer from 'ember-file-upload/system/data-transfer'; -import { module, test } from 'qunit'; - -module('data-transfer', function (hooks) { - hooks.beforeEach(function () { - this.subject = new DataTransfer({}); - }); - - hooks.afterEach(function () { - this.subject = null; - }); - - test('with no native dataTransfer', function (assert) { - assert.strictEqual(this.subject.files, null); - }); - - test('multiple=false; a single item being dragged', function (assert) { - this.subject.dataTransfer = { - items: [ - { - type: 'image/jpeg', - }, - ], - }; - assert.strictEqual(this.subject.files.length, 1); - }); - - test('multiple=false; a single file being dropped', function (assert) { - this.subject.dataTransfer = { - files: [ - { - name: 'tomster.jpg', - type: 'image/jpeg', - }, - ], - }; - assert.strictEqual(this.subject.files.length, 1); - }); - - test('multiple=false; multiple items being dragged', function (assert) { - this.subject.dataTransfer = { - items: [ - { - type: 'image/jpeg', - }, - { - type: 'image/png', - }, - ], - }; - assert.strictEqual(this.subject.files.length, 1); - }); - - test('multiple=false; multiple files being dropped', function (assert) { - this.subject.dataTransfer = { - files: [ - { - name: 'tomster.jpg', - type: 'image/jpeg', - }, - { - name: 'zoey.png', - type: 'image/png', - }, - ], - }; - assert.strictEqual(this.subject.files.length, 1); - }); - - test('multiple=true; a single item being dragged', function (assert) { - this.subject.dataTransfer = { - items: [ - { - type: 'image/jpeg', - }, - ], - }; - this.subject.queue = { multiple: true }; - assert.strictEqual(this.subject.files.length, 1); - }); - - test('multiple=true; a single file being dropped', function (assert) { - this.subject.dataTransfer = { - files: [ - { - name: 'tomster.jpg', - type: 'image/jpeg', - }, - ], - }; - this.subject.queue = { multiple: true }; - assert.strictEqual(this.subject.files.length, 1); - }); - - test('multiple=true; multiple items being dragged', function (assert) { - this.subject.dataTransfer = { - items: [ - { - type: 'image/jpeg', - }, - { - type: 'image/png', - }, - ], - }; - this.subject.queue = { multiple: true }; - assert.strictEqual(this.subject.files.length, 2); - }); - - test('multiple=true; multiple files being dropped', function (assert) { - this.subject.dataTransfer = { - files: [ - { - name: 'tomster.jpg', - type: 'image/jpeg', - }, - { - name: 'zoey.png', - type: 'image/png', - }, - ], - }; - this.subject.queue = { multiple: true }; - assert.strictEqual(this.subject.files.length, 2); - }); -}); diff --git a/ember-file-upload/tests/unit/system/data-transfer-wrapper-test.js b/ember-file-upload/tests/unit/system/data-transfer-wrapper-test.js new file mode 100644 index 00000000..0478b45b --- /dev/null +++ b/ember-file-upload/tests/unit/system/data-transfer-wrapper-test.js @@ -0,0 +1,69 @@ +import DataTransferWrapper from 'ember-file-upload/system/data-transfer-wrapper'; +import { module, test } from 'qunit'; + +module('data-transfer-wrapper', function (hooks) { + hooks.beforeEach(function () { + this.subject = new DataTransferWrapper({}); + }); + + hooks.afterEach(function () { + this.subject = null; + }); + + test('with no native dataTransfer', function (assert) { + assert.deepEqual(this.subject.files, []); + }); + + test('a single item being dragged', function (assert) { + this.subject.dataTransfer = { + items: [ + { + type: 'image/jpeg', + }, + ], + }; + assert.strictEqual(this.subject.filesOrItems.length, 1); + }); + + test('a single file being dropped', function (assert) { + this.subject.dataTransfer = { + files: [ + { + name: 'tomster.jpg', + type: 'image/jpeg', + }, + ], + }; + assert.strictEqual(this.subject.filesOrItems.length, 1); + }); + + test('multiple items being dragged', function (assert) { + this.subject.dataTransfer = { + items: [ + { + type: 'image/jpeg', + }, + { + type: 'image/png', + }, + ], + }; + assert.strictEqual(this.subject.filesOrItems.length, 2); + }); + + test('multiple files being dropped', function (assert) { + this.subject.dataTransfer = { + files: [ + { + name: 'tomster.jpg', + type: 'image/jpeg', + }, + { + name: 'zoey.png', + type: 'image/png', + }, + ], + }; + assert.strictEqual(this.subject.filesOrItems.length, 2); + }); +});